chore: wave-0 baseline (Tauri2+React shell, platform detect, fake catalog)

This commit is contained in:
AgentDock 施工员
2026-08-25 00:21:13 +08:00
commit 296843215f
137 changed files with 6696 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "agentdock-store"
version.workspace = true
edition.workspace = true
license.workspace = true
description = "SQLite 本地状态存储:schema 迁移与仓库(架构 §2 状态存储层)"
[dependencies]
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+32
View File
@@ -0,0 +1,32 @@
//! 存储层错误类型
use std::fmt;
#[derive(Debug)]
pub enum StoreError {
Io(std::io::Error),
Sqlite(rusqlite::Error),
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreError::Io(e) => write!(f, "IO: {e}"),
StoreError::Sqlite(e) => write!(f, "SQLite: {e}"),
}
}
}
impl std::error::Error for StoreError {}
impl From<std::io::Error> for StoreError {
fn from(e: std::io::Error) -> Self {
StoreError::Io(e)
}
}
impl From<rusqlite::Error> for StoreError {
fn from(e: rusqlite::Error) -> Self {
StoreError::Sqlite(e)
}
}
+11
View File
@@ -0,0 +1,11 @@
//! agentdock-store —— 本地状态存储(SQLite)
//!
//! Wave 0SQLite 空库 + schema 迁移骨架。仓库查询接口随后续波次实现。
pub mod error;
pub mod migrations;
pub mod schema;
pub mod store;
pub use error::StoreError;
pub use store::{Store, default_data_dir};
+26
View File
@@ -0,0 +1,26 @@
//! 迁移执行器(Wave 0 骨架)
use rusqlite::Connection;
use crate::schema::{MIGRATIONS, SCHEMA_VERSION};
/// 应用全部迁移。所有语句均为幂等(IF NOT EXISTS),可安全重复执行。
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
let tx = conn.unchecked_transaction()?;
for sql in MIGRATIONS {
tx.execute_batch(sql)?;
}
tx.execute(
"INSERT OR REPLACE INTO schema_version (version) VALUES (?1)",
[SCHEMA_VERSION],
)?;
tx.commit()
}
/// 读取当前 schema 版本(未初始化返回 0)
pub fn current_version(conn: &Connection) -> rusqlite::Result<i64> {
conn.query_row("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1", [], |r| {
r.get(0)
})
.or(Ok(0))
}
+33
View File
@@ -0,0 +1,33 @@
//! schema 定义与迁移骨架(Wave 0
//!
//! 迁移采用顺序数组:每个元素是一段幂等 SQLCREATE TABLE IF NOT EXISTS)。
//! 后续波次新增表时在 `MIGRATIONS` 尾部追加并提升 `SCHEMA_VERSION`。
/// 当前 schema 版本
pub const SCHEMA_VERSION: i64 = 1;
/// 迁移列表(按顺序执行,均需幂等)
pub const MIGRATIONS: [&str; 3] = [
// 迁移 1schema 版本表
"CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
// 迁移 2:已安装 CLI 记录
"CREATE TABLE IF NOT EXISTS installed_clis (
cli_id TEXT PRIMARY KEY,
name_zh TEXT NOT NULL,
version TEXT,
install_channel TEXT,
detected_at TEXT
);",
// 迁移 3:诊断历史
"CREATE TABLE IF NOT EXISTS diagnostic_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cli_id TEXT NOT NULL,
rule_id TEXT NOT NULL,
severity TEXT NOT NULL,
message_zh TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);",
];
+105
View File
@@ -0,0 +1,105 @@
//! Store:打开/创建 SQLite 空库并应用迁移(Wave 0
use std::path::{Path, PathBuf};
use rusqlite::Connection;
use crate::error::StoreError;
use crate::migrations;
/// 本地状态存储句柄
pub struct Store {
conn: Connection,
path: PathBuf,
}
impl Store {
/// 打开(不存在则创建)指定路径的 SQLite 库并应用迁移
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, StoreError> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let conn = Connection::open(&path)?;
conn.pragma_update(None, "foreign_keys", true)?;
migrations::migrate(&conn)?;
Ok(Self { conn, path })
}
/// 打开默认数据目录下的 agentdock.dbWindows: %APPDATA%\agentdock
pub fn open_default() -> Result<Self, StoreError> {
Self::open(default_data_dir().join("agentdock.db"))
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn connection(&self) -> &Connection {
&self.conn
}
}
/// 默认数据目录
pub fn default_data_dir() -> PathBuf {
#[cfg(windows)]
{
if let Some(appdata) = std::env::var_os("APPDATA") {
return PathBuf::from(appdata).join("agentdock");
}
PathBuf::from(".").join("agentdock")
}
#[cfg(not(windows))]
{
if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
return PathBuf::from(xdg).join("agentdock");
}
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(".local/share").join("agentdock");
}
PathBuf::from(".").join("agentdock")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::migrations::current_version;
use crate::schema::SCHEMA_VERSION;
#[test]
fn in_memory_migration_applies_and_is_idempotent() {
let conn = Connection::open_in_memory().expect("内存库应可打开");
migrations::migrate(&conn).expect("首次迁移应成功");
assert_eq!(current_version(&conn).unwrap(), SCHEMA_VERSION);
// 幂等:重复迁移不报错、版本不变
migrations::migrate(&conn).expect("重复迁移应成功");
assert_eq!(current_version(&conn).unwrap(), SCHEMA_VERSION);
// 关键表存在
let tables: Vec<String> = conn
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('schema_version','installed_clis','diagnostic_history')")
.unwrap()
.query_map([], |r| r.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(tables.len(), 3);
}
#[test]
fn open_creates_empty_db_file() {
let dir = std::env::temp_dir().join(format!("agentdock-test-{}", std::process::id()));
let db = dir.join("agentdock.db");
let _ = std::fs::remove_dir_all(&dir);
let store = Store::open(&db).expect("应能创建库文件");
assert!(db.exists(), "SQLite 空库文件应已创建");
assert_eq!(current_version(store.connection()).unwrap(), SCHEMA_VERSION);
let _ = std::fs::remove_dir_all(&dir);
}
}