refactor(skill): remove skillsPath configuration (#310)

Remove the skillsPath field from SkillRepo and Skill structs since
recursive scanning now automatically discovers skills in all directories.
Simplify the UI by removing the path input field.
This commit is contained in:
YoVinchen
2025-11-28 16:26:28 +08:00
committed by GitHub
parent f4c284f86c
commit 0f959112b1
14 changed files with 30 additions and 142 deletions

View File

@@ -90,7 +90,6 @@ pub async fn install_skill(
.clone()
.unwrap_or_else(|| "main".to_string()),
enabled: true,
skills_path: skill.skills_path.clone(), // 使用技能记录的 skills_path
};
service

View File

@@ -58,7 +58,9 @@ impl Database {
pub fn get_skill_repos(&self) -> Result<Vec<SkillRepo>, AppError> {
let conn = lock_conn!(self.conn);
let mut stmt = conn
.prepare("SELECT owner, name, branch, enabled, skills_path FROM skill_repos ORDER BY owner ASC, name ASC")
.prepare(
"SELECT owner, name, branch, enabled FROM skill_repos ORDER BY owner ASC, name ASC",
)
.map_err(|e| AppError::Database(e.to_string()))?;
let repo_iter = stmt
@@ -68,7 +70,6 @@ impl Database {
name: row.get(1)?,
branch: row.get(2)?,
enabled: row.get(3)?,
skills_path: row.get(4)?,
})
})
.map_err(|e| AppError::Database(e.to_string()))?;
@@ -84,8 +85,8 @@ impl Database {
pub fn save_skill_repo(&self, repo: &SkillRepo) -> Result<(), AppError> {
let conn = lock_conn!(self.conn);
conn.execute(
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
params![repo.owner, repo.name, repo.branch, repo.enabled],
).map_err(|e| AppError::Database(e.to_string()))?;
Ok(())
}

View File

@@ -202,8 +202,8 @@ impl Database {
for repo in &config.skills.repos {
tx.execute(
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled, skills_path) VALUES (?1, ?2, ?3, ?4, ?5)",
params![repo.owner, repo.name, repo.branch, repo.enabled, repo.skills_path],
"INSERT OR REPLACE INTO skill_repos (owner, name, branch, enabled) VALUES (?1, ?2, ?3, ?4)",
params![repo.owner, repo.name, repo.branch, repo.enabled],
).map_err(|e| AppError::Database(format!("Migrate skill repo failed: {e}")))?;
}

View File

@@ -104,7 +104,6 @@ impl Database {
name TEXT NOT NULL,
branch TEXT NOT NULL DEFAULT 'main',
enabled BOOLEAN NOT NULL DEFAULT 1,
skills_path TEXT,
PRIMARY KEY (owner, name)
)",
[],
@@ -233,7 +232,7 @@ impl Database {
"TEXT NOT NULL DEFAULT 'main'",
)?;
Self::add_column_if_missing(conn, "skill_repos", "enabled", "BOOLEAN NOT NULL DEFAULT 1")?;
Self::add_column_if_missing(conn, "skill_repos", "skills_path", "TEXT")?;
// 注意: skills_path 字段已被移除,因为现在支持全仓库递归扫描
Ok(())
}

View File

@@ -100,12 +100,8 @@ pub struct DeepLinkImportRequest {
/// Skill directory name
#[serde(skip_serializing_if = "Option::is_none")]
pub directory: Option<String>,
/// Repository branch (default: "main")
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
/// Skills subdirectory path (e.g., "skills")
#[serde(skip_serializing_if = "Option::is_none")]
pub skills_path: Option<String>,
// ============ Config file fields (v3.8+) ============
/// Base64 encoded config content

View File

@@ -143,7 +143,6 @@ fn parse_provider_deeplink(
repo: None,
directory: None,
branch: None,
skills_path: None,
config,
config_format,
config_url,
@@ -204,7 +203,6 @@ fn parse_prompt_deeplink(
repo: None,
directory: None,
branch: None,
skills_path: None,
config: None,
config_format: None,
config_url: None,
@@ -262,7 +260,6 @@ fn parse_mcp_deeplink(
repo: None,
directory: None,
branch: None,
skills_path: None,
config_url: None,
})
}
@@ -287,10 +284,6 @@ fn parse_skill_deeplink(
let directory = params.get("directory").cloned();
let branch = params.get("branch").cloned();
let skills_path = params
.get("skills_path")
.or_else(|| params.get("skillsPath"))
.cloned();
Ok(DeepLinkImportRequest {
version,
@@ -298,7 +291,6 @@ fn parse_skill_deeplink(
repo: Some(repo),
directory,
branch,
skills_path,
icon: None,
app: Some("claude".to_string()), // Skills are Claude-only
name: None,

View File

@@ -40,7 +40,6 @@ pub fn import_skill_from_deeplink(
name: name.clone(),
branch: request.branch.unwrap_or_else(|| "main".to_string()),
enabled: request.enabled.unwrap_or(true),
skills_path: request.skills_path,
};
// Save using Database

View File

@@ -142,7 +142,6 @@ fn test_build_gemini_provider_with_model() {
repo: None,
directory: None,
branch: None,
skills_path: None,
content: None,
description: None,
enabled: None,
@@ -189,7 +188,6 @@ fn test_build_gemini_provider_without_model() {
repo: None,
directory: None,
branch: None,
skills_path: None,
content: None,
description: None,
enabled: None,
@@ -231,7 +229,6 @@ fn test_parse_and_merge_config_claude() {
repo: None,
directory: None,
branch: None,
skills_path: None,
content: None,
description: None,
enabled: None,
@@ -275,7 +272,6 @@ fn test_parse_and_merge_config_url_override() {
repo: None,
directory: None,
branch: None,
skills_path: None,
content: None,
description: None,
enabled: None,
@@ -372,12 +368,11 @@ fn test_parse_mcp_deeplink() {
#[test]
fn test_parse_skill_deeplink() {
let url = "ccswitch://v1/import?resource=skill&repo=owner/repo&directory=skills&branch=dev&skills_path=src";
let url = "ccswitch://v1/import?resource=skill&repo=owner/repo&directory=skills&branch=dev";
let request = parse_deeplink_url(&url).unwrap();
assert_eq!(request.resource, "skill");
assert_eq!(request.repo.unwrap(), "owner/repo");
assert_eq!(request.directory.unwrap(), "skills");
assert_eq!(request.branch.unwrap(), "dev");
assert_eq!(request.skills_path.unwrap(), "src");
}

View File

@@ -34,9 +34,6 @@ pub struct Skill {
/// 分支名称
#[serde(rename = "repoBranch")]
pub repo_branch: Option<String>,
/// 技能所在的子目录路径 (可选, 如 "skills")
#[serde(rename = "skillsPath")]
pub skills_path: Option<String>,
}
/// 仓库配置
@@ -50,9 +47,6 @@ pub struct SkillRepo {
pub branch: String,
/// 是否启用
pub enabled: bool,
/// 技能所在的子目录路径 (可选, 如 "skills", "my-skills/subdir")
#[serde(rename = "skillsPath")]
pub skills_path: Option<String>,
}
/// 技能安装状态
@@ -84,21 +78,18 @@ impl Default for SkillStore {
name: "awesome-claude-skills".to_string(),
branch: "main".to_string(),
enabled: true,
skills_path: None, // 扫描根目录
},
SkillRepo {
owner: "anthropics".to_string(),
name: "skills".to_string(),
branch: "main".to_string(),
enabled: true,
skills_path: None, // 扫描根目录
},
SkillRepo {
owner: "cexll".to_string(),
name: "myclaude".to_string(),
branch: "master".to_string(),
enabled: true,
skills_path: Some("skills".to_string()), // 扫描 skills 子目录
},
],
}
@@ -194,25 +185,8 @@ impl SkillService {
})??;
let mut skills = Vec::new();
// 确定要扫描的目录路径
let scan_dir = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skillsPath则扫描该子目录
let subdir = temp_dir.join(skills_path.trim_matches('/'));
if !subdir.exists() {
log::warn!(
"仓库 {}/{} 中指定的技能路径 '{}' 不存在",
repo.owner,
repo.name,
skills_path
);
let _ = fs::remove_dir_all(&temp_dir);
return Ok(skills);
}
subdir
} else {
// 否则扫描仓库根目录
temp_dir.clone()
};
// 扫描仓库根目录(支持全仓库递归扫描)
let scan_dir = temp_dir.clone();
// 递归扫描目录查找所有技能
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?;
@@ -284,11 +258,7 @@ impl SkillService {
let meta = self.parse_skill_metadata(skill_md)?;
// 构建 README URL
let readme_path = if let Some(ref skills_path) = repo.skills_path {
format!("{}/{}", skills_path.trim_matches('/'), directory)
} else {
directory.to_string()
};
let readme_path = directory.to_string();
Ok(Skill {
key: format!("{}/{}:{}", repo.owner, repo.name, directory),
@@ -303,7 +273,6 @@ impl SkillService {
repo_owner: Some(repo.owner.clone()),
repo_name: Some(repo.name.clone()),
repo_branch: Some(repo.branch.clone()),
skills_path: repo.skills_path.clone(),
})
}
@@ -405,7 +374,6 @@ impl SkillService {
repo_owner: None,
repo_name: None,
repo_branch: None,
skills_path: None,
});
}
@@ -574,16 +542,8 @@ impl SkillService {
))
})??;
// 根据 skills_path 确定源目录路径
let source = if let Some(ref skills_path) = repo.skills_path {
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir
.join(skills_path.trim_matches('/'))
.join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)
};
// 确定源目录路径(技能相对于仓库根目录的路径)
let source = temp_dir.join(&directory);
if !source.exists() {
let _ = fs::remove_dir_all(&temp_dir);

View File

@@ -30,22 +30,11 @@ export function SkillConfirmation({
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-muted-foreground">
{t("deeplink.skill.branch")}
</label>
<div className="mt-1 text-sm">{request.branch || "main"}</div>
</div>
{request.skillsPath && (
<div>
<label className="block text-sm font-medium text-muted-foreground">
{t("deeplink.skill.skillsPath")}
</label>
<div className="mt-1 text-sm">{request.skillsPath}</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-muted-foreground">
{t("deeplink.skill.branch")}
</label>
<div className="mt-1 text-sm">{request.branch || "main"}</div>
</div>
<div className="text-blue-600 dark:text-blue-400 text-sm bg-blue-50 dark:bg-blue-950/30 p-3 rounded border border-blue-200 dark:border-blue-800">

View File

@@ -34,7 +34,6 @@ export function RepoManager({
const { t } = useTranslation();
const [repoUrl, setRepoUrl] = useState("");
const [branch, setBranch] = useState("");
const [skillsPath, setSkillsPath] = useState("");
const [error, setError] = useState("");
const getSkillCount = (repo: SkillRepo) =>
@@ -80,12 +79,10 @@ export function RepoManager({
name: parsed.name,
branch: branch || "main",
enabled: true,
skillsPath: skillsPath.trim() || undefined, // 仅在有值时传递
});
setRepoUrl("");
setBranch("");
setSkillsPath("");
} catch (e) {
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
}
@@ -130,13 +127,6 @@ export function RepoManager({
onChange={(e) => setBranch(e.target.value)}
className="flex-1"
/>
<Input
id="skills-path"
placeholder={t("skills.repo.pathPlaceholder")}
value={skillsPath}
onChange={(e) => setSkillsPath(e.target.value)}
className="flex-1"
/>
<Button
onClick={handleAdd}
className="w-full sm:w-auto sm:px-4"
@@ -171,12 +161,6 @@ export function RepoManager({
</div>
<div className="mt-1 text-xs text-muted-foreground">
{t("skills.repo.branch")}: {repo.branch || "main"}
{repo.skillsPath && (
<>
<span className="mx-2"></span>
{t("skills.repo.path")}: {repo.skillsPath}
</>
)}
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
{t("skills.repo.skillCount", {
count: getSkillCount(repo),

View File

@@ -26,7 +26,6 @@ export function RepoManagerPanel({
const { t } = useTranslation();
const [repoUrl, setRepoUrl] = useState("");
const [branch, setBranch] = useState("");
const [skillsPath, setSkillsPath] = useState("");
const [error, setError] = useState("");
const getSkillCount = (repo: SkillRepo) =>
@@ -67,12 +66,10 @@ export function RepoManagerPanel({
name: parsed.name,
branch: branch || "main",
enabled: true,
skillsPath: skillsPath.trim() || undefined,
});
setRepoUrl("");
setBranch("");
setSkillsPath("");
} catch (e) {
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
}
@@ -110,31 +107,17 @@ export function RepoManagerPanel({
className="mt-2"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<Label htmlFor="branch" className="text-foreground">
{t("skills.repo.branch")}
</Label>
<Input
id="branch"
placeholder={t("skills.repo.branchPlaceholder")}
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="mt-2"
/>
</div>
<div>
<Label htmlFor="skills-path" className="text-foreground">
{t("skills.repo.path")}
</Label>
<Input
id="skills-path"
placeholder={t("skills.repo.pathPlaceholder")}
value={skillsPath}
onChange={(e) => setSkillsPath(e.target.value)}
className="mt-2"
/>
</div>
<div>
<Label htmlFor="branch" className="text-foreground">
{t("skills.repo.branch")}
</Label>
<Input
id="branch"
placeholder={t("skills.repo.branchPlaceholder")}
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="mt-2"
/>
</div>
{error && (
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
@@ -174,12 +157,6 @@ export function RepoManagerPanel({
</div>
<div className="mt-1 text-xs text-muted-foreground">
{t("skills.repo.branch")}: {repo.branch || "main"}
{repo.skillsPath && (
<>
<span className="mx-2"></span>
{t("skills.repo.path")}: {repo.skillsPath}
</>
)}
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
{t("skills.repo.skillCount", {
count: getSkillCount(repo),

View File

@@ -33,7 +33,6 @@ export interface DeepLinkImportRequest {
repo?: string;
directory?: string;
branch?: string;
skillsPath?: string;
// Config file fields
config?: string;

View File

@@ -10,7 +10,6 @@ export interface Skill {
repoOwner?: string;
repoName?: string;
repoBranch?: string;
skillsPath?: string; // 技能所在的子目录路径,如 "skills"
}
export interface SkillRepo {
@@ -18,7 +17,6 @@ export interface SkillRepo {
name: string;
branch: string;
enabled: boolean;
skillsPath?: string; // 可选:技能所在的子目录路径,如 "skills"
}
export const skillsApi = {