添加qiming-rcoder模块

This commit is contained in:
Codex
2026-06-01 13:54:52 +08:00
parent 8092c4b1f8
commit 4b1a580132
539 changed files with 151650 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
//! DuckDB 连接管理
//!
//! 提供线程安全的数据库连接管理
use crate::error::{DuckDbError, DuckDbResult};
use duckdb::Connection;
use parking_lot::Mutex;
use std::sync::Arc;
/// 线程安全的 DuckDB 连接包装器
///
/// 使用 `Arc<Mutex<Connection>>` 实现线程安全访问
#[derive(Clone)]
pub struct DuckDbConnection {
inner: Arc<Mutex<Connection>>,
}
impl DuckDbConnection {
/// DuckDB 内存限制默认值2GB
const DEFAULT_MEMORY_LIMIT: &str = "2GB";
/// 创建内存模式的数据库连接
pub fn open_in_memory() -> DuckDbResult<Self> {
let conn = Connection::open_in_memory()
.map_err(|e| DuckDbError::ConnectionError(e.to_string()))?;
// 设置 DuckDB 内存限制,防止无限占用内存
// 注意memory_limit 只限制 buffer manager部分数据结构如向量、查询结果
// 可能在 buffer manager 外部分配,因此实际内存可能略高于此限制
conn.execute(
&format!("PRAGMA memory_limit='{}'", Self::DEFAULT_MEMORY_LIMIT),
[],
)
.map_err(|e| DuckDbError::ConnectionError(format!("Failed to set memory_limit: {}", e)))?;
Ok(Self {
inner: Arc::new(Mutex::new(conn)),
})
}
/// 创建内存模式的数据库连接(带自定义内存限制)
///
/// # Arguments
/// * `memory_limit` - 内存限制字符串,如 "2GB", "512MB", "1GB"
pub fn open_in_memory_with_limit(memory_limit: &str) -> DuckDbResult<Self> {
let conn = Connection::open_in_memory()
.map_err(|e| DuckDbError::ConnectionError(e.to_string()))?;
// 设置自定义内存限制
conn.execute(
&format!("PRAGMA memory_limit='{}'", memory_limit),
[],
)
.map_err(|e| DuckDbError::ConnectionError(format!("Failed to set memory_limit: {}", e)))?;
Ok(Self {
inner: Arc::new(Mutex::new(conn)),
})
}
/// 执行需要独占连接的操作
///
/// 使用闭包模式确保锁的正确释放
pub fn with_connection<F, T>(&self, f: F) -> DuckDbResult<T>
where
F: FnOnce(&Connection) -> DuckDbResult<T>,
{
let conn = self.inner.lock();
f(&conn)
}
/// 执行需要可变连接的操作
pub fn with_connection_mut<F, T>(&self, f: F) -> DuckDbResult<T>
where
F: FnOnce(&mut Connection) -> DuckDbResult<T>,
{
let mut conn = self.inner.lock();
f(&mut conn)
}
/// 尝试获取连接(非阻塞)
pub fn try_with_connection<F, T>(&self, f: F) -> DuckDbResult<Option<T>>
where
F: FnOnce(&Connection) -> DuckDbResult<T>,
{
match self.inner.try_lock() {
Some(conn) => Ok(Some(f(&conn)?)),
None => Ok(None),
}
}
/// 获取 DuckDB 内存使用统计
///
/// 返回格式化的内存使用信息字符串,用于调试和监控
pub fn get_memory_stats(&self) -> DuckDbResult<String> {
self.with_connection(|c| {
// 查询内存使用情况
let mut stmt = c.prepare(
"SELECT name, size, reservation FROM duckdb_memory() ORDER BY size DESC LIMIT 10",
)?;
let mut rows = stmt.query([])?;
let mut result = String::from("DuckDB Memory Usage (Top 10):\n");
while let Some(row) = rows.next()? {
let name: String = row.get(0).unwrap_or_default();
let size: i64 = row.get(1).unwrap_or(0);
let reservation: i64 = row.get(2).unwrap_or(0);
result.push_str(&format!(
" {}: size={}, reservation={}\n",
name, size, reservation
));
}
Ok(result)
})
}
/// 执行事务
///
/// 自动处理事务的提交和回滚
pub fn transaction<F, T>(&self, f: F) -> DuckDbResult<T>
where
F: FnOnce(&Connection) -> DuckDbResult<T>,
{
let conn = self.inner.lock();
// 开始事务
conn.execute("BEGIN TRANSACTION", [])
.map_err(|e| DuckDbError::TransactionError(format!("failed to begin transaction: {}", e)))?;
// 执行操作
match f(&conn) {
Ok(result) => {
// 提交事务
conn.execute("COMMIT", [])
.map_err(|e| DuckDbError::TransactionError(format!("failed to commit transaction: {}", e)))?;
Ok(result)
}
Err(e) => {
// 回滚事务
let _ = conn.execute("ROLLBACK", []);
Err(e)
}
}
}
}
impl std::fmt::Debug for DuckDbConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DuckDbConnection")
.field("inner", &"<Connection>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_in_memory() {
let conn = DuckDbConnection::open_in_memory();
assert!(conn.is_ok());
}
#[test]
fn test_with_connection() {
let conn = DuckDbConnection::open_in_memory().unwrap();
let result = conn.with_connection(|c| {
c.execute("SELECT 1", [])?;
Ok(42)
});
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_transaction_commit() {
let conn = DuckDbConnection::open_in_memory().unwrap();
// 创建测试表
conn.with_connection(|c| {
c.execute("CREATE TABLE test (id INTEGER)", [])?;
Ok(())
})
.unwrap();
// 使用事务插入数据
conn.transaction(|c| {
c.execute("INSERT INTO test VALUES (1)", [])?;
Ok(())
})
.unwrap();
// 验证数据存在
let count: i32 = conn
.with_connection(|c| {
let mut stmt = c.prepare("SELECT COUNT(*) FROM test")?;
let mut rows = stmt.query([])?;
let row = rows.next()?.unwrap();
Ok(row.get(0)?)
})
.unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_transaction_rollback() {
let conn = DuckDbConnection::open_in_memory().unwrap();
// 创建测试表
conn.with_connection(|c| {
c.execute("CREATE TABLE test (id INTEGER)", [])?;
Ok(())
})
.unwrap();
// 使用事务插入数据,但返回错误触发回滚
let result: DuckDbResult<()> = conn.transaction(|c| {
c.execute("INSERT INTO test VALUES (1)", [])?;
Err(DuckDbError::InternalError("故意触发回滚".to_string()))
});
assert!(result.is_err());
// 验证数据不存在(已回滚)
let count: i32 = conn
.with_connection(|c| {
let mut stmt = c.prepare("SELECT COUNT(*) FROM test")?;
let mut rows = stmt.query([])?;
let row = rows.next()?.unwrap();
Ok(row.get(0)?)
})
.unwrap();
assert_eq!(count, 0);
}
}

View File

@@ -0,0 +1,68 @@
//! DuckDB Manager error types
//!
//! Defines error types for DuckDB storage manager
use thiserror::Error;
/// DuckDB storage manager error type
#[derive(Debug, Error)]
pub enum DuckDbError {
/// Database connection error
#[error("database connection error: {0}")]
ConnectionError(String),
/// SQL execution error
#[error("SQL execution error: {0}")]
QueryError(String),
/// Data not found
#[error("data not found: {entity} with {key} = {value}")]
NotFound {
entity: &'static str,
key: &'static str,
value: String,
},
/// Data already exists
#[error("data already exists: {entity} with {key} = {value}")]
AlreadyExists {
entity: &'static str,
key: &'static str,
value: String,
},
/// Transaction error
#[error("transaction error: {0}")]
TransactionError(String),
/// Serialization/deserialization error
#[error("serialization error: {0}")]
SerializationError(String),
/// Data integrity error
#[error("data integrity error: {0}")]
IntegrityError(String),
/// Initialization error
#[error("initialization error: {0}")]
InitializationError(String),
/// Internal error
#[error("internal error: {0}")]
InternalError(String),
}
impl From<duckdb::Error> for DuckDbError {
fn from(err: duckdb::Error) -> Self {
DuckDbError::QueryError(err.to_string())
}
}
impl From<serde_json::Error> for DuckDbError {
fn from(err: serde_json::Error) -> Self {
DuckDbError::SerializationError(err.to_string())
}
}
/// DuckDB 操作结果类型
pub type DuckDbResult<T> = Result<T, DuckDbError>;

View File

@@ -0,0 +1,78 @@
//! DuckDB Manager
//!
//! 提供基于 DuckDB 内存数据库的存储管理,用于替代 DashMap
//!
//! # 主要特性
//!
//! - **内存模式**: 使用 DuckDB 内存数据库,无需持久化
//! - **两表设计**: `containers` 和 `projects` 表(会话信息已合并到 projects
//! - **线程安全**: 使用 `Arc<Mutex<Connection>>` 实现线程安全访问
//! - **事务支持**: 对需要原子性的操作(如状态更新)提供事务支持
//!
//! # 示例
//!
//! ```rust,ignore
//! use duckdb_manager::{DuckDbManager, create_storage};
//! use duckdb_manager::models::{ContainerRecord, ProjectRecord};
//! use shared_types::ServiceType;
//!
//! // 创建存储
//! let storage = create_storage().unwrap();
//!
//! // 保存容器
//! let container = ContainerRecord::new(
//! "c1".to_string(),
//! "container-1".to_string(),
//! "127.0.0.1".to_string(),
//! 8080,
//! 8080,
//! ServiceType::RCoder,
//! "running".to_string(),
//! "http://localhost:8080".to_string(),
//! );
//! storage.save_container(&container).unwrap();
//!
//! // 保存项目
//! let project = ProjectRecord::new(
//! "p1".to_string(),
//! ServiceType::RCoder,
//! "c1".to_string(),
//! );
//! storage.save_project(&project).unwrap();
//!
//! // 更新会话
//! storage.update_session("p1", "session-1").unwrap();
//!
//! // 通过会话ID查询
//! let project = storage.get_project_by_session("session-1").unwrap();
//! ```
//!
//! # 模块结构
//!
//! - `connection`: 数据库连接管理
//! - `error`: 错误类型定义
//! - `models`: 数据模型定义
//! - `schema`: 数据库 Schema 定义和初始化
//! - `repositories`: 数据访问层Repository 模式)
//! - `manager`: 全局管理器
//! - `storage`: 统一存储接口
pub mod connection;
pub mod error;
pub mod manager;
pub mod models;
pub mod repositories;
pub mod schema;
pub mod storage;
// 重新导出常用类型
pub use connection::DuckDbConnection;
pub use error::{DuckDbError, DuckDbResult};
pub use manager::{DuckDbManager, get_global_manager, init_global_manager};
pub use models::{
CleanupResult, ContainerRecord, IdleContainerInfo, OrphanContainerInfo, ProjectRecord,
StorageStats,
};
pub use repositories::{ContainerRepository, ProjectRepository};
pub use schema::SchemaInitializer;
pub use storage::{DuckDbStorage, UnifiedStorage, create_storage};

View File

@@ -0,0 +1,287 @@
//! DuckDB 全局管理器
//!
//! 提供 DuckDB 存储的全局管理入口
use crate::connection::DuckDbConnection;
use crate::error::DuckDbResult;
use crate::models::StorageStats;
use crate::repositories::{ContainerRepository, ProjectRepository};
use crate::schema::SchemaInitializer;
use parking_lot::RwLock;
use std::sync::Arc;
/// DuckDB 全局管理器
///
/// 管理数据库连接和提供存储访问入口
pub struct DuckDbManager {
/// 数据库连接
conn: DuckDbConnection,
/// 是否已初始化
initialized: Arc<RwLock<bool>>,
}
impl DuckDbManager {
/// 创建新的 DuckDB 管理器(内存模式)
pub fn new_in_memory() -> DuckDbResult<Self> {
let conn = DuckDbConnection::open_in_memory()?;
let manager = Self {
conn,
initialized: Arc::new(RwLock::new(false)),
};
// 初始化 Schema
manager.initialize()?;
Ok(manager)
}
/// 初始化数据库 Schema
fn initialize(&self) -> DuckDbResult<()> {
let mut initialized = self.initialized.write();
if *initialized {
return Ok(());
}
SchemaInitializer::initialize(&self.conn)?;
*initialized = true;
tracing::info!("DuckDB Manager initialized");
Ok(())
}
/// 获取容器 Repository
///
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`
/// 确保并发访问时的线程安全。
pub fn containers(&self) -> DuckDbResult<ContainerRepository> {
Ok(ContainerRepository::new(self.conn.clone()))
}
/// 获取项目 Repository
///
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`
/// 确保并发访问时的线程安全。
pub fn projects(&self) -> DuckDbResult<ProjectRepository> {
Ok(ProjectRepository::new(self.conn.clone()))
}
/// 获取存储统计信息
pub fn get_stats(&self) -> DuckDbResult<StorageStats> {
let containers = self.containers()?;
let projects = self.projects()?;
let total_containers = containers.count()?;
let total_projects = projects.count()?;
let active_sessions = projects.count_active_sessions()?;
let projects_by_service_type = projects.count_by_service_type()?;
// 计算活跃和闲置容器
let all_containers = containers.find_all()?;
let idle_threshold_minutes = 30; // 30 分钟闲置阈值
let mut active_containers = 0;
let mut idle_containers = 0;
for container in &all_containers {
if container.is_idle(idle_threshold_minutes) {
idle_containers += 1;
} else {
active_containers += 1;
}
}
Ok(StorageStats {
total_containers,
total_projects,
active_sessions,
active_containers,
idle_containers,
projects_by_service_type,
})
}
/// 验证数据库状态
pub fn verify(&self) -> DuckDbResult<bool> {
SchemaInitializer::verify(&self.conn)
}
/// 获取原始连接(用于高级操作)
pub fn connection(&self) -> &DuckDbConnection {
&self.conn
}
/// 执行自定义 SQL 查询(用于复杂查询)
pub fn execute_query<F, T>(&self, f: F) -> DuckDbResult<T>
where
F: FnOnce(&duckdb::Connection) -> DuckDbResult<T>,
{
self.conn.with_connection(f)
}
/// 执行事务
pub fn transaction<F, T>(&self, f: F) -> DuckDbResult<T>
where
F: FnOnce(&duckdb::Connection) -> DuckDbResult<T>,
{
self.conn.transaction(f)
}
}
impl Clone for DuckDbManager {
fn clone(&self) -> Self {
// 注意:克隆后的 Manager 共享相同的底层连接
Self {
conn: self.conn.clone(),
initialized: self.initialized.clone(),
}
}
}
impl std::fmt::Debug for DuckDbManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DuckDbManager")
.field("initialized", &*self.initialized.read())
.finish()
}
}
/// 全局 DuckDB 管理器单例
static GLOBAL_MANAGER: std::sync::OnceLock<DuckDbManager> = std::sync::OnceLock::new();
/// 初始化全局 DuckDB 管理器
///
/// 应在应用启动时调用一次
pub fn init_global_manager() -> DuckDbResult<&'static DuckDbManager> {
// 尝试初始化,如果已初始化则返回现有实例
if let Some(manager) = GLOBAL_MANAGER.get() {
return Ok(manager);
}
let manager = DuckDbManager::new_in_memory()?;
// 尝试设置全局管理器,如果失败(已被其他线程设置)则返回现有实例
match GLOBAL_MANAGER.set(manager) {
Ok(()) => Ok(GLOBAL_MANAGER.get().unwrap()),
Err(_) => Ok(GLOBAL_MANAGER.get().unwrap()),
}
}
/// 获取全局 DuckDB 管理器
///
/// 如果未初始化,将自动初始化
pub fn get_global_manager() -> DuckDbResult<&'static DuckDbManager> {
match GLOBAL_MANAGER.get() {
Some(manager) => Ok(manager),
None => init_global_manager(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{ContainerRecord, ProjectRecord};
use shared_types::ServiceType;
#[test]
fn test_new_in_memory() {
let manager = DuckDbManager::new_in_memory().unwrap();
assert!(manager.verify().unwrap());
}
#[test]
fn test_get_stats_empty() {
let manager = DuckDbManager::new_in_memory().unwrap();
let stats = manager.get_stats().unwrap();
assert_eq!(stats.total_containers, 0);
assert_eq!(stats.total_projects, 0);
assert_eq!(stats.active_sessions, 0);
}
#[test]
fn test_get_stats_with_data() {
let manager = DuckDbManager::new_in_memory().unwrap();
// 添加容器
let containers = manager.containers().unwrap();
containers
.upsert(&ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
))
.unwrap();
// 添加项目
let projects = manager.projects().unwrap();
projects
.upsert(&ProjectRecord::new(
"p1".to_string(),
ServiceType::RCoder,
"c1".to_string(),
))
.unwrap();
// 添加会话
projects.update_session("p1", "session-1").unwrap();
let stats = manager.get_stats().unwrap();
assert_eq!(stats.total_containers, 1);
assert_eq!(stats.total_projects, 1);
assert_eq!(stats.active_sessions, 1);
}
#[test]
fn test_clone_shares_data() {
let manager = DuckDbManager::new_in_memory().unwrap();
// 使用原始 manager 添加数据
let containers = manager.containers().unwrap();
containers
.upsert(&ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
))
.unwrap();
// 克隆 manager
let cloned = manager.clone();
// 通过克隆的 manager 读取数据
let cloned_containers = cloned.containers().unwrap();
let found = cloned_containers.find_by_id("c1").unwrap();
assert!(found.is_some());
}
#[test]
fn test_transaction() {
let manager = DuckDbManager::new_in_memory().unwrap();
// 成功的事务
let result = manager.transaction(|conn| {
conn.execute(
"INSERT INTO containers (container_id, container_name, container_ip, internal_port, external_port, service_type, status, service_url, created_at, last_activity) VALUES ('c1', 'name', '127.0.0.1', 8080, 8080, 'rcoder', 'running', 'http://localhost', NOW(), NOW())",
[],
)?;
Ok(())
});
assert!(result.is_ok());
// 验证数据存在
let containers = manager.containers().unwrap();
assert!(containers.exists("c1").unwrap());
}
}

View File

@@ -0,0 +1,267 @@
//! DuckDB Manager 数据模型
//!
//! 定义数据库表对应的记录结构和辅助类型
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use shared_types::ServiceType;
/// 容器记录 - 对应 containers 表
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerRecord {
/// 容器ID (主键)
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 容器IP地址
pub container_ip: String,
/// 内部端口
pub internal_port: u16,
/// 外部端口
pub external_port: u16,
/// 服务类型
pub service_type: ServiceType,
/// 容器状态
pub status: String,
/// 服务URL
pub service_url: String,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 最后活动时间
pub last_activity: DateTime<Utc>,
}
impl ContainerRecord {
/// 创建新的容器记录
pub fn new(
container_id: String,
container_name: String,
container_ip: String,
internal_port: u16,
external_port: u16,
service_type: ServiceType,
status: String,
service_url: String,
) -> Self {
let now = Utc::now();
Self {
container_id,
container_name,
container_ip,
internal_port,
external_port,
service_type,
status,
service_url,
created_at: now,
last_activity: now,
}
}
/// 检查容器是否处于保护期(创建后 5 分钟内)
pub fn is_in_protection_period(&self, protection_minutes: i64) -> bool {
let elapsed = Utc::now().signed_duration_since(self.created_at);
elapsed.num_minutes() < protection_minutes
}
/// 检查容器是否闲置
pub fn is_idle(&self, idle_minutes: i64) -> bool {
let elapsed = Utc::now().signed_duration_since(self.last_activity);
elapsed.num_minutes() >= idle_minutes
}
}
/// 项目记录 - 对应 projects 表
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectRecord {
/// 项目ID (主键)
pub project_id: String,
/// 会话ID
pub session_id: Option<String>,
/// 服务类型
pub service_type: ServiceType,
/// 关联的容器ID
pub container_id: String,
/// 用户ID (ComputerAgentRunner 模式)
pub user_id: Option<String>,
/// Pod ID (共享容器模式)
pub pod_id: Option<String>,
/// Agent 状态码
pub agent_status_code: Option<i32>,
/// Agent 状态名称
pub agent_status_name: Option<String>,
/// 请求ID
pub request_id: Option<String>,
/// 模型提供商配置 (JSON)
pub model_provider_json: Option<String>,
/// 创建时间
pub created_at: DateTime<Utc>,
/// 最后活动时间
pub last_activity: DateTime<Utc>,
/// 会话创建时间
pub session_created_at: Option<DateTime<Utc>>,
/// 会话最后活动时间
pub session_last_activity: Option<DateTime<Utc>>,
}
impl ProjectRecord {
/// 创建新的项目记录
pub fn new(project_id: String, service_type: ServiceType, container_id: String) -> Self {
let now = Utc::now();
Self {
project_id,
session_id: None,
service_type,
container_id,
user_id: None,
pod_id: None,
agent_status_code: None,
agent_status_name: None,
request_id: None,
model_provider_json: None,
created_at: now,
last_activity: now,
session_created_at: None,
session_last_activity: None,
}
}
/// 创建带用户ID的项目记录 (ComputerAgentRunner 模式)
pub fn new_with_user_id(
project_id: String,
user_id: String,
service_type: ServiceType,
container_id: String,
) -> Self {
let now = Utc::now();
Self {
project_id,
session_id: None,
service_type,
container_id,
user_id: Some(user_id),
pod_id: None,
agent_status_code: None,
agent_status_name: None,
request_id: None,
model_provider_json: None,
created_at: now,
last_activity: now,
session_created_at: None,
session_last_activity: None,
}
}
/// 获取容器唯一标识
///
/// 根据 service_type 返回不同的标识符:
/// - RCoder 模式:返回 pod_id如果存在共享容器否则返回 project_id
/// - ComputerAgentRunner 模式:返回 user_id如果存在
pub fn container_key(&self) -> &str {
match self.service_type {
ServiceType::ComputerAgentRunner => self.user_id.as_deref().unwrap_or(&self.project_id),
ServiceType::RCoder => self.pod_id.as_deref().unwrap_or(&self.project_id),
}
}
}
/// 清理结果统计
#[derive(Debug, Clone, Default)]
pub struct CleanupResult {
/// 清理的容器数量
pub cleaned_containers: usize,
/// 清理的项目数量
pub cleaned_projects: usize,
/// 清理的孤立容器数量
pub orphan_containers: usize,
/// 清理的 gRPC 连接数量
pub grpc_connections: usize,
/// 清理的 VNC 后端数量
pub vnc_backends: usize,
/// 错误信息
pub errors: Vec<String>,
}
impl CleanupResult {
/// 创建新的清理结果
pub fn new() -> Self {
Self::default()
}
/// 添加错误信息
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
}
/// 合并另一个清理结果
pub fn merge(&mut self, other: CleanupResult) {
self.cleaned_containers += other.cleaned_containers;
self.cleaned_projects += other.cleaned_projects;
self.orphan_containers += other.orphan_containers;
self.grpc_connections += other.grpc_connections;
self.vnc_backends += other.vnc_backends;
self.errors.extend(other.errors);
}
/// 是否有错误
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
/// 总清理数量
pub fn total_cleaned(&self) -> usize {
self.cleaned_containers + self.cleaned_projects + self.orphan_containers
}
}
/// 存储统计信息
#[derive(Debug, Clone, Default)]
pub struct StorageStats {
/// 总容器数量
pub total_containers: usize,
/// 总项目数量
pub total_projects: usize,
/// 活跃会话数量
pub active_sessions: usize,
/// 活跃容器数量
pub active_containers: usize,
/// 闲置容器数量
pub idle_containers: usize,
/// 按服务类型统计的项目数量
pub projects_by_service_type: std::collections::HashMap<ServiceType, usize>,
}
impl StorageStats {
/// 创建新的统计信息
pub fn new() -> Self {
Self::default()
}
}
/// 闲置容器信息 - 用于清理任务
#[derive(Debug, Clone)]
pub struct IdleContainerInfo {
/// 容器ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 服务类型
pub service_type: ServiceType,
/// 闲置时长(分钟)
pub idle_minutes: i64,
/// 关联的项目ID列表
pub project_ids: Vec<String>,
}
/// 孤立容器信息 - 用于清理任务
#[derive(Debug, Clone)]
pub struct OrphanContainerInfo {
/// 容器ID
pub container_id: String,
/// 容器名称
pub container_name: String,
/// 服务类型
pub service_type: ServiceType,
/// 创建时间
pub created_at: DateTime<Utc>,
}

View File

@@ -0,0 +1,476 @@
//! 容器 Repository
//!
//! 提供容器表的数据访问操作
use crate::connection::DuckDbConnection;
use crate::error::{DuckDbError, DuckDbResult};
use crate::models::{ContainerRecord, IdleContainerInfo};
use chrono::{DateTime, Utc};
use duckdb::params;
use shared_types::ServiceType;
/// 容器 Repository
pub struct ContainerRepository {
conn: DuckDbConnection,
}
impl ContainerRepository {
/// 创建新的 ContainerRepository
pub fn new(conn: DuckDbConnection) -> Self {
Self { conn }
}
/// 插入或更新容器记录
pub fn upsert(&self, record: &ContainerRecord) -> DuckDbResult<()> {
let service_type_str = record.service_type.to_string();
let created_at_str = record.created_at.to_rfc3339();
let last_activity_str = record.last_activity.to_rfc3339();
self.conn.with_connection(|c| {
c.execute(
r#"
INSERT OR REPLACE INTO containers (
container_id, container_name, container_ip,
internal_port, external_port, service_type,
status, service_url, created_at, last_activity
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
params![
record.container_id,
record.container_name,
record.container_ip,
record.internal_port as i32,
record.external_port as i32,
service_type_str,
record.status,
record.service_url,
created_at_str,
last_activity_str,
],
)?;
Ok(())
})
}
/// 根据容器ID查找容器
pub fn find_by_id(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT container_id, container_name, container_ip,
internal_port, external_port, service_type,
status, service_url, created_at, last_activity
FROM containers
WHERE container_id = ?
"#,
)?;
let mut rows = stmt.query(params![container_id])?;
match rows.next()? {
Some(row) => Ok(Some(Self::row_to_record(row)?)),
None => Ok(None),
}
})
}
/// 删除容器
pub fn delete(&self, container_id: &str) -> DuckDbResult<bool> {
self.conn.with_connection(|c| {
let affected = c.execute(
"DELETE FROM containers WHERE container_id = ?",
params![container_id],
)?;
Ok(affected > 0)
})
}
/// 检查容器是否存在
pub fn exists(&self, container_id: &str) -> DuckDbResult<bool> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare("SELECT 1 FROM containers WHERE container_id = ? LIMIT 1")?;
let mut rows = stmt.query(params![container_id])?;
Ok(rows.next()?.is_some())
})
}
/// 更新容器最后活动时间
pub fn update_activity(&self, container_id: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
"UPDATE containers SET last_activity = ? WHERE container_id = ?",
params![now_str, container_id],
)?;
Ok(affected > 0)
})
}
/// 根据会话ID更新关联容器的最后活动时间
pub fn update_activity_by_session(&self, session_id: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE containers
SET last_activity = ?
WHERE container_id IN (
SELECT container_id FROM projects WHERE session_id = ?
)
"#,
params![now_str, session_id],
)?;
Ok(affected > 0)
})
}
/// 使用指定时间更新容器最后活动时间(用于保持项目和容器时间一致)
pub fn update_activity_with_time(
&self,
container_id: &str,
time: DateTime<Utc>,
) -> DuckDbResult<bool> {
let time_str = time.to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
"UPDATE containers SET last_activity = ? WHERE container_id = ?",
params![time_str, container_id],
)?;
Ok(affected > 0)
})
}
/// 更新容器状态
pub fn update_status(&self, container_id: &str, status: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
"UPDATE containers SET status = ?, last_activity = ? WHERE container_id = ?",
params![status, now_str, container_id],
)?;
Ok(affected > 0)
})
}
/// 获取所有容器
pub fn find_all(&self) -> DuckDbResult<Vec<ContainerRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT container_id, container_name, container_ip,
internal_port, external_port, service_type,
status, service_url, created_at, last_activity
FROM containers
ORDER BY created_at DESC
"#,
)?;
let mut rows = stmt.query([])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 按服务类型查找容器
pub fn find_by_service_type(
&self,
service_type: ServiceType,
) -> DuckDbResult<Vec<ContainerRecord>> {
let service_type_str = service_type.to_string();
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT container_id, container_name, container_ip,
internal_port, external_port, service_type,
status, service_url, created_at, last_activity
FROM containers
WHERE service_type = ?
ORDER BY created_at DESC
"#,
)?;
let mut rows = stmt.query(params![service_type_str])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 查找闲置容器(超过指定分钟数未活动且不在保护期内)
pub fn find_idle_containers(
&self,
idle_minutes: i64,
protection_minutes: i64,
) -> DuckDbResult<Vec<IdleContainerInfo>> {
self.conn.with_connection(|c| {
// 使用 DuckDB 的时间函数计算
let mut stmt = c.prepare(
r#"
SELECT c.container_id, c.container_name, c.service_type,
DATEDIFF('minute', c.last_activity, NOW()) as idle_mins
FROM containers c
WHERE DATEDIFF('minute', c.last_activity, NOW()) >= ?
AND DATEDIFF('minute', c.created_at, NOW()) >= ?
ORDER BY idle_mins DESC
"#,
)?;
let mut rows = stmt.query(params![idle_minutes, protection_minutes])?;
let mut results = Vec::new();
while let Some(row) = rows.next()? {
let container_id: String = row.get(0)?;
let container_name: String = row.get(1)?;
let service_type_str: String = row.get(2)?;
let idle_mins: i64 = row.get(3)?;
let service_type = service_type_str
.parse::<ServiceType>()
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
results.push(IdleContainerInfo {
container_id,
container_name,
service_type,
idle_minutes: idle_mins,
project_ids: Vec::new(), // 稍后填充
});
}
Ok(results)
})
}
/// 查找孤立容器(存在于 Docker 但不在数据库中)
///
/// 此方法用于与外部容器列表进行比对
pub fn find_orphan_containers(
&self,
docker_container_ids: &[String],
) -> DuckDbResult<Vec<String>> {
if docker_container_ids.is_empty() {
return Ok(Vec::new());
}
self.conn.with_connection(|c| {
// 获取数据库中所有容器 ID
let mut stmt = c.prepare("SELECT container_id FROM containers")?;
let mut rows = stmt.query([])?;
let mut db_container_ids = std::collections::HashSet::new();
while let Some(row) = rows.next()? {
let id: String = row.get(0)?;
db_container_ids.insert(id);
}
// 找出在 Docker 中存在但不在数据库中的容器
let orphans: Vec<String> = docker_container_ids
.iter()
.filter(|id| !db_container_ids.contains(*id))
.cloned()
.collect();
Ok(orphans)
})
}
/// 获取容器数量统计
pub fn count(&self) -> DuckDbResult<usize> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare("SELECT COUNT(*) FROM containers")?;
let mut rows = stmt.query([])?;
let row = rows
.next()?
.ok_or_else(|| DuckDbError::InternalError("unable to get container count".to_string()))?;
let count: i64 = row.get(0)?;
Ok(count as usize)
})
}
/// 按服务类型统计容器数量
pub fn count_by_service_type(
&self,
) -> DuckDbResult<std::collections::HashMap<ServiceType, usize>> {
self.conn.with_connection(|c| {
let mut stmt =
c.prepare("SELECT service_type, COUNT(*) FROM containers GROUP BY service_type")?;
let mut rows = stmt.query([])?;
let mut counts = std::collections::HashMap::new();
while let Some(row) = rows.next()? {
let service_type_str: String = row.get(0)?;
let count: i64 = row.get(1)?;
if let Ok(service_type) = service_type_str.parse::<ServiceType>() {
counts.insert(service_type, count as usize);
}
}
Ok(counts)
})
}
/// 从数据库行转换为 ContainerRecord
fn row_to_record(row: &duckdb::Row<'_>) -> DuckDbResult<ContainerRecord> {
let container_id: String = row.get(0)?;
let container_name: String = row.get(1)?;
let container_ip: String = row.get(2)?;
let internal_port: i32 = row.get(3)?;
let external_port: i32 = row.get(4)?;
let service_type_str: String = row.get(5)?;
let status: String = row.get(6)?;
let service_url: String = row.get(7)?;
// DuckDB 返回 TIMESTAMP 类型,需要使用 get_ref 并转换
let created_at = Self::get_timestamp_from_row(row, 8)?;
let last_activity = Self::get_timestamp_from_row(row, 9)?;
let service_type = service_type_str
.parse::<ServiceType>()
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
Ok(ContainerRecord {
container_id,
container_name,
container_ip,
internal_port: internal_port as u16,
external_port: external_port as u16,
service_type,
status,
service_url,
created_at,
last_activity,
})
}
/// 从行中获取时间戳
fn get_timestamp_from_row(row: &duckdb::Row<'_>, idx: usize) -> DuckDbResult<DateTime<Utc>> {
// DuckDB 将 TIMESTAMP 返回为微秒级 Unix 时间戳 (i64)
// 或者可能返回字符串,取决于存储方式
use duckdb::types::ValueRef;
let value_ref = row.get_ref(idx)?;
match value_ref {
ValueRef::Timestamp(_, micros) => {
// 微秒转秒和纳秒
let secs = micros / 1_000_000;
let nsecs = ((micros % 1_000_000) * 1000) as u32;
Ok(DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now))
}
ValueRef::Text(bytes) => {
let s = std::str::from_utf8(bytes)
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
}
_ => Ok(Utc::now()), // 默认返回当前时间
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::SchemaInitializer;
fn setup_test_db() -> ContainerRepository {
let conn = DuckDbConnection::open_in_memory().unwrap();
SchemaInitializer::initialize(&conn).unwrap();
ContainerRepository::new(conn)
}
fn create_test_record(id: &str) -> ContainerRecord {
ContainerRecord::new(
id.to_string(),
format!("container-{}", id),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
format!("http://localhost:8080/{}", id),
)
}
#[test]
fn test_upsert_and_find() {
let repo = setup_test_db();
let record = create_test_record("c1");
repo.upsert(&record).unwrap();
let found = repo.find_by_id("c1").unwrap();
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.container_id, "c1");
assert_eq!(found.container_name, "container-c1");
}
#[test]
fn test_delete() {
let repo = setup_test_db();
let record = create_test_record("c1");
repo.upsert(&record).unwrap();
assert!(repo.exists("c1").unwrap());
repo.delete("c1").unwrap();
assert!(!repo.exists("c1").unwrap());
}
#[test]
fn test_update_activity() {
let repo = setup_test_db();
let record = create_test_record("c1");
repo.upsert(&record).unwrap();
let before = repo.find_by_id("c1").unwrap().unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
repo.update_activity("c1").unwrap();
let after = repo.find_by_id("c1").unwrap().unwrap();
assert!(after.last_activity >= before.last_activity);
}
#[test]
fn test_count() {
let repo = setup_test_db();
assert_eq!(repo.count().unwrap(), 0);
repo.upsert(&create_test_record("c1")).unwrap();
repo.upsert(&create_test_record("c2")).unwrap();
assert_eq!(repo.count().unwrap(), 2);
}
#[test]
fn test_find_by_service_type() {
let repo = setup_test_db();
let mut rcoder = create_test_record("c1");
rcoder.service_type = ServiceType::RCoder;
let mut agent = create_test_record("c2");
agent.service_type = ServiceType::ComputerAgentRunner;
repo.upsert(&rcoder).unwrap();
repo.upsert(&agent).unwrap();
let rcoder_containers = repo.find_by_service_type(ServiceType::RCoder).unwrap();
assert_eq!(rcoder_containers.len(), 1);
assert_eq!(rcoder_containers[0].container_id, "c1");
}
}

View File

@@ -0,0 +1,9 @@
//! Repository 层模块
//!
//! 提供数据访问抽象层
mod container;
mod project;
pub use container::ContainerRepository;
pub use project::ProjectRepository;

View File

@@ -0,0 +1,867 @@
//! 项目 Repository
//!
//! 提供项目表的数据访问操作
use crate::connection::DuckDbConnection;
use crate::error::{DuckDbError, DuckDbResult};
use crate::models::ProjectRecord;
use chrono::{DateTime, Utc};
use duckdb::params;
use shared_types::ServiceType;
/// 项目 Repository
pub struct ProjectRepository {
conn: DuckDbConnection,
}
impl ProjectRepository {
/// 创建新的 ProjectRepository
pub fn new(conn: DuckDbConnection) -> Self {
Self { conn }
}
/// 插入或更新项目记录
pub fn upsert(&self, record: &ProjectRecord) -> DuckDbResult<()> {
let service_type_str = record.service_type.to_string();
let created_at_str = record.created_at.to_rfc3339();
let last_activity_str = record.last_activity.to_rfc3339();
let session_created_at_str = record.session_created_at.map(|dt| dt.to_rfc3339());
let session_last_activity_str = record.session_last_activity.map(|dt| dt.to_rfc3339());
self.conn.with_connection(|c| {
c.execute(
r#"
INSERT OR REPLACE INTO projects (
project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
params![
record.project_id,
record.session_id,
service_type_str,
record.container_id,
record.user_id,
record.pod_id,
record.agent_status_code,
record.agent_status_name,
record.request_id,
record.model_provider_json,
created_at_str,
last_activity_str,
session_created_at_str,
session_last_activity_str,
],
)?;
Ok(())
})
}
/// 根据项目ID查找项目
pub fn find_by_id(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE project_id = ?
"#,
)?;
let mut rows = stmt.query(params![project_id])?;
match rows.next()? {
Some(row) => Ok(Some(Self::row_to_record(row)?)),
None => Ok(None),
}
})
}
/// 根据会话ID查找项目
pub fn find_by_session_id(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE session_id = ?
"#,
)?;
let mut rows = stmt.query(params![session_id])?;
match rows.next()? {
Some(row) => Ok(Some(Self::row_to_record(row)?)),
None => Ok(None),
}
})
}
/// 根据用户ID查找所有项目 (ComputerAgentRunner 模式)
///
/// 返回该用户的所有项目记录,按最后活动时间倒序排列
pub fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE user_id = ?
ORDER BY last_activity DESC
"#,
)?;
let mut rows = stmt.query(params![user_id])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 获取用户最新活跃项目的容器ID (ComputerAgentRunner 核心用例)
///
/// 在 ComputerAgentRunner 模式下,一个用户对应一个容器,
/// 此方法返回该用户最近活跃项目关联的容器ID
pub fn get_latest_container_id_by_user_id(
&self,
user_id: &str,
) -> DuckDbResult<Option<String>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT container_id
FROM projects
WHERE user_id = ?
ORDER BY last_activity DESC
LIMIT 1
"#,
)?;
let mut rows = stmt.query(params![user_id])?;
match rows.next()? {
Some(row) => {
let container_id: String = row.get(0)?;
Ok(Some(container_id))
}
None => Ok(None),
}
})
}
/// 根据 pod_id 查找所有项目 (RCoder 共享容器模式)
///
/// 在 RCoder 共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
/// 返回该 Pod 下所有项目记录,按最后活动时间倒序排列
pub fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE pod_id = ?
ORDER BY last_activity DESC
"#,
)?;
let mut rows = stmt.query(params![pod_id])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 获取 Pod 最新活跃项目的容器ID (共享容器模式)
///
/// 在共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
/// 此方法返回该 Pod 下最近活跃项目关联的容器ID
pub fn get_latest_container_id_by_pod_id(
&self,
pod_id: &str,
) -> DuckDbResult<Option<String>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT container_id
FROM projects
WHERE pod_id = ?
ORDER BY last_activity DESC
LIMIT 1
"#,
)?;
let mut rows = stmt.query(params![pod_id])?;
match rows.next()? {
Some(row) => {
let container_id: String = row.get(0)?;
Ok(Some(container_id))
}
None => Ok(None),
}
})
}
/// 根据容器ID查找所有关联的项目
pub fn find_by_container_id(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE container_id = ?
"#,
)?;
let mut rows = stmt.query(params![container_id])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 删除项目
pub fn delete(&self, project_id: &str) -> DuckDbResult<bool> {
self.conn.with_connection(|c| {
let affected = c.execute(
"DELETE FROM projects WHERE project_id = ?",
params![project_id],
)?;
Ok(affected > 0)
})
}
/// 根据容器ID删除所有关联的项目
pub fn delete_by_container_id(&self, container_id: &str) -> DuckDbResult<usize> {
self.conn.with_connection(|c| {
let affected = c.execute(
"DELETE FROM projects WHERE container_id = ?",
params![container_id],
)?;
Ok(affected)
})
}
/// 检查项目是否存在
pub fn exists(&self, project_id: &str) -> DuckDbResult<bool> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare("SELECT 1 FROM projects WHERE project_id = ? LIMIT 1")?;
let mut rows = stmt.query(params![project_id])?;
Ok(rows.next()?.is_some())
})
}
/// 更新项目最后活动时间,返回实际更新使用的时间戳
pub fn update_activity(&self, project_id: &str) -> DuckDbResult<Option<DateTime<Utc>>> {
let now = Utc::now();
let now_str = now.to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
"UPDATE projects SET last_activity = ? WHERE project_id = ?",
params![now_str, project_id],
)?;
Ok(if affected > 0 { Some(now) } else { None })
})
}
/// 更新会话信息
pub fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET session_id = ?,
last_activity = ?,
session_created_at = COALESCE(session_created_at, ?),
session_last_activity = ?
WHERE project_id = ?
"#,
params![session_id, now_str, now_str, now_str, project_id],
)?;
Ok(affected > 0)
})
}
/// 清除会话信息(将 session_id 设置为 NULL
///
/// 用于 Agent 停止后清理会话状态
pub fn clear_session(&self, project_id: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET session_id = NULL,
last_activity = ?,
session_last_activity = ?
WHERE project_id = ?
"#,
params![now_str, now_str, project_id],
)?;
Ok(affected > 0)
})
}
/// 更新会话活动时间
pub fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET last_activity = ?,
session_last_activity = ?
WHERE session_id = ?
"#,
params![now_str, now_str, session_id],
)?;
Ok(affected > 0)
})
}
/// 原子更新 Agent 状态(使用事务)
///
/// 注意:此方法不会更新 last_activity只更新状态字段
/// 这是为了避免每 30 秒的状态检查刷新活动时间,导致闲置容器永远不会被清理
pub fn update_status_atomic(
&self,
project_id: &str,
status_code: i32,
status_name: &str,
) -> DuckDbResult<bool> {
self.conn.transaction(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET agent_status_code = ?,
agent_status_name = ?
WHERE project_id = ?
"#,
params![status_code, status_name, project_id],
)?;
Ok(affected > 0)
})
}
/// 根据会话ID获取容器名称
///
/// 与 `get_container_id_by_session` 不同,此方法返回 `container_name` 而不是 `container_id`。
/// 容器名称是稳定的(如 `computer-agent-runner-user_123`),即使容器被重建,
/// 可以通过 Docker API 查询到新容器。而 container_id 在容器重建后会改变。
///
/// # 用途
/// 用于 SSE 连接验证时,通过 container_name 实时查询 Docker API 获取容器状态。
pub fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT c.container_name
FROM projects p
JOIN containers c ON p.container_id = c.container_id
WHERE p.session_id = ?
"#,
)?;
let mut rows = stmt.query(params![session_id])?;
match rows.next()? {
Some(row) => {
let container_name: String = row.get(0)?;
Ok(Some(container_name))
}
None => Ok(None),
}
})
}
/// 查找需要清理的项目(闲置超过指定分钟数)
pub fn find_projects_for_cleanup(&self, idle_minutes: i64) -> DuckDbResult<Vec<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE DATEDIFF('minute', last_activity, NOW()) >= ?
"#,
)?;
let mut rows = stmt.query(params![idle_minutes])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 获取所有项目
pub fn find_all(&self) -> DuckDbResult<Vec<ProjectRecord>> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
ORDER BY created_at DESC
"#,
)?;
let mut rows = stmt.query([])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 按服务类型查找项目
pub fn find_by_service_type(
&self,
service_type: ServiceType,
) -> DuckDbResult<Vec<ProjectRecord>> {
let service_type_str = service_type.to_string();
self.conn.with_connection(|c| {
let mut stmt = c.prepare(
r#"
SELECT project_id, session_id, service_type, container_id,
user_id, pod_id, agent_status_code, agent_status_name,
request_id, model_provider_json, created_at, last_activity,
session_created_at, session_last_activity
FROM projects
WHERE service_type = ?
ORDER BY created_at DESC
"#,
)?;
let mut rows = stmt.query(params![service_type_str])?;
let mut records = Vec::new();
while let Some(row) = rows.next()? {
records.push(Self::row_to_record(row)?);
}
Ok(records)
})
}
/// 获取项目数量统计
pub fn count(&self) -> DuckDbResult<usize> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare("SELECT COUNT(*) FROM projects")?;
let mut rows = stmt.query([])?;
let row = rows
.next()?
.ok_or_else(|| DuckDbError::InternalError("unable to get project count".to_string()))?;
let count: i64 = row.get(0)?;
Ok(count as usize)
})
}
/// 获取活跃会话数量
pub fn count_active_sessions(&self) -> DuckDbResult<usize> {
self.conn.with_connection(|c| {
let mut stmt =
c.prepare("SELECT COUNT(*) FROM projects WHERE session_id IS NOT NULL")?;
let mut rows = stmt.query([])?;
let row = rows
.next()?
.ok_or_else(|| DuckDbError::InternalError("unable to get session count".to_string()))?;
let count: i64 = row.get(0)?;
Ok(count as usize)
})
}
/// 按服务类型统计项目数量
pub fn count_by_service_type(
&self,
) -> DuckDbResult<std::collections::HashMap<ServiceType, usize>> {
self.conn.with_connection(|c| {
let mut stmt =
c.prepare("SELECT service_type, COUNT(*) FROM projects GROUP BY service_type")?;
let mut rows = stmt.query([])?;
let mut counts = std::collections::HashMap::new();
while let Some(row) = rows.next()? {
let service_type_str: String = row.get(0)?;
let count: i64 = row.get(1)?;
if let Ok(service_type) = service_type_str.parse::<ServiceType>() {
counts.insert(service_type, count as usize);
}
}
Ok(counts)
})
}
/// 更新模型提供商配置
pub fn update_model_provider(
&self,
project_id: &str,
model_provider_json: Option<&str>,
) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET model_provider_json = ?,
last_activity = ?
WHERE project_id = ?
"#,
params![model_provider_json, now_str, project_id],
)?;
Ok(affected > 0)
})
}
/// 更新请求ID
pub fn update_request_id(
&self,
project_id: &str,
request_id: Option<&str>,
) -> DuckDbResult<bool> {
let now_str = Utc::now().to_rfc3339();
self.conn.with_connection(|c| {
let affected = c.execute(
r#"
UPDATE projects
SET request_id = ?,
last_activity = ?
WHERE project_id = ?
"#,
params![request_id, now_str, project_id],
)?;
Ok(affected > 0)
})
}
/// 从数据库行转换为 ProjectRecord
fn row_to_record(row: &duckdb::Row<'_>) -> DuckDbResult<ProjectRecord> {
let project_id: String = row.get(0)?;
let session_id: Option<String> = row.get(1)?;
let service_type_str: String = row.get(2)?;
let container_id: String = row.get(3)?;
let user_id: Option<String> = row.get(4)?;
let pod_id: Option<String> = row.get(5)?;
let agent_status_code: Option<i32> = row.get(6)?;
let agent_status_name: Option<String> = row.get(7)?;
let request_id: Option<String> = row.get(8)?;
let model_provider_json: Option<String> = row.get(9)?;
// DuckDB 返回 TIMESTAMP 类型,需要使用 get_ref 并转换
let created_at = Self::get_timestamp_from_row(row, 10)?;
let last_activity = Self::get_timestamp_from_row(row, 11)?;
let session_created_at = Self::get_optional_timestamp_from_row(row, 12)?;
let session_last_activity = Self::get_optional_timestamp_from_row(row, 13)?;
let service_type = service_type_str
.parse::<ServiceType>()
.map_err(|e| DuckDbError::InternalError(format!("failed to parse service type: {}", e)))?;
Ok(ProjectRecord {
project_id,
session_id,
service_type,
container_id,
user_id,
pod_id,
agent_status_code,
agent_status_name,
request_id,
model_provider_json,
created_at,
last_activity,
session_created_at,
session_last_activity,
})
}
/// 从行中获取时间戳
fn get_timestamp_from_row(row: &duckdb::Row<'_>, idx: usize) -> DuckDbResult<DateTime<Utc>> {
use duckdb::types::ValueRef;
let value_ref = row.get_ref(idx)?;
match value_ref {
ValueRef::Timestamp(_, micros) => {
let secs = micros / 1_000_000;
let nsecs = ((micros % 1_000_000) * 1000) as u32;
Ok(DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now))
}
ValueRef::Text(bytes) => {
let s = std::str::from_utf8(bytes)
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
DateTime::parse_from_rfc3339(s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
}
_ => Ok(Utc::now()),
}
}
/// 从行中获取可选时间戳
fn get_optional_timestamp_from_row(
row: &duckdb::Row<'_>,
idx: usize,
) -> DuckDbResult<Option<DateTime<Utc>>> {
use duckdb::types::ValueRef;
let value_ref = row.get_ref(idx)?;
match value_ref {
ValueRef::Null => Ok(None),
ValueRef::Timestamp(_, micros) => {
let secs = micros / 1_000_000;
let nsecs = ((micros % 1_000_000) * 1000) as u32;
Ok(Some(
DateTime::from_timestamp(secs, nsecs).unwrap_or_else(Utc::now),
))
}
ValueRef::Text(bytes) => {
let s = std::str::from_utf8(bytes)
.map_err(|e| DuckDbError::InternalError(format!("UTF8 parsing failed: {}", e)))?;
DateTime::parse_from_rfc3339(s)
.map(|dt| Some(dt.with_timezone(&Utc)))
.map_err(|e| DuckDbError::InternalError(format!("timestamp parsing failed: {}", e)))
}
_ => Ok(None),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::SchemaInitializer;
fn setup_test_db() -> ProjectRepository {
let conn = DuckDbConnection::open_in_memory().unwrap();
SchemaInitializer::initialize(&conn).unwrap();
ProjectRepository::new(conn)
}
fn create_test_record(id: &str) -> ProjectRecord {
ProjectRecord::new(
id.to_string(),
ServiceType::RCoder,
format!("container-{}", id),
)
}
#[test]
fn test_upsert_and_find() {
let repo = setup_test_db();
let record = create_test_record("p1");
repo.upsert(&record).unwrap();
let found = repo.find_by_id("p1").unwrap();
assert!(found.is_some());
let found = found.unwrap();
assert_eq!(found.project_id, "p1");
assert_eq!(found.container_id, "container-p1");
}
#[test]
fn test_update_session() {
let repo = setup_test_db();
let record = create_test_record("p1");
repo.upsert(&record).unwrap();
repo.update_session("p1", "session-1").unwrap();
let found = repo.find_by_id("p1").unwrap().unwrap();
assert_eq!(found.session_id, Some("session-1".to_string()));
assert!(found.session_created_at.is_some());
}
#[test]
fn test_find_by_session_id() {
let repo = setup_test_db();
let record = create_test_record("p1");
repo.upsert(&record).unwrap();
repo.update_session("p1", "session-1").unwrap();
let found = repo.find_by_session_id("session-1").unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().project_id, "p1");
}
#[test]
fn test_update_status_atomic() {
let repo = setup_test_db();
let record = create_test_record("p1");
repo.upsert(&record).unwrap();
repo.update_status_atomic("p1", 1, "running").unwrap();
let found = repo.find_by_id("p1").unwrap().unwrap();
assert_eq!(found.agent_status_code, Some(1));
assert_eq!(found.agent_status_name, Some("running".to_string()));
}
#[test]
fn test_delete() {
let repo = setup_test_db();
let record = create_test_record("p1");
repo.upsert(&record).unwrap();
assert!(repo.exists("p1").unwrap());
repo.delete("p1").unwrap();
assert!(!repo.exists("p1").unwrap());
}
#[test]
fn test_delete_by_container_id() {
let repo = setup_test_db();
let mut p1 = create_test_record("p1");
p1.container_id = "c1".to_string();
let mut p2 = create_test_record("p2");
p2.container_id = "c1".to_string();
let mut p3 = create_test_record("p3");
p3.container_id = "c2".to_string();
repo.upsert(&p1).unwrap();
repo.upsert(&p2).unwrap();
repo.upsert(&p3).unwrap();
let deleted = repo.delete_by_container_id("c1").unwrap();
assert_eq!(deleted, 2);
assert!(!repo.exists("p1").unwrap());
assert!(!repo.exists("p2").unwrap());
assert!(repo.exists("p3").unwrap());
}
#[test]
fn test_count_active_sessions() {
let repo = setup_test_db();
repo.upsert(&create_test_record("p1")).unwrap();
repo.upsert(&create_test_record("p2")).unwrap();
repo.upsert(&create_test_record("p3")).unwrap();
assert_eq!(repo.count_active_sessions().unwrap(), 0);
repo.update_session("p1", "session-1").unwrap();
repo.update_session("p2", "session-2").unwrap();
assert_eq!(repo.count_active_sessions().unwrap(), 2);
}
#[test]
fn test_find_projects_by_user_id() {
let repo = setup_test_db();
let record1 = ProjectRecord::new_with_user_id(
"p1".to_string(),
"user-1".to_string(),
ServiceType::ComputerAgentRunner,
"c1".to_string(),
);
let record2 = ProjectRecord::new_with_user_id(
"p2".to_string(),
"user-1".to_string(),
ServiceType::ComputerAgentRunner,
"c1".to_string(),
);
repo.upsert(&record1).unwrap();
repo.upsert(&record2).unwrap();
let found = repo.find_projects_by_user_id("user-1").unwrap();
assert_eq!(found.len(), 2);
// 验证按 last_activity 倒序排列
let project_ids: Vec<&str> = found.iter().map(|r| r.project_id.as_str()).collect();
assert!(project_ids.contains(&"p1"));
assert!(project_ids.contains(&"p2"));
}
#[test]
fn test_get_latest_container_id_by_user_id() {
let repo = setup_test_db();
// 用户没有项目时返回 None
let container_id = repo.get_latest_container_id_by_user_id("user-1").unwrap();
assert!(container_id.is_none());
// 创建项目
let record = ProjectRecord::new_with_user_id(
"p1".to_string(),
"user-1".to_string(),
ServiceType::ComputerAgentRunner,
"c1".to_string(),
);
repo.upsert(&record).unwrap();
// 现在应该返回容器ID
let container_id = repo.get_latest_container_id_by_user_id("user-1").unwrap();
assert_eq!(container_id, Some("c1".to_string()));
}
#[test]
fn test_get_latest_container_id_by_pod_id() {
let repo = setup_test_db();
// Pod 没有项目时返回 None
let container_id = repo.get_latest_container_id_by_pod_id("pod-1").unwrap();
assert!(container_id.is_none());
// 创建带 pod_id 的项目
let mut record = ProjectRecord::new_with_user_id(
"p1".to_string(),
"user-1".to_string(),
ServiceType::ComputerAgentRunner,
"c1".to_string(),
);
record.pod_id = Some("pod-1".to_string());
repo.upsert(&record).unwrap();
// 现在应该返回容器ID
let container_id = repo.get_latest_container_id_by_pod_id("pod-1").unwrap();
assert_eq!(container_id, Some("c1".to_string()));
}
}

View File

@@ -0,0 +1,204 @@
//! DuckDB Schema 定义和初始化
//!
//! 包含数据库表结构定义和初始化逻辑
use crate::connection::DuckDbConnection;
use crate::error::{DuckDbError, DuckDbResult};
/// 容器表 DDL
const CREATE_CONTAINERS_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS containers (
container_id VARCHAR PRIMARY KEY,
container_name VARCHAR NOT NULL,
container_ip VARCHAR NOT NULL,
internal_port INTEGER NOT NULL,
external_port INTEGER NOT NULL,
service_type VARCHAR NOT NULL,
status VARCHAR NOT NULL,
service_url VARCHAR NOT NULL,
created_at TIMESTAMP NOT NULL,
last_activity TIMESTAMP NOT NULL
)
"#;
/// 项目表 DDL
const CREATE_PROJECTS_TABLE: &str = r#"
CREATE TABLE IF NOT EXISTS projects (
project_id VARCHAR PRIMARY KEY,
session_id VARCHAR,
service_type VARCHAR NOT NULL,
container_id VARCHAR NOT NULL,
user_id VARCHAR,
pod_id VARCHAR,
agent_status_code INTEGER,
agent_status_name VARCHAR,
request_id VARCHAR,
model_provider_json VARCHAR,
created_at TIMESTAMP NOT NULL,
last_activity TIMESTAMP NOT NULL,
session_created_at TIMESTAMP,
session_last_activity TIMESTAMP
)
"#;
/// 容器表索引
const CREATE_CONTAINERS_INDEXES: &[&str] = &[
"CREATE INDEX IF NOT EXISTS idx_containers_service_type ON containers(service_type)",
"CREATE INDEX IF NOT EXISTS idx_containers_status ON containers(status)",
"CREATE INDEX IF NOT EXISTS idx_containers_last_activity ON containers(last_activity)",
];
/// 项目表索引
const CREATE_PROJECTS_INDEXES: &[&str] = &[
"CREATE INDEX IF NOT EXISTS idx_projects_session_id ON projects(session_id)",
"CREATE INDEX IF NOT EXISTS idx_projects_container_id ON projects(container_id)",
"CREATE INDEX IF NOT EXISTS idx_projects_user_id ON projects(user_id)",
"CREATE INDEX IF NOT EXISTS idx_projects_pod_id ON projects(pod_id)",
"CREATE INDEX IF NOT EXISTS idx_projects_service_type ON projects(service_type)",
"CREATE INDEX IF NOT EXISTS idx_projects_last_activity ON projects(last_activity)",
];
/// Schema 初始化器
pub struct SchemaInitializer;
impl SchemaInitializer {
/// 初始化数据库 Schema
///
/// 创建所有必需的表和索引
pub fn initialize(conn: &DuckDbConnection) -> DuckDbResult<()> {
conn.with_connection(|c| {
// 创建表
c.execute(CREATE_CONTAINERS_TABLE, []).map_err(|e| {
DuckDbError::InitializationError(format!("failed to create containers table: {}", e))
})?;
c.execute(CREATE_PROJECTS_TABLE, []).map_err(|e| {
DuckDbError::InitializationError(format!("failed to create projects table: {}", e))
})?;
// 创建容器表索引
for sql in CREATE_CONTAINERS_INDEXES {
c.execute(sql, []).map_err(|e| {
DuckDbError::InitializationError(format!("failed to create containers table index: {}", e))
})?;
}
// 创建项目表索引
for sql in CREATE_PROJECTS_INDEXES {
c.execute(sql, []).map_err(|e| {
DuckDbError::InitializationError(format!("failed to create projects table index: {}", e))
})?;
}
tracing::info!("DuckDB Schema initialized");
Ok(())
})
}
/// 重置数据库(仅用于测试)
#[cfg(test)]
pub fn reset(conn: &DuckDbConnection) -> DuckDbResult<()> {
conn.with_connection(|c| {
c.execute("DROP TABLE IF EXISTS projects", [])?;
c.execute("DROP TABLE IF EXISTS containers", [])?;
Ok(())
})?;
Self::initialize(conn)
}
/// 验证 Schema 是否正确初始化
pub fn verify(conn: &DuckDbConnection) -> DuckDbResult<bool> {
conn.with_connection(|c| {
// 检查 containers 表
let containers_exists: i32 = {
let mut stmt = c.prepare(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'containers'"
)?;
let mut rows = stmt.query([])?;
let row = rows
.next()?
.ok_or_else(|| DuckDbError::InternalError("unable to query table info".to_string()))?;
row.get(0)?
};
// 检查 projects 表
let projects_exists: i32 = {
let mut stmt = c.prepare(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'projects'",
)?;
let mut rows = stmt.query([])?;
let row = rows
.next()?
.ok_or_else(|| DuckDbError::InternalError("unable to query table info".to_string()))?;
row.get(0)?
};
Ok(containers_exists > 0 && projects_exists > 0)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initialize_schema() {
let conn = DuckDbConnection::open_in_memory().unwrap();
let result = SchemaInitializer::initialize(&conn);
assert!(result.is_ok());
}
#[test]
fn test_verify_schema() {
let conn = DuckDbConnection::open_in_memory().unwrap();
SchemaInitializer::initialize(&conn).unwrap();
let verified = SchemaInitializer::verify(&conn).unwrap();
assert!(verified);
}
#[test]
fn test_reset_schema() {
let conn = DuckDbConnection::open_in_memory().unwrap();
SchemaInitializer::initialize(&conn).unwrap();
// 插入一些数据
conn.with_connection(|c| {
c.execute(
"INSERT INTO containers (container_id, container_name, container_ip, internal_port, external_port, service_type, status, service_url, created_at, last_activity) VALUES ('c1', 'name1', '127.0.0.1', 8080, 8080, 'rcoder', 'running', 'http://localhost', NOW(), NOW())",
[],
)?;
Ok(())
}).unwrap();
// 重置
SchemaInitializer::reset(&conn).unwrap();
// 验证数据已清空
let count: i32 = conn
.with_connection(|c| {
let mut stmt = c.prepare("SELECT COUNT(*) FROM containers")?;
let mut rows = stmt.query([])?;
let row = rows.next()?.unwrap();
Ok(row.get(0)?)
})
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_idempotent_initialization() {
let conn = DuckDbConnection::open_in_memory().unwrap();
// 多次初始化应该是幂等的
SchemaInitializer::initialize(&conn).unwrap();
SchemaInitializer::initialize(&conn).unwrap();
SchemaInitializer::initialize(&conn).unwrap();
let verified = SchemaInitializer::verify(&conn).unwrap();
assert!(verified);
}
}

View File

@@ -0,0 +1,658 @@
//! 统一存储接口
//!
//! 提供 UnifiedStorage trait 和实现,用于抽象数据访问层
use crate::connection::DuckDbConnection;
use crate::error::DuckDbResult;
use crate::models::{
CleanupResult, ContainerRecord, IdleContainerInfo, ProjectRecord, StorageStats,
};
use crate::repositories::{ContainerRepository, ProjectRepository};
use crate::schema::SchemaInitializer;
use shared_types::ServiceType;
use std::sync::Arc;
/// 统一存储接口
///
/// 提供高层次的数据访问抽象,隐藏底层实现细节
pub trait UnifiedStorage: Send + Sync {
// ========== 容器操作 ==========
/// 保存或更新容器
fn save_container(&self, record: &ContainerRecord) -> DuckDbResult<()>;
/// 获取容器
fn get_container(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>>;
/// 删除容器
fn delete_container(&self, container_id: &str) -> DuckDbResult<bool>;
/// 检查容器是否存在
fn container_exists(&self, container_id: &str) -> DuckDbResult<bool>;
/// 更新容器活动时间
fn update_container_activity(&self, container_id: &str) -> DuckDbResult<bool>;
/// 使用指定时间更新容器活动时间(用于保持项目和容器时间一致)
fn update_container_activity_with_time(
&self,
container_id: &str,
time: chrono::DateTime<chrono::Utc>,
) -> DuckDbResult<bool>;
/// 获取所有容器
fn get_all_containers(&self) -> DuckDbResult<Vec<ContainerRecord>>;
/// 按服务类型获取容器
fn get_containers_by_service_type(
&self,
service_type: ServiceType,
) -> DuckDbResult<Vec<ContainerRecord>>;
/// 查找闲置容器
fn find_idle_containers(
&self,
idle_minutes: i64,
protection_minutes: i64,
) -> DuckDbResult<Vec<IdleContainerInfo>>;
// ========== 项目操作 ==========
/// 保存或更新项目
fn save_project(&self, record: &ProjectRecord) -> DuckDbResult<()>;
/// 获取项目
fn get_project(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>>;
/// 删除项目
fn delete_project(&self, project_id: &str) -> DuckDbResult<bool>;
/// 检查项目是否存在
fn project_exists(&self, project_id: &str) -> DuckDbResult<bool>;
/// 更新项目活动时间,返回实际更新使用的时间戳
fn update_project_activity(
&self,
project_id: &str,
) -> DuckDbResult<Option<chrono::DateTime<chrono::Utc>>>;
/// 获取所有项目
fn get_all_projects(&self) -> DuckDbResult<Vec<ProjectRecord>>;
/// 根据用户ID查找所有项目ComputerAgentRunner模式
///
/// 返回该用户的所有项目记录,按最后活动时间倒序排列
fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
/// 获取用户最新活跃项目的容器IDComputerAgentRunner核心用例
///
/// 在 ComputerAgentRunner 模式下,一个用户对应一个容器,
/// 此方法返回该用户最近活跃项目关联的容器ID
fn get_latest_container_id_by_user_id(&self, user_id: &str) -> DuckDbResult<Option<String>>;
/// 获取 Pod 最新活跃项目的容器ID共享容器模式
///
/// 在共享容器模式下,多个项目可能共享同一个容器(通过 pod_id 标识),
/// 此方法返回该 Pod 下最近活跃项目关联的容器ID
fn get_latest_container_id_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Option<String>>;
/// 根据 pod_id 查找所有项目RCoder 共享容器模式)
///
/// 返回该 Pod 下所有项目记录,按最后活动时间倒序排列
fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
// ========== 会话操作 ==========
/// 根据会话ID获取项目
fn get_project_by_session(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>>;
/// 根据会话ID获取容器名称
///
/// 与 `get_container_id_by_session` 不同,返回稳定的 `container_name`。
/// 即使容器被重建container_name 保持不变,可用于通过 Docker API 查询容器状态。
fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>>;
/// 更新会话
fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool>;
/// 清除会话(将 session_id 设置为 NULL
fn clear_session(&self, project_id: &str) -> DuckDbResult<bool>;
/// 更新会话活动时间
fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool>;
// ========== 状态操作 ==========
/// 原子更新 Agent 状态
fn update_agent_status(
&self,
project_id: &str,
status_code: i32,
status_name: &str,
) -> DuckDbResult<bool>;
// ========== 关联操作 ==========
/// 根据容器ID获取关联的项目
fn get_projects_by_container(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>>;
/// 删除容器及其关联的项目
fn delete_container_with_projects(&self, container_id: &str) -> DuckDbResult<(bool, usize)>;
// ========== 清理操作 ==========
/// 执行清理(删除闲置的容器和项目)
fn cleanup(&self, idle_minutes: i64, protection_minutes: i64) -> DuckDbResult<CleanupResult>;
// ========== 统计操作 ==========
/// 获取存储统计信息
fn get_stats(&self) -> DuckDbResult<StorageStats>;
}
/// DuckDB 统一存储实现
pub struct DuckDbStorage {
conn: DuckDbConnection,
}
impl DuckDbStorage {
/// 创建新的 DuckDB 存储
pub fn new() -> DuckDbResult<Self> {
let conn = DuckDbConnection::open_in_memory()?;
SchemaInitializer::initialize(&conn)?;
Ok(Self { conn })
}
/// 从现有连接创建存储
pub fn from_connection(conn: DuckDbConnection) -> DuckDbResult<Self> {
SchemaInitializer::initialize(&conn)?;
Ok(Self { conn })
}
/// 获取容器 Repository
///
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`
/// 确保并发访问时的线程安全。
fn containers(&self) -> DuckDbResult<ContainerRepository> {
Ok(ContainerRepository::new(self.conn.clone()))
}
/// 获取项目 Repository
///
/// 注意:返回的 Repository 共享同一个 `Arc<Mutex<Connection>>`
/// 确保并发访问时的线程安全。
fn projects(&self) -> DuckDbResult<ProjectRepository> {
Ok(ProjectRepository::new(self.conn.clone()))
}
/// 执行原始 SQL 查询(仅用于调试)
///
/// ⚠️ 此方法仅用于开发和调试目的
pub fn execute_raw_query(
&self,
sql: &str,
) -> DuckDbResult<(Vec<String>, Vec<serde_json::Value>)> {
self.conn.with_connection(|c| {
let mut stmt = c.prepare(sql)?;
let column_count = stmt.column_count();
// 获取列名
let columns: Vec<String> = (0..column_count)
.map(|i| {
stmt.column_name(i)
.map_or("?".to_string(), |v| v.to_string())
})
.collect();
// 执行查询并收集结果
let mut rows: Vec<serde_json::Value> = Vec::new();
let mut result_rows = stmt.query([])?;
while let Some(row) = result_rows.next()? {
let mut row_obj = serde_json::Map::new();
for (i, col_name) in columns.iter().enumerate() {
// 尝试以字符串形式获取值
let value: serde_json::Value = if let Ok(v) = row.get::<_, String>(i) {
serde_json::Value::String(v)
} else if let Ok(v) = row.get::<_, i64>(i) {
serde_json::Value::Number(v.into())
} else if let Ok(v) = row.get::<_, f64>(i) {
if let Some(n) = serde_json::Number::from_f64(v) {
serde_json::Value::Number(n)
} else {
serde_json::Value::Null
}
} else if let Ok(v) = row.get::<_, bool>(i) {
serde_json::Value::Bool(v)
} else {
serde_json::Value::Null
};
row_obj.insert(col_name.clone(), value);
}
rows.push(serde_json::Value::Object(row_obj));
}
Ok((columns, rows))
})
}
/// 获取 DuckDB 内存使用统计(用于调试和监控)
///
/// 返回内存使用的详细信息,帮助定位内存问题
pub fn get_memory_stats(&self) -> DuckDbResult<String> {
self.conn.get_memory_stats()
}
}
impl UnifiedStorage for DuckDbStorage {
// ========== 容器操作 ==========
fn save_container(&self, record: &ContainerRecord) -> DuckDbResult<()> {
self.containers()?.upsert(record)
}
fn get_container(&self, container_id: &str) -> DuckDbResult<Option<ContainerRecord>> {
self.containers()?.find_by_id(container_id)
}
fn delete_container(&self, container_id: &str) -> DuckDbResult<bool> {
self.containers()?.delete(container_id)
}
fn container_exists(&self, container_id: &str) -> DuckDbResult<bool> {
self.containers()?.exists(container_id)
}
fn update_container_activity(&self, container_id: &str) -> DuckDbResult<bool> {
self.containers()?.update_activity(container_id)
}
fn update_container_activity_with_time(
&self,
container_id: &str,
time: chrono::DateTime<chrono::Utc>,
) -> DuckDbResult<bool> {
self.containers()?
.update_activity_with_time(container_id, time)
}
fn get_all_containers(&self) -> DuckDbResult<Vec<ContainerRecord>> {
self.containers()?.find_all()
}
fn get_containers_by_service_type(
&self,
service_type: ServiceType,
) -> DuckDbResult<Vec<ContainerRecord>> {
self.containers()?.find_by_service_type(service_type)
}
fn find_idle_containers(
&self,
idle_minutes: i64,
protection_minutes: i64,
) -> DuckDbResult<Vec<IdleContainerInfo>> {
let mut idle_containers = self
.containers()?
.find_idle_containers(idle_minutes, protection_minutes)?;
// 为每个闲置容器填充关联的项目ID
let projects_repo = self.projects()?;
for container in &mut idle_containers {
let projects = projects_repo.find_by_container_id(&container.container_id)?;
container.project_ids = projects.iter().map(|p| p.project_id.clone()).collect();
}
Ok(idle_containers)
}
// ========== 项目操作 ==========
fn save_project(&self, record: &ProjectRecord) -> DuckDbResult<()> {
self.projects()?.upsert(record)
}
fn get_project(&self, project_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
self.projects()?.find_by_id(project_id)
}
fn delete_project(&self, project_id: &str) -> DuckDbResult<bool> {
self.projects()?.delete(project_id)
}
fn project_exists(&self, project_id: &str) -> DuckDbResult<bool> {
self.projects()?.exists(project_id)
}
fn update_project_activity(
&self,
project_id: &str,
) -> DuckDbResult<Option<chrono::DateTime<chrono::Utc>>> {
self.projects()?.update_activity(project_id)
}
fn get_all_projects(&self) -> DuckDbResult<Vec<ProjectRecord>> {
self.projects()?.find_all()
}
fn find_projects_by_user_id(&self, user_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.projects()?.find_projects_by_user_id(user_id)
}
fn get_latest_container_id_by_user_id(&self, user_id: &str) -> DuckDbResult<Option<String>> {
self.projects()?.get_latest_container_id_by_user_id(user_id)
}
fn get_latest_container_id_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Option<String>> {
self.projects()?.get_latest_container_id_by_pod_id(pod_id)
}
fn find_projects_by_pod_id(&self, pod_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.projects()?.find_projects_by_pod_id(pod_id)
}
// ========== 会话操作 ==========
fn get_project_by_session(&self, session_id: &str) -> DuckDbResult<Option<ProjectRecord>> {
self.projects()?.find_by_session_id(session_id)
}
fn get_container_name_by_session(&self, session_id: &str) -> DuckDbResult<Option<String>> {
self.projects()?.get_container_name_by_session(session_id)
}
fn update_session(&self, project_id: &str, session_id: &str) -> DuckDbResult<bool> {
self.projects()?.update_session(project_id, session_id)
}
fn clear_session(&self, project_id: &str) -> DuckDbResult<bool> {
self.projects()?.clear_session(project_id)
}
/// 更新会话活动时间(同时也更新关联容器的活动时间)
fn update_session_activity(&self, session_id: &str) -> DuckDbResult<bool> {
let updated = self.projects()?.update_session_activity(session_id)?;
// 如果会话活跃,同时也更新关联容器的活跃状态
if updated {
// 使用新方法直接通过 session_id 更新容器活动时间,无需获取 container_id
let _ = self.containers()?.update_activity_by_session(session_id);
}
Ok(updated)
}
// ========== 状态操作 ==========
fn update_agent_status(
&self,
project_id: &str,
status_code: i32,
status_name: &str,
) -> DuckDbResult<bool> {
self.projects()?
.update_status_atomic(project_id, status_code, status_name)
}
// ========== 关联操作 ==========
fn get_projects_by_container(&self, container_id: &str) -> DuckDbResult<Vec<ProjectRecord>> {
self.projects()?.find_by_container_id(container_id)
}
fn delete_container_with_projects(&self, container_id: &str) -> DuckDbResult<(bool, usize)> {
// 先删除关联的项目
let deleted_projects = self.projects()?.delete_by_container_id(container_id)?;
// 再删除容器
let container_deleted = self.containers()?.delete(container_id)?;
Ok((container_deleted, deleted_projects))
}
// ========== 清理操作 ==========
fn cleanup(&self, idle_minutes: i64, protection_minutes: i64) -> DuckDbResult<CleanupResult> {
let mut result = CleanupResult::new();
// 查找闲置容器
let idle_containers = self.find_idle_containers(idle_minutes, protection_minutes)?;
for container in idle_containers {
// 删除容器及其关联的项目
match self.delete_container_with_projects(&container.container_id) {
Ok((container_deleted, projects_deleted)) => {
if container_deleted {
result.cleaned_containers += 1;
}
result.cleaned_projects += projects_deleted;
}
Err(e) => {
result.add_error(format!("failed to delete container {}: {}", container.container_id, e));
}
}
}
Ok(result)
}
// ========== 统计操作 ==========
fn get_stats(&self) -> DuckDbResult<StorageStats> {
let containers = self.containers()?;
let projects = self.projects()?;
let total_containers = containers.count()?;
let total_projects = projects.count()?;
let active_sessions = projects.count_active_sessions()?;
let projects_by_service_type = projects.count_by_service_type()?;
// 计算活跃和闲置容器
let all_containers = containers.find_all()?;
let idle_threshold_minutes = 30;
let mut active_containers = 0;
let mut idle_containers = 0;
for container in &all_containers {
if container.is_idle(idle_threshold_minutes) {
idle_containers += 1;
} else {
active_containers += 1;
}
}
Ok(StorageStats {
total_containers,
total_projects,
active_sessions,
active_containers,
idle_containers,
projects_by_service_type,
})
}
}
impl Clone for DuckDbStorage {
fn clone(&self) -> Self {
Self {
conn: self.conn.clone(),
}
}
}
impl std::fmt::Debug for DuckDbStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DuckDbStorage").finish()
}
}
/// 创建共享的 UnifiedStorage 实例
pub fn create_storage() -> DuckDbResult<Arc<dyn UnifiedStorage>> {
Ok(Arc::new(DuckDbStorage::new()?))
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_storage() -> DuckDbStorage {
DuckDbStorage::new().unwrap()
}
#[test]
fn test_container_crud() {
let storage = create_test_storage();
let record = ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
);
// Create
storage.save_container(&record).unwrap();
// Read
let found = storage.get_container("c1").unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().container_name, "container-1");
// Update
storage.update_container_activity("c1").unwrap();
// Delete
storage.delete_container("c1").unwrap();
assert!(!storage.container_exists("c1").unwrap());
}
#[test]
fn test_project_crud() {
let storage = create_test_storage();
let record = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
// Create
storage.save_project(&record).unwrap();
// Read
let found = storage.get_project("p1").unwrap();
assert!(found.is_some());
// Update
storage.update_project_activity("p1").unwrap();
// Delete
storage.delete_project("p1").unwrap();
assert!(!storage.project_exists("p1").unwrap());
}
#[test]
fn test_session_operations() {
let storage = create_test_storage();
// 创建容器 (必须先创建容器,因为项目关联到容器)
let container = ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
);
storage.save_container(&container).unwrap();
// 创建项目
let record = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
storage.save_project(&record).unwrap();
// 更新会话
storage.update_session("p1", "session-1").unwrap();
// 通过会话ID查询
let project = storage.get_project_by_session("session-1").unwrap();
assert!(project.is_some());
assert_eq!(project.unwrap().project_id, "p1");
// 获取容器名称
let container_name = storage.get_container_name_by_session("session-1").unwrap();
assert_eq!(container_name, Some("container-1".to_string()));
}
#[test]
fn test_delete_container_with_projects() {
let storage = create_test_storage();
// 创建容器
let container = ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
);
storage.save_container(&container).unwrap();
// 创建多个关联项目
for i in 1..=3 {
let project =
ProjectRecord::new(format!("p{}", i), ServiceType::RCoder, "c1".to_string());
storage.save_project(&project).unwrap();
}
// 删除容器及关联项目
let (container_deleted, projects_deleted) =
storage.delete_container_with_projects("c1").unwrap();
assert!(container_deleted);
assert_eq!(projects_deleted, 3);
// 验证数据已删除
assert!(!storage.container_exists("c1").unwrap());
assert!(!storage.project_exists("p1").unwrap());
}
#[test]
fn test_get_stats() {
let storage = create_test_storage();
// 添加容器
let container = ContainerRecord::new(
"c1".to_string(),
"container-1".to_string(),
"127.0.0.1".to_string(),
8080,
8080,
ServiceType::RCoder,
"running".to_string(),
"http://localhost:8080".to_string(),
);
storage.save_container(&container).unwrap();
// 添加项目
let project = ProjectRecord::new("p1".to_string(), ServiceType::RCoder, "c1".to_string());
storage.save_project(&project).unwrap();
// 添加会话
storage.update_session("p1", "session-1").unwrap();
let stats = storage.get_stats().unwrap();
assert_eq!(stats.total_containers, 1);
assert_eq!(stats.total_projects, 1);
assert_eq!(stats.active_sessions, 1);
assert_eq!(stats.active_containers, 1);
assert_eq!(stats.idle_containers, 0);
}
}