Compare commits

...

1 Commits

Author SHA1 Message Date
YoVinchen
d9a4c7cb86 refactor(skill): remove skillsPath configuration
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.
2025-11-28 16:23:06 +08:00
14 changed files with 30 additions and 142 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -100,12 +100,8 @@ pub struct DeepLinkImportRequest {
/// Skill directory name /// Skill directory name
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub directory: Option<String>, pub directory: Option<String>,
/// Repository branch (default: "main")
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>, 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+) ============ // ============ Config file fields (v3.8+) ============
/// Base64 encoded config content /// Base64 encoded config content

View File

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

View File

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

View File

@@ -142,7 +142,6 @@ fn test_build_gemini_provider_with_model() {
repo: None, repo: None,
directory: None, directory: None,
branch: None, branch: None,
skills_path: None,
content: None, content: None,
description: None, description: None,
enabled: None, enabled: None,
@@ -189,7 +188,6 @@ fn test_build_gemini_provider_without_model() {
repo: None, repo: None,
directory: None, directory: None,
branch: None, branch: None,
skills_path: None,
content: None, content: None,
description: None, description: None,
enabled: None, enabled: None,
@@ -231,7 +229,6 @@ fn test_parse_and_merge_config_claude() {
repo: None, repo: None,
directory: None, directory: None,
branch: None, branch: None,
skills_path: None,
content: None, content: None,
description: None, description: None,
enabled: None, enabled: None,
@@ -275,7 +272,6 @@ fn test_parse_and_merge_config_url_override() {
repo: None, repo: None,
directory: None, directory: None,
branch: None, branch: None,
skills_path: None,
content: None, content: None,
description: None, description: None,
enabled: None, enabled: None,
@@ -372,12 +368,11 @@ fn test_parse_mcp_deeplink() {
#[test] #[test]
fn test_parse_skill_deeplink() { 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(); let request = parse_deeplink_url(&url).unwrap();
assert_eq!(request.resource, "skill"); assert_eq!(request.resource, "skill");
assert_eq!(request.repo.unwrap(), "owner/repo"); assert_eq!(request.repo.unwrap(), "owner/repo");
assert_eq!(request.directory.unwrap(), "skills"); assert_eq!(request.directory.unwrap(), "skills");
assert_eq!(request.branch.unwrap(), "dev"); 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")] #[serde(rename = "repoBranch")]
pub repo_branch: Option<String>, 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 branch: String,
/// 是否启用 /// 是否启用
pub enabled: bool, 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(), name: "awesome-claude-skills".to_string(),
branch: "main".to_string(), branch: "main".to_string(),
enabled: true, enabled: true,
skills_path: None, // 扫描根目录
}, },
SkillRepo { SkillRepo {
owner: "anthropics".to_string(), owner: "anthropics".to_string(),
name: "skills".to_string(), name: "skills".to_string(),
branch: "main".to_string(), branch: "main".to_string(),
enabled: true, enabled: true,
skills_path: None, // 扫描根目录
}, },
SkillRepo { SkillRepo {
owner: "cexll".to_string(), owner: "cexll".to_string(),
name: "myclaude".to_string(), name: "myclaude".to_string(),
branch: "master".to_string(), branch: "master".to_string(),
enabled: true, enabled: true,
skills_path: Some("skills".to_string()), // 扫描 skills 子目录
}, },
], ],
} }
@@ -194,25 +185,8 @@ impl SkillService {
})??; })??;
let mut skills = Vec::new(); let mut skills = Vec::new();
// 确定要扫描的目录路径 // 扫描仓库根目录(支持全仓库递归扫描)
let scan_dir = if let Some(ref skills_path) = repo.skills_path { let scan_dir = temp_dir.clone();
// 如果指定了 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()
};
// 递归扫描目录查找所有技能 // 递归扫描目录查找所有技能
self.scan_dir_recursive(&scan_dir, &scan_dir, repo, &mut skills)?; 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)?; let meta = self.parse_skill_metadata(skill_md)?;
// 构建 README URL // 构建 README URL
let readme_path = if let Some(ref skills_path) = repo.skills_path { let readme_path = directory.to_string();
format!("{}/{}", skills_path.trim_matches('/'), directory)
} else {
directory.to_string()
};
Ok(Skill { Ok(Skill {
key: format!("{}/{}:{}", repo.owner, repo.name, directory), key: format!("{}/{}:{}", repo.owner, repo.name, directory),
@@ -303,7 +273,6 @@ impl SkillService {
repo_owner: Some(repo.owner.clone()), repo_owner: Some(repo.owner.clone()),
repo_name: Some(repo.name.clone()), repo_name: Some(repo.name.clone()),
repo_branch: Some(repo.branch.clone()), repo_branch: Some(repo.branch.clone()),
skills_path: repo.skills_path.clone(),
}) })
} }
@@ -405,7 +374,6 @@ impl SkillService {
repo_owner: None, repo_owner: None,
repo_name: None, repo_name: None,
repo_branch: 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 { let source = temp_dir.join(&directory);
// 如果指定了 skills_path源路径为: temp_dir/skills_path/directory
temp_dir
.join(skills_path.trim_matches('/'))
.join(&directory)
} else {
// 否则源路径为: temp_dir/directory
temp_dir.join(&directory)
};
if !source.exists() { if !source.exists() {
let _ = fs::remove_dir_all(&temp_dir); let _ = fs::remove_dir_all(&temp_dir);

View File

@@ -30,22 +30,11 @@ export function SkillConfirmation({
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div>
<div> <label className="block text-sm font-medium text-muted-foreground">
<label className="block text-sm font-medium text-muted-foreground"> {t("deeplink.skill.branch")}
{t("deeplink.skill.branch")} </label>
</label> <div className="mt-1 text-sm">{request.branch || "main"}</div>
<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> </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"> <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 { t } = useTranslation();
const [repoUrl, setRepoUrl] = useState(""); const [repoUrl, setRepoUrl] = useState("");
const [branch, setBranch] = useState(""); const [branch, setBranch] = useState("");
const [skillsPath, setSkillsPath] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const getSkillCount = (repo: SkillRepo) => const getSkillCount = (repo: SkillRepo) =>
@@ -80,12 +79,10 @@ export function RepoManager({
name: parsed.name, name: parsed.name,
branch: branch || "main", branch: branch || "main",
enabled: true, enabled: true,
skillsPath: skillsPath.trim() || undefined, // 仅在有值时传递
}); });
setRepoUrl(""); setRepoUrl("");
setBranch(""); setBranch("");
setSkillsPath("");
} catch (e) { } catch (e) {
setError(e instanceof Error ? e.message : t("skills.repo.addFailed")); setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
} }
@@ -130,13 +127,6 @@ export function RepoManager({
onChange={(e) => setBranch(e.target.value)} onChange={(e) => setBranch(e.target.value)}
className="flex-1" className="flex-1"
/> />
<Input
id="skills-path"
placeholder={t("skills.repo.pathPlaceholder")}
value={skillsPath}
onChange={(e) => setSkillsPath(e.target.value)}
className="flex-1"
/>
<Button <Button
onClick={handleAdd} onClick={handleAdd}
className="w-full sm:w-auto sm:px-4" className="w-full sm:w-auto sm:px-4"
@@ -171,12 +161,6 @@ export function RepoManager({
</div> </div>
<div className="mt-1 text-xs text-muted-foreground"> <div className="mt-1 text-xs text-muted-foreground">
{t("skills.repo.branch")}: {repo.branch || "main"} {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]"> <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", { {t("skills.repo.skillCount", {
count: getSkillCount(repo), count: getSkillCount(repo),

View File

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

View File

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

View File

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