mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 00:35:32 +08:00
refactor(profiles): shared project entity with per-scope switching
Projects are now global shared entities; Claude and Codex groups switch independently via scoped current pointers and scoped payload slots. - Remove scope column from profiles; keep current_profile_id_<scope> - Use Option<Vec<String>> for mcp/skills to distinguish 'never captured' from 'captured empty', preventing cross-side accidental disable - Update/apply operations scoped to the active group via merge_scope_from - Tray menu nests same shared list under Claude Code/Codex groups - Add i18n for per-scope tooltips and 'not saved for this side' hint Refs: profile P1 shared-entity redesign
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
//! 项目 Profile 数据访问对象
|
||||
//!
|
||||
//! profiles 表存放按 app 快照的配置方案(供应商/MCP/Skills/Prompt),
|
||||
//! payload 为原始 JSON 文本,解析在 service 层进行。
|
||||
//! current_profile_id 存放于 settings 表(key-value)。
|
||||
//! profiles 表存放全应用共享的项目实体(供应商/MCP/Skills/Prompt 快照),
|
||||
//! payload 为原始 JSON 文本(按 app 分槽),解析在 service 层进行。
|
||||
//! 各应用分组(scope)独立的 current 标记存放于 settings 表(key-value)。
|
||||
|
||||
use crate::database::{lock_conn, Database};
|
||||
use crate::error::AppError;
|
||||
use rusqlite::params;
|
||||
|
||||
const CURRENT_PROFILE_ID_KEY: &str = "current_profile_id";
|
||||
/// 每个 scope 的 current 标记 key = 前缀 + scope(如 current_profile_id_claude)
|
||||
const CURRENT_PROFILE_ID_KEY_PREFIX: &str = "current_profile_id_";
|
||||
|
||||
/// 项目 Profile 记录
|
||||
fn current_profile_key(scope: &str) -> String {
|
||||
format!("{CURRENT_PROFILE_ID_KEY_PREFIX}{scope}")
|
||||
}
|
||||
|
||||
/// 项目 Profile 记录(全应用共享,无所属分组)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Profile {
|
||||
pub id: String,
|
||||
@@ -109,22 +114,20 @@ impl Database {
|
||||
Ok(affected > 0)
|
||||
}
|
||||
|
||||
/// 读取当前激活的项目 id(未使用项目时为 None)
|
||||
pub fn get_current_profile_id(&self) -> Result<Option<String>, AppError> {
|
||||
self.get_setting(CURRENT_PROFILE_ID_KEY)
|
||||
/// 读取某分组当前激活的项目 id(未使用项目时为 None)
|
||||
pub fn get_current_profile_id(&self, scope: &str) -> Result<Option<String>, AppError> {
|
||||
self.get_setting(¤t_profile_key(scope))
|
||||
}
|
||||
|
||||
/// 设置当前激活的项目 id;None 表示"不使用项目"(删除 key)
|
||||
pub fn set_current_profile_id(&self, id: Option<&str>) -> Result<(), AppError> {
|
||||
/// 设置某分组当前激活的项目 id;None 表示"不使用项目"(删除 key)
|
||||
pub fn set_current_profile_id(&self, scope: &str, id: Option<&str>) -> Result<(), AppError> {
|
||||
let key = current_profile_key(scope);
|
||||
match id {
|
||||
Some(id) => self.set_setting(CURRENT_PROFILE_ID_KEY, id),
|
||||
Some(id) => self.set_setting(&key, id),
|
||||
None => {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
"DELETE FROM settings WHERE key = ?1",
|
||||
params![CURRENT_PROFILE_ID_KEY],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute("DELETE FROM settings WHERE key = ?1", params![key])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -180,17 +183,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_profile_id_set_and_clear() -> Result<(), AppError> {
|
||||
fn test_current_profile_id_is_scoped() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
assert_eq!(db.get_current_profile_id()?, None);
|
||||
db.set_current_profile_id(Some("a"))?;
|
||||
assert_eq!(db.get_current_profile_id()?, Some("a".to_string()));
|
||||
db.set_current_profile_id(None)?;
|
||||
assert_eq!(db.get_current_profile_id()?, None);
|
||||
assert_eq!(db.get_current_profile_id("claude")?, None);
|
||||
assert_eq!(db.get_current_profile_id("codex")?, None);
|
||||
|
||||
// 两个分组的标记互不影响
|
||||
db.set_current_profile_id("claude", Some("a"))?;
|
||||
db.set_current_profile_id("codex", Some("b"))?;
|
||||
assert_eq!(db.get_current_profile_id("claude")?, Some("a".to_string()));
|
||||
assert_eq!(db.get_current_profile_id("codex")?, Some("b".to_string()));
|
||||
|
||||
db.set_current_profile_id("claude", None)?;
|
||||
assert_eq!(db.get_current_profile_id("claude")?, None);
|
||||
assert_eq!(db.get_current_profile_id("codex")?, Some("b".to_string()));
|
||||
|
||||
// 重复清除应幂等
|
||||
db.set_current_profile_id(None)?;
|
||||
assert_eq!(db.get_current_profile_id()?, None);
|
||||
db.set_current_profile_id("claude", None)?;
|
||||
assert_eq!(db.get_current_profile_id("claude")?, None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,8 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 19. Profiles 表(项目配置方案:按 app 快照供应商/MCP/Skills/Prompt)
|
||||
// 19. Profiles 表(全应用共享的项目实体,payload 按 app 分槽快照
|
||||
// 供应商/MCP/Skills/Prompt;各应用分组的 current 标记在 settings 表)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -309,6 +310,20 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 修复跑过未发布开发版的库:current 标记曾是全局 key,现按应用分组
|
||||
// (随 v12 定稿为 current_profile_id_<scope>,不单独 bump 版本)
|
||||
if conn
|
||||
.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value)
|
||||
SELECT 'current_profile_id_claude', value FROM settings
|
||||
WHERE key = 'current_profile_id'",
|
||||
[],
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
let _ = conn.execute("DELETE FROM settings WHERE key = 'current_profile_id'", []);
|
||||
}
|
||||
|
||||
// 尝试添加 live_takeover_active 列到 proxy_config 表
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE proxy_config ADD COLUMN live_takeover_active INTEGER NOT NULL DEFAULT 0",
|
||||
|
||||
@@ -426,6 +426,62 @@ fn migration_v10_to_v11_rebuilds_rollups_with_request_model_dimension() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_create_tables_repairs_dev_global_profile_marker() {
|
||||
let conn = Connection::open_in_memory().expect("open memory db");
|
||||
|
||||
// 模拟跑过未发布开发版的库:user_version 已是 12(迁移不会再跑),
|
||||
// 但 current 标记还是全局 key(现按应用分组)
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE profiles (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
sort_order INTEGER,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
);
|
||||
INSERT INTO profiles (id, name, payload) VALUES ('p1', 'Project A', '{}');
|
||||
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT);
|
||||
INSERT INTO settings (key, value) VALUES ('current_profile_id', 'p1');
|
||||
"#,
|
||||
)
|
||||
.expect("seed dev v12 shape");
|
||||
Database::set_user_version(&conn, 12).expect("set user_version=12");
|
||||
|
||||
Database::create_tables_on_conn(&conn).expect("create tables should repair marker");
|
||||
|
||||
// 全局 current 标记改名为 claude 组标记,旧 key 删除
|
||||
let claude_marker: String = conn
|
||||
.query_row(
|
||||
"SELECT value FROM settings WHERE key = 'current_profile_id_claude'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("scoped current marker");
|
||||
assert_eq!(claude_marker, "p1");
|
||||
let old_marker: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM settings WHERE key = 'current_profile_id'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("count old marker");
|
||||
assert_eq!(old_marker, 0);
|
||||
|
||||
// 修复必须幂等:再跑一遍不应破坏已迁移的标记
|
||||
Database::create_tables_on_conn(&conn).expect("repair is idempotent");
|
||||
let claude_marker: String = conn
|
||||
.query_row(
|
||||
"SELECT value FROM settings WHERE key = 'current_profile_id_claude'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("scoped current marker survives");
|
||||
assert_eq!(claude_marker, "p1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_create_tables_repairs_legacy_proxy_config_singleton_to_per_app() {
|
||||
let conn = Connection::open_in_memory().expect("open memory db");
|
||||
|
||||
Reference in New Issue
Block a user