飞轮智能成长阶段一:
- 梦核 prompt 注入跨项目复盘笔记,新增【高频陷阱】分析节
- 修复 collect_flywheel_notes 查错表 bug(projects → project_workspaces)
- MCP append_project_note 描述内嵌【复盘】结构化格式,引导 Claude 写标准笔记
- FlywheelStats 新增 project_notes 字段,支持 win_path/wsl_path 两种路径匹配
项目个人备忘(user_project_todos):
- 新表 + 4 个 Rust command,依赖注入写法,6 个单元测试全绿
- UserTodoPanel 组件,📝 按钮展开,支持新增/勾选/删除
CONVENTIONS v1.4.4:新增 Rust 可测试性规范(依赖注入强制要求)
643 lines
25 KiB
Rust
643 lines
25 KiB
Rust
use once_cell::sync::OnceCell;
|
||
use r2d2::Pool;
|
||
use r2d2_sqlite::SqliteConnectionManager;
|
||
use std::path::PathBuf;
|
||
|
||
pub type DbPool = Pool<SqliteConnectionManager>;
|
||
|
||
static POOL: OnceCell<DbPool> = OnceCell::new();
|
||
|
||
pub fn init(data_dir: PathBuf) {
|
||
std::fs::create_dir_all(&data_dir).expect("create data dir");
|
||
let db_path = data_dir.join("dev-manager.db");
|
||
let manager = SqliteConnectionManager::file(db_path);
|
||
let pool = Pool::new(manager).expect("create sqlite pool");
|
||
|
||
{
|
||
let conn = pool.get().expect("get conn for migration");
|
||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
||
.expect("pragma");
|
||
migrate(&conn);
|
||
}
|
||
|
||
POOL.set(pool).expect("pool already initialized");
|
||
}
|
||
|
||
pub fn pool() -> &'static DbPool {
|
||
POOL.get().expect("db not initialized")
|
||
}
|
||
|
||
fn migrate(conn: &rusqlite::Connection) {
|
||
conn.execute_batch(
|
||
"
|
||
CREATE TABLE IF NOT EXISTS projects (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
type TEXT,
|
||
status TEXT DEFAULT 'active',
|
||
priority TEXT DEFAULT 'mid',
|
||
wsl_path TEXT,
|
||
win_path TEXT,
|
||
tech_stack TEXT,
|
||
recent_focus TEXT,
|
||
startup_commands TEXT,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS product_groups (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS project_group_map (
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
group_id TEXT NOT NULL REFERENCES product_groups(id) ON DELETE CASCADE,
|
||
role TEXT,
|
||
PRIMARY KEY (project_id, group_id)
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS relations (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
from_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
to_project TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
relation_type TEXT,
|
||
note TEXT
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS activity_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
summary TEXT NOT NULL,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS project_tools (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
icon TEXT NOT NULL DEFAULT '🔧',
|
||
exe_path TEXT NOT NULL,
|
||
args TEXT,
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS dev_tools (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
category TEXT NOT NULL,
|
||
cmd TEXT,
|
||
path TEXT,
|
||
version TEXT,
|
||
note TEXT,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS project_env_tools (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
layer INTEGER NOT NULL,
|
||
name TEXT NOT NULL,
|
||
source TEXT,
|
||
version TEXT,
|
||
note TEXT,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS network_monitored_tools (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
exe_path TEXT NOT NULL,
|
||
enabled INTEGER NOT NULL DEFAULT 1
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS network_mode_log (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
mode TEXT NOT NULL,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('editor', 'cursor');
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('wsl_distro', 'Ubuntu');
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('dev_mode', '0');
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_id', '');
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('github_client_secret', '');
|
||
INSERT OR IGNORE INTO settings (key, value) VALUES ('mcp_port', '27190');
|
||
",
|
||
)
|
||
.expect("migrate");
|
||
|
||
// 增量迁移:为 dev_tools 添加 env 字段(已存在则忽略)
|
||
let _ = conn.execute(
|
||
"ALTER TABLE dev_tools ADD COLUMN env TEXT NOT NULL DEFAULT 'windows'",
|
||
[],
|
||
);
|
||
|
||
// 增量迁移:Git 多空间功能所需新表
|
||
conn.execute_batch(
|
||
"
|
||
CREATE TABLE IF NOT EXISTS git_accounts (
|
||
id TEXT PRIMARY KEY,
|
||
username TEXT NOT NULL UNIQUE,
|
||
avatar_url TEXT,
|
||
provider TEXT NOT NULL DEFAULT 'github',
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS spaces (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
local_root TEXT NOT NULL,
|
||
is_current INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS deploy_commands (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
command TEXT NOT NULL,
|
||
run_in TEXT NOT NULL DEFAULT 'windows'
|
||
);
|
||
",
|
||
)
|
||
.expect("migrate git spaces");
|
||
|
||
// 增量迁移:git_accounts 补 UNIQUE 索引(已存在则忽略)
|
||
let _ = conn.execute(
|
||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_git_accounts_username ON git_accounts(username)",
|
||
[],
|
||
);
|
||
|
||
// 增量迁移:projects 表新增 Git 相关字段(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN space_id TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN repo_url TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN upstream_url TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN default_branch TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN last_push_at TEXT", []);
|
||
|
||
// 增量迁移:platform 字段("windows" | "wsl",已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN platform TEXT", []);
|
||
|
||
// 增量迁移:product_groups 表新增 space_id(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE product_groups ADD COLUMN space_id TEXT", []);
|
||
|
||
// 增量迁移:dev_tools 表新增 space_id(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE dev_tools ADD COLUMN space_id TEXT", []);
|
||
|
||
// 增量迁移:network_monitored_tools 新增 space_id(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE network_monitored_tools ADD COLUMN space_id TEXT", []);
|
||
|
||
// 增量迁移:空间级别的 settings 覆盖表
|
||
let _ = conn.execute_batch(
|
||
"CREATE TABLE IF NOT EXISTS space_settings (
|
||
space_id TEXT NOT NULL,
|
||
key TEXT NOT NULL,
|
||
value TEXT NOT NULL,
|
||
PRIMARY KEY (space_id, key)
|
||
);"
|
||
);
|
||
|
||
// 增量迁移:项目生命周期字段
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN staging_url TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN prod_url TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE projects ADD COLUMN server_note TEXT", []);
|
||
|
||
// ── 方案A重构:project_profiles + project_workspaces ──────────────────────
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS project_profiles (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
type TEXT,
|
||
status TEXT DEFAULT 'active',
|
||
priority TEXT DEFAULT 'mid',
|
||
tech_stack TEXT,
|
||
repo_url TEXT,
|
||
upstream_url TEXT,
|
||
default_branch TEXT,
|
||
staging_url TEXT,
|
||
prod_url TEXT,
|
||
server_note TEXT,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS project_workspaces (
|
||
id TEXT PRIMARY KEY,
|
||
profile_id TEXT NOT NULL REFERENCES project_profiles(id) ON DELETE CASCADE,
|
||
space_id TEXT,
|
||
wsl_path TEXT,
|
||
win_path TEXT,
|
||
platform TEXT DEFAULT 'windows',
|
||
recent_focus TEXT,
|
||
startup_commands TEXT,
|
||
last_push_at TEXT,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_pw_profile_space
|
||
ON project_workspaces(profile_id, ifnull(space_id, ''));
|
||
").expect("migrate project profiles/workspaces");
|
||
|
||
// ── git_repos:GitHub 仓库注册表(全局锚点)────────────────────────────────
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS git_repos (
|
||
id TEXT PRIMARY KEY,
|
||
repo_url TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
default_branch TEXT DEFAULT 'main',
|
||
upstream_url TEXT,
|
||
staging_url TEXT,
|
||
prod_url TEXT,
|
||
server_note TEXT,
|
||
status TEXT DEFAULT 'active',
|
||
priority TEXT DEFAULT 'mid',
|
||
tech_stack TEXT,
|
||
last_synced_at TEXT,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_git_repos_url ON git_repos(repo_url);
|
||
").expect("migrate git_repos");
|
||
|
||
// project_workspaces 添加 repo_id 外键(已存在则忽略)
|
||
let _ = conn.execute(
|
||
"ALTER TABLE project_workspaces ADD COLUMN repo_id TEXT REFERENCES git_repos(id) ON DELETE SET NULL",
|
||
[],
|
||
);
|
||
|
||
// 增量迁移:项目模板系统
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS project_templates (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
platform TEXT NOT NULL DEFAULT 'both',
|
||
tech_stack TEXT,
|
||
init_commands TEXT,
|
||
is_builtin INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS project_template_files (
|
||
id TEXT PRIMARY KEY,
|
||
template_id TEXT NOT NULL REFERENCES project_templates(id) ON DELETE CASCADE,
|
||
path TEXT NOT NULL,
|
||
content TEXT NOT NULL DEFAULT '',
|
||
UNIQUE(template_id, path)
|
||
);
|
||
").expect("migrate project templates");
|
||
|
||
// 迁移 project_group_map:将外键从 projects(id) 改为 project_workspaces(id)
|
||
let pgm_sql: Option<String> = conn.query_row(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_group_map'",
|
||
[],
|
||
|row| row.get(0),
|
||
).ok();
|
||
if pgm_sql.map(|s| s.contains("REFERENCES projects")).unwrap_or(false) {
|
||
conn.execute_batch("
|
||
PRAGMA foreign_keys=OFF;
|
||
CREATE TABLE project_group_map_new (
|
||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
||
group_id TEXT NOT NULL REFERENCES product_groups(id) ON DELETE CASCADE,
|
||
role TEXT,
|
||
PRIMARY KEY (project_id, group_id)
|
||
);
|
||
INSERT OR IGNORE INTO project_group_map_new (project_id, group_id, role)
|
||
SELECT m.project_id, m.group_id, m.role
|
||
FROM project_group_map m
|
||
INNER JOIN project_workspaces pw ON pw.id = m.project_id;
|
||
DROP TABLE project_group_map;
|
||
ALTER TABLE project_group_map_new RENAME TO project_group_map;
|
||
PRAGMA foreign_keys=ON;
|
||
").expect("migrate project_group_map FK to project_workspaces");
|
||
}
|
||
|
||
// 迁移 project_tools / activity_log / project_env_tools:
|
||
// 将外键从 projects(id) 改为 project_workspaces(id)
|
||
let pt_sql: Option<String> = conn.query_row(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_tools'",
|
||
[], |row| row.get(0),
|
||
).ok();
|
||
if pt_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) {
|
||
conn.execute_batch("
|
||
PRAGMA foreign_keys=OFF;
|
||
CREATE TABLE project_tools_new (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
icon TEXT NOT NULL DEFAULT '🔧',
|
||
exe_path TEXT NOT NULL,
|
||
args TEXT,
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
INSERT OR IGNORE INTO project_tools_new
|
||
SELECT t.id, t.project_id, t.name, t.icon, t.exe_path, t.args, t.sort_order
|
||
FROM project_tools t
|
||
INNER JOIN project_workspaces pw ON pw.id = t.project_id;
|
||
DROP TABLE project_tools;
|
||
ALTER TABLE project_tools_new RENAME TO project_tools;
|
||
PRAGMA foreign_keys=ON;
|
||
").expect("migrate project_tools FK to project_workspaces");
|
||
}
|
||
|
||
let al_sql: Option<String> = conn.query_row(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='activity_log'",
|
||
[], |row| row.get(0),
|
||
).ok();
|
||
if al_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) {
|
||
conn.execute_batch("
|
||
PRAGMA foreign_keys=OFF;
|
||
CREATE TABLE activity_log_new (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
||
summary TEXT NOT NULL,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
INSERT OR IGNORE INTO activity_log_new (id, project_id, summary, created_at)
|
||
SELECT a.id, a.project_id, a.summary, a.created_at
|
||
FROM activity_log a
|
||
INNER JOIN project_workspaces pw ON pw.id = a.project_id;
|
||
DROP TABLE activity_log;
|
||
ALTER TABLE activity_log_new RENAME TO activity_log;
|
||
PRAGMA foreign_keys=ON;
|
||
").expect("migrate activity_log FK to project_workspaces");
|
||
}
|
||
|
||
let pet_sql: Option<String> = conn.query_row(
|
||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='project_env_tools'",
|
||
[], |row| row.get(0),
|
||
).ok();
|
||
if pet_sql.as_deref().map(|s| s.contains("REFERENCES projects")).unwrap_or(false) {
|
||
conn.execute_batch("
|
||
PRAGMA foreign_keys=OFF;
|
||
CREATE TABLE project_env_tools_new (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES project_workspaces(id) ON DELETE CASCADE,
|
||
layer INTEGER NOT NULL,
|
||
name TEXT NOT NULL,
|
||
source TEXT,
|
||
version TEXT,
|
||
note TEXT,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
INSERT OR IGNORE INTO project_env_tools_new
|
||
SELECT t.id, t.project_id, t.layer, t.name, t.source, t.version, t.note, t.updated_at
|
||
FROM project_env_tools t
|
||
INNER JOIN project_workspaces pw ON pw.id = t.project_id;
|
||
DROP TABLE project_env_tools;
|
||
ALTER TABLE project_env_tools_new RENAME TO project_env_tools;
|
||
PRAGMA foreign_keys=ON;
|
||
").expect("migrate project_env_tools FK to project_workspaces");
|
||
}
|
||
|
||
// 从旧 projects 表迁移数据(INSERT OR IGNORE 保证幂等)
|
||
let _ = conn.execute_batch("
|
||
INSERT OR IGNORE INTO project_profiles
|
||
(id, name, description, type, status, priority, tech_stack,
|
||
repo_url, upstream_url, default_branch,
|
||
staging_url, prod_url, server_note, updated_at)
|
||
SELECT
|
||
'prof-' || id,
|
||
name, description, type, status, priority, tech_stack,
|
||
repo_url, upstream_url, default_branch,
|
||
staging_url, prod_url, server_note, updated_at
|
||
FROM projects;
|
||
|
||
INSERT OR IGNORE INTO project_workspaces
|
||
(id, profile_id, space_id, wsl_path, win_path, platform,
|
||
recent_focus, startup_commands, last_push_at, updated_at)
|
||
SELECT
|
||
id,
|
||
'prof-' || id,
|
||
space_id, wsl_path, win_path, COALESCE(platform, 'windows'),
|
||
recent_focus, startup_commands, last_push_at, updated_at
|
||
FROM projects;
|
||
");
|
||
|
||
// 增量迁移:project_profiles 加 tags 字段
|
||
let _ = conn.execute("ALTER TABLE project_profiles ADD COLUMN tags TEXT", []);
|
||
|
||
// 标签管理表
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS project_tags (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
color TEXT NOT NULL DEFAULT '#8b5cf6'
|
||
);
|
||
CREATE TABLE IF NOT EXISTS project_tag_map (
|
||
profile_id TEXT NOT NULL,
|
||
tag_id TEXT NOT NULL,
|
||
PRIMARY KEY (profile_id, tag_id)
|
||
);
|
||
").expect("create tag tables");
|
||
|
||
// 增量迁移:product_groups 加 sort_order 字段
|
||
let _ = conn.execute(
|
||
"ALTER TABLE product_groups ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0",
|
||
[],
|
||
);
|
||
|
||
// CI/CD 配置表(替代 deploy_commands)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS cicd_configs (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL UNIQUE,
|
||
config_type TEXT NOT NULL DEFAULT 'web_ssh',
|
||
trigger_branch TEXT NOT NULL DEFAULT 'main',
|
||
build_command TEXT DEFAULT 'pnpm build',
|
||
node_version TEXT DEFAULT '20',
|
||
server_host TEXT,
|
||
server_user TEXT DEFAULT 'ubuntu',
|
||
deploy_path TEXT,
|
||
start_command TEXT DEFAULT 'pm2 restart app',
|
||
oss_endpoint TEXT DEFAULT 'oss-cn-hangzhou.aliyuncs.com',
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create cicd_configs table");
|
||
|
||
// 资源视角画布状态(每个产品组独立保存)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS canvas_states (
|
||
group_id TEXT PRIMARY KEY,
|
||
data TEXT NOT NULL,
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create canvas_states table");
|
||
|
||
// 运行时可观测性日志(最多保留 500 条)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS runtime_logs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
ts TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
|
||
category TEXT NOT NULL,
|
||
level TEXT NOT NULL,
|
||
message TEXT NOT NULL,
|
||
context TEXT
|
||
);
|
||
").expect("create runtime_logs table");
|
||
|
||
// AI 笔记(Claude 通过 MCP 或用户手动写入的分析结论)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS project_notes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT NOT NULL,
|
||
source TEXT NOT NULL DEFAULT 'mcp',
|
||
content TEXT NOT NULL,
|
||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
||
);
|
||
").expect("create project_notes table");
|
||
|
||
// 用户项目个人备忘(自由文本待办,Claude 不感知)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS user_project_todos (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT NOT NULL,
|
||
content TEXT NOT NULL,
|
||
done INTEGER NOT NULL DEFAULT 0,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now', 'localtime'))
|
||
);
|
||
").expect("create user_project_todos table");
|
||
|
||
// 蓝图 Buff:记录哪些项目施加了 buff
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS blueprint_buffs (
|
||
project_path TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
last_sync_at TEXT,
|
||
last_commit TEXT,
|
||
status TEXT NOT NULL DEFAULT 'active'
|
||
);
|
||
").expect("create blueprint_buffs table");
|
||
|
||
// 仪表盘全局快启应用(无项目 FK 约束)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS global_tools (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
icon TEXT NOT NULL DEFAULT '🔧',
|
||
exe_path TEXT NOT NULL,
|
||
args TEXT,
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
").expect("create global_tools table");
|
||
|
||
// 增量迁移:项目历史快照(每日一条,供 Claude 做趋势分析)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS project_snapshots (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
project_id TEXT NOT NULL,
|
||
snapshot_date TEXT NOT NULL,
|
||
days_since_commit INTEGER,
|
||
blueprint_done_rate REAL,
|
||
blueprint_in_progress INTEGER DEFAULT 0,
|
||
blueprint_blocked INTEGER DEFAULT 0,
|
||
has_errors INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
UNIQUE(project_id, snapshot_date)
|
||
);
|
||
").expect("create project_snapshots table");
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub fn conn_with_schema() -> rusqlite::Connection {
|
||
let conn = rusqlite::Connection::open_in_memory().unwrap();
|
||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
||
.unwrap();
|
||
migrate(&conn);
|
||
conn
|
||
}
|
||
|
||
/// 写入运行时日志。失败时静默(不能让日志写入反过来崩溃业务逻辑)
|
||
pub fn log_event(category: &str, level: &str, message: &str, context: Option<&str>) {
|
||
let pool = match POOL.get() {
|
||
Some(p) => p,
|
||
None => return,
|
||
};
|
||
let conn = match pool.get() {
|
||
Ok(c) => c,
|
||
Err(_) => return,
|
||
};
|
||
let _ = conn.execute(
|
||
"INSERT INTO runtime_logs (category, level, message, context) VALUES (?1, ?2, ?3, ?4)",
|
||
rusqlite::params![category, level, message, context],
|
||
);
|
||
// 超出 550 条时才触发清理,保留最新 500 条(避免每次写入都全表扫描)
|
||
let count: i64 = conn
|
||
.query_row("SELECT COUNT(*) FROM runtime_logs", [], |r| r.get(0))
|
||
.unwrap_or(0);
|
||
if count > 550 {
|
||
let _ = conn.execute(
|
||
"DELETE FROM runtime_logs WHERE id NOT IN (SELECT id FROM runtime_logs ORDER BY id DESC LIMIT 500)",
|
||
[],
|
||
);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn migrate_idempotent() {
|
||
// 连续两次 migrate 不应 panic
|
||
let conn = conn_with_schema();
|
||
migrate(&conn);
|
||
}
|
||
|
||
#[test]
|
||
fn core_tables_exist() {
|
||
let conn = conn_with_schema();
|
||
let count: i64 = conn
|
||
.query_row(
|
||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' \
|
||
AND name IN ('projects','settings','spaces','git_repos','runtime_logs',\
|
||
'project_profiles','project_workspaces')",
|
||
[],
|
||
|r| r.get(0),
|
||
)
|
||
.unwrap();
|
||
assert_eq!(count, 7, "缺少核心表");
|
||
}
|
||
|
||
#[test]
|
||
fn default_settings_seeded() {
|
||
let conn = conn_with_schema();
|
||
|
||
let editor: String = conn
|
||
.query_row("SELECT value FROM settings WHERE key='editor'", [], |r| r.get(0))
|
||
.unwrap();
|
||
assert_eq!(editor, "cursor");
|
||
|
||
let port: String = conn
|
||
.query_row("SELECT value FROM settings WHERE key='mcp_port'", [], |r| r.get(0))
|
||
.unwrap();
|
||
assert_eq!(port, "27190");
|
||
}
|
||
|
||
#[test]
|
||
fn runtime_logs_insert_and_count() {
|
||
let conn = conn_with_schema();
|
||
conn.execute(
|
||
"INSERT INTO runtime_logs (category, level, message) VALUES ('test','info','hello')",
|
||
[],
|
||
)
|
||
.unwrap();
|
||
let count: i64 = conn
|
||
.query_row("SELECT COUNT(*) FROM runtime_logs", [], |r| r.get(0))
|
||
.unwrap();
|
||
assert_eq!(count, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn log_event_silent_when_no_pool() {
|
||
// pool 未初始化时应静默返回,不 panic
|
||
log_event("test", "info", "no-pool test", None);
|
||
}
|
||
}
|