- 新表 commit_files(sha,path 联合主键 + path 索引),migration 入 migrate() - read_git_log 加 --name-only:单次 git 调用同时取 conventional 指标、 Rules-Applied trailer 与触碰文件,零额外进程开销 - parse_git_log_output 纯函数解析 header/文件行混排输出(含 \x1f 的行 是 header,其后非空行归属该 commit) - record_commit_entry 落库文件清单(INSERT OR IGNORE 幂等),buff 增量 与 ingest 全量两条链路自动覆盖,无需各自改动 Rules-Applied: R05,R06,R07
999 lines
41 KiB
Rust
999 lines
41 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")
|
||
}
|
||
|
||
/// pool 未初始化时返回 None,测试环境中安全调用。
|
||
pub(crate) fn try_pool() -> Option<&'static DbPool> {
|
||
POOL.get()
|
||
}
|
||
|
||
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");
|
||
|
||
// 飞轮 L0:从 git 解析的 conventional commit 执行质量指标
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS commit_metrics (
|
||
sha TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
commit_type TEXT,
|
||
scope TEXT,
|
||
is_rework INTEGER NOT NULL DEFAULT 0,
|
||
is_ci_auto INTEGER NOT NULL DEFAULT 0,
|
||
committed_at TEXT,
|
||
parsed_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_commit_metrics_project ON commit_metrics(project_id);
|
||
").expect("create commit_metrics table");
|
||
|
||
// 增量迁移(飞轮 R1):Rules-Applied trailer 规则命中列(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE commit_metrics ADD COLUMN rules_applied TEXT", []);
|
||
|
||
// 飞轮阶段三(P3-B):commit 触碰的文件清单,供文件级返工风险画像
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS commit_files (
|
||
sha TEXT NOT NULL,
|
||
path TEXT NOT NULL,
|
||
PRIMARY KEY (sha, path)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_commit_files_path ON commit_files(path);
|
||
").expect("create commit_files table");
|
||
|
||
// 飞轮 L1:PR 生命周期事件(opened / closed / merged)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS pr_events (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL,
|
||
pr_number INTEGER NOT NULL,
|
||
title TEXT,
|
||
action TEXT NOT NULL,
|
||
head_branch TEXT,
|
||
base_branch TEXT,
|
||
author TEXT,
|
||
event_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_pr_events_project ON pr_events(project_id);
|
||
").expect("create pr_events 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");
|
||
|
||
// 增量迁移:模板 variants 列(多初始化方式下拉选项)
|
||
let _ = conn.execute_batch(
|
||
"ALTER TABLE project_templates ADD COLUMN variants TEXT;"
|
||
);
|
||
|
||
// 增量迁移:服务器注册表 + 项目-服务器关联
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS servers (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
server_host TEXT NOT NULL,
|
||
server_port TEXT DEFAULT '22',
|
||
username TEXT,
|
||
password TEXT,
|
||
ssh_key_path TEXT,
|
||
quick_links TEXT,
|
||
notes TEXT,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS project_server_map (
|
||
project_id TEXT NOT NULL,
|
||
server_id TEXT NOT NULL,
|
||
role TEXT DEFAULT 'production',
|
||
deploy_type TEXT DEFAULT 'bare',
|
||
deploy_path TEXT,
|
||
container_name TEXT,
|
||
start_command TEXT,
|
||
notes TEXT,
|
||
PRIMARY KEY (project_id, server_id)
|
||
);
|
||
").expect("create servers tables");
|
||
|
||
// 增量迁移:project_server_map 新增部署类型字段(已存在则忽略)
|
||
let _ = conn.execute("ALTER TABLE project_server_map ADD COLUMN deploy_type TEXT DEFAULT 'bare'", []);
|
||
let _ = conn.execute("ALTER TABLE project_server_map ADD COLUMN container_name TEXT", []);
|
||
let _ = conn.execute("ALTER TABLE project_server_map ADD COLUMN start_command TEXT", []);
|
||
|
||
// 增量迁移:GitHub 镜像源 + 服务器网络配置 + 软件仓库
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS github_mirrors (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
url_prefix TEXT NOT NULL,
|
||
is_builtin INTEGER DEFAULT 0,
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
CREATE TABLE IF NOT EXISTS server_network_config (
|
||
server_id TEXT PRIMARY KEY,
|
||
github_mirror_id TEXT,
|
||
last_tested TEXT,
|
||
test_results TEXT
|
||
);
|
||
CREATE TABLE IF NOT EXISTS server_software (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
category TEXT,
|
||
install_type TEXT NOT NULL DEFAULT 'package',
|
||
install_cmd TEXT,
|
||
github_url TEXT,
|
||
remote_path TEXT,
|
||
post_cmd TEXT,
|
||
is_builtin INTEGER DEFAULT 0,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create software mgmt tables");
|
||
|
||
// 软件分类标签
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS software_categories (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
color TEXT NOT NULL DEFAULT '#6b7280',
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
").expect("create software_categories table");
|
||
|
||
// 内置分类标签(幂等)
|
||
conn.execute_batch("
|
||
INSERT OR IGNORE INTO software_categories (id, name, color, sort_order)
|
||
VALUES ('cat-web', 'Web 服务', '#3b82f6', 0);
|
||
INSERT OR IGNORE INTO software_categories (id, name, color, sort_order)
|
||
VALUES ('cat-runtime', '运行时', '#10b981', 1);
|
||
INSERT OR IGNORE INTO software_categories (id, name, color, sort_order)
|
||
VALUES ('cat-tool', '工具', '#f59e0b', 2);
|
||
INSERT OR IGNORE INTO software_categories (id, name, color, sort_order)
|
||
VALUES ('cat-custom', '自定义', '#8b5cf6', 3);
|
||
").expect("seed software categories");
|
||
|
||
// 迁移旧 category 字符串 → category id
|
||
let _ = conn.execute_batch("
|
||
UPDATE server_software SET category = 'cat-web' WHERE category = 'web';
|
||
UPDATE server_software SET category = 'cat-runtime' WHERE category = 'runtime';
|
||
UPDATE server_software SET category = 'cat-tool' WHERE category = 'tool';
|
||
UPDATE server_software SET category = 'cat-custom' WHERE category = 'custom';
|
||
");
|
||
|
||
// 内置镜像源(幂等)
|
||
conn.execute_batch("
|
||
INSERT OR IGNORE INTO github_mirrors (id, name, url_prefix, is_builtin, sort_order)
|
||
VALUES ('mirror-direct', '直连(无镜像)', 'https://github.com', 1, 0);
|
||
INSERT OR IGNORE INTO github_mirrors (id, name, url_prefix, is_builtin, sort_order)
|
||
VALUES ('mirror-ghfast', 'ghfast.top', 'https://ghfast.top', 1, 1);
|
||
INSERT OR IGNORE INTO github_mirrors (id, name, url_prefix, is_builtin, sort_order)
|
||
VALUES ('mirror-ghproxy', 'gh-proxy.com', 'https://gh-proxy.com', 1, 2);
|
||
").expect("seed github mirrors");
|
||
|
||
// 内置软件模板(幂等)
|
||
conn.execute_batch("
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-nginx', 'Nginx', 'Web 服务器 / 反向代理', 'cat-web', 'package', 'apt install -y nginx && systemctl enable nginx', 1, 0);
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-docker', 'Docker', '容器运行时', 'cat-runtime', 'package', 'apt install -y docker.io && systemctl enable docker', 1, 1);
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-node20', 'Node.js 20','JavaScript 运行时', 'cat-runtime', 'package', 'curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt install -y nodejs', 1, 2);
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-pm2', 'PM2', 'Node.js 进程管理', 'cat-runtime', 'package', 'npm install -g pm2', 1, 3);
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-git', 'Git', '版本控制', 'cat-tool', 'package', 'apt install -y git', 1, 4);
|
||
INSERT OR IGNORE INTO server_software (id, name, description, category, install_type, install_cmd, is_builtin, sort_order)
|
||
VALUES ('sw-htop', 'htop', '系统监控工具', 'cat-tool', 'package', 'apt install -y htop', 1, 5);
|
||
").expect("seed server software");
|
||
|
||
// 增量迁移:服务连接配置(每台服务器上部署的各种服务的连接信息)
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS server_service_configs (
|
||
id TEXT PRIMARY KEY,
|
||
server_id TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
category TEXT DEFAULT 'custom',
|
||
scope TEXT DEFAULT 'system',
|
||
config_data TEXT NOT NULL DEFAULT '[]',
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create server_service_configs table");
|
||
let _ = conn.execute("ALTER TABLE server_service_configs ADD COLUMN scope TEXT DEFAULT 'system'", []);
|
||
|
||
// 增量迁移:API 密钥管理
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS api_keys (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
provider TEXT NOT NULL,
|
||
api_key TEXT NOT NULL,
|
||
base_url TEXT,
|
||
model TEXT,
|
||
notes TEXT,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create api_keys table");
|
||
|
||
// 增量迁移:宿主机部署应用
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS server_host_apps (
|
||
id TEXT PRIMARY KEY,
|
||
server_id TEXT NOT NULL,
|
||
name TEXT NOT NULL,
|
||
deploy_type TEXT NOT NULL DEFAULT 'systemd',
|
||
service_name TEXT,
|
||
binary_path TEXT,
|
||
config_path TEXT,
|
||
log_path TEXT,
|
||
port TEXT,
|
||
start_cmd TEXT,
|
||
stop_cmd TEXT,
|
||
restart_cmd TEXT,
|
||
status_cmd TEXT,
|
||
notes TEXT,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create server_host_apps table");
|
||
|
||
// ── 源码库 ──────────────────────────────────────────────────────────
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS source_categories (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
icon TEXT DEFAULT '📁',
|
||
color TEXT DEFAULT '#6B7280',
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS source_projects (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
description TEXT,
|
||
category_id TEXT REFERENCES source_categories(id) ON DELETE SET NULL,
|
||
language TEXT,
|
||
tags TEXT,
|
||
repo_url TEXT,
|
||
introduction TEXT,
|
||
readme TEXT,
|
||
notes TEXT,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS source_versions (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT NOT NULL REFERENCES source_projects(id) ON DELETE CASCADE,
|
||
version TEXT NOT NULL,
|
||
file_path TEXT NOT NULL,
|
||
archive_path TEXT,
|
||
file_size INTEGER DEFAULT 0,
|
||
is_primary INTEGER DEFAULT 0,
|
||
dir_tree TEXT,
|
||
notes TEXT,
|
||
imported_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create source_library tables");
|
||
|
||
// 增量迁移:项目历史快照(每日一条,供 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");
|
||
|
||
// 增量迁移:云产品管理
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS cloud_products (
|
||
id TEXT PRIMARY KEY,
|
||
vendor TEXT NOT NULL DEFAULT 'aliyun',
|
||
product_type TEXT NOT NULL DEFAULT 'ecs',
|
||
name TEXT NOT NULL,
|
||
region TEXT,
|
||
instance_id TEXT,
|
||
endpoint TEXT,
|
||
expire_at TEXT,
|
||
status TEXT NOT NULL DEFAULT 'active',
|
||
config_data TEXT NOT NULL DEFAULT '{}',
|
||
notes TEXT,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS cloud_db_databases (
|
||
id TEXT PRIMARY KEY,
|
||
product_id TEXT NOT NULL REFERENCES cloud_products(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
username TEXT,
|
||
password TEXT,
|
||
charset TEXT DEFAULT 'utf8mb4',
|
||
notes TEXT,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create cloud_products table");
|
||
|
||
// 增量迁移:cloud_db_databases 加 db_driver 字段
|
||
let _ = conn.execute(
|
||
"ALTER TABLE cloud_db_databases ADD COLUMN db_driver TEXT NOT NULL DEFAULT 'mysql'",
|
||
[],
|
||
);
|
||
|
||
// 增量迁移:Docker 容器应用管理
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS docker_apps (
|
||
id TEXT PRIMARY KEY,
|
||
product_id TEXT NOT NULL REFERENCES cloud_products(id) ON DELETE CASCADE,
|
||
name TEXT NOT NULL,
|
||
access_url TEXT,
|
||
username TEXT,
|
||
password TEXT,
|
||
notes TEXT,
|
||
sort_order INTEGER DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now')),
|
||
updated_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create docker_apps table");
|
||
|
||
// 增量迁移:Agent 基础设施文档腐化评分缓存
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS doc_freshness_cache (
|
||
project_id TEXT PRIMARY KEY,
|
||
score TEXT NOT NULL,
|
||
details TEXT NOT NULL,
|
||
checked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||
);
|
||
").expect("create doc_freshness_cache table");
|
||
|
||
// 增量迁移:Gitea 实例 + 项目-仓库绑定
|
||
conn.execute_batch("
|
||
CREATE TABLE IF NOT EXISTS gitea_instances (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
url TEXT NOT NULL,
|
||
access_token TEXT NOT NULL,
|
||
is_default INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS gitea_repos (
|
||
id TEXT PRIMARY KEY,
|
||
instance_id TEXT NOT NULL REFERENCES gitea_instances(id) ON DELETE CASCADE,
|
||
project_id TEXT NOT NULL,
|
||
gitea_owner TEXT NOT NULL,
|
||
gitea_repo_name TEXT NOT NULL,
|
||
clone_url TEXT,
|
||
webhook_secret TEXT,
|
||
webhook_id INTEGER,
|
||
linked_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE UNIQUE INDEX IF NOT EXISTS idx_gitea_repos_project ON gitea_repos(project_id);
|
||
").expect("create gitea tables");
|
||
}
|
||
|
||
#[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);
|
||
}
|
||
}
|