fix: replace implicit app inference with explicit selection for Skills import and sync

Skills import previously inferred app enablement from filesystem presence,
causing incorrect multi-app activation when the same skill directory existed
under multiple app paths. Now the frontend submits explicit app selections
via ImportSkillSelection, and schema migration preserves a snapshot of
legacy app mappings to avoid lossy reconstruction.

Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks)
and MCP sync_all_enabled (removes disabled servers from live config).
This commit is contained in:
Jason
2026-03-14 17:20:01 +08:00
parent f1d2c6045b
commit 7097a0d710
13 changed files with 560 additions and 39 deletions
+31 -1
View File
@@ -5,6 +5,13 @@
use super::{lock_conn, Database, SCHEMA_VERSION};
use crate::error::AppError;
use rusqlite::Connection;
use serde::Serialize;
#[derive(Serialize)]
struct LegacySkillMigrationRow {
directory: String,
app_type: String,
}
impl Database {
/// 创建所有数据库表
@@ -846,12 +853,31 @@ impl Database {
log::info!("开始迁移 skills 表到 v3 结构(统一管理架构)...");
// 1. 备份旧数据(用于日志)
// 1. 备份旧数据(用于日志和后续启动迁移
let old_count: i64 = conn
.query_row("SELECT COUNT(*) FROM skills", [], |row| row.get(0))
.unwrap_or(0);
log::info!("旧 skills 表有 {old_count} 条记录");
let mut stmt = conn
.prepare(
"SELECT directory, app_type FROM skills
WHERE installed = 1",
)
.map_err(|e| AppError::Database(format!("查询旧 skills 快照失败: {e}")))?;
let snapshot_rows: Vec<LegacySkillMigrationRow> = stmt
.query_map([], |row| {
Ok(LegacySkillMigrationRow {
directory: row.get(0)?,
app_type: row.get(1)?,
})
})
.map_err(|e| AppError::Database(format!("读取旧 skills 快照失败: {e}")))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| AppError::Database(format!("解析旧 skills 快照失败: {e}")))?;
let snapshot_json = serde_json::to_string(&snapshot_rows)
.map_err(|e| AppError::Database(format!("序列化旧 skills 快照失败: {e}")))?;
// 标记:需要在启动后从文件系统扫描并重建 Skills 数据
// 说明:v3 结构将 Skills 的 SSOT 迁移到 ~/.cc-switch/skills/
// 旧表只存“安装记录”,无法直接无损迁移到新结构,因此改为启动后扫描 app 目录导入。
@@ -859,6 +885,10 @@ impl Database {
"INSERT OR REPLACE INTO settings (key, value) VALUES ('skills_ssot_migration_pending', 'true')",
[],
);
let _ = conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('skills_ssot_migration_snapshot', ?1)",
[snapshot_json],
);
// 2. 删除旧表
conn.execute("DROP TABLE IF EXISTS skills", [])
+19
View File
@@ -488,6 +488,25 @@ fn migration_from_v3_8_schema_v1_to_current_schema_v3() {
matches!(pending.as_deref(), Some("true") | Some("1")),
"skills_ssot_migration_pending should be set after v2->v3 migration"
);
let snapshot: Option<String> = conn
.query_row(
"SELECT value FROM settings WHERE key = 'skills_ssot_migration_snapshot'",
[],
|r| r.get(0),
)
.ok();
let snapshot = snapshot.expect("skills migration snapshot should be recorded");
let snapshot_rows: serde_json::Value =
serde_json::from_str(&snapshot).expect("parse skills migration snapshot");
assert!(
snapshot_rows
.as_array()
.is_some_and(|rows| rows.iter().any(|row| {
row.get("directory").and_then(|v| v.as_str()) == Some("demo-skill")
&& row.get("app_type").and_then(|v| v.as_str()) == Some("claude")
})),
"skills migration snapshot should preserve legacy app mapping"
);
// v3.9+ 新增:proxy_config 三行 seed 必须存在(否则 UI 会查不到默认值)
let proxy_rows: i64 = conn