Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7481,6 +7481,131 @@ pub async fn list_installed_skills() -> Result<CommandResult<SkillsPayload>, Str
}).await.map_err(|error| format!("读取本地 Skills 失败:{error}"))
}

async fn resolve_skill_repo_for_operation(
manager: &codex_plus_core::skills::SkillsManager,
repo_key: &str,
) -> anyhow::Result<Vec<codex_plus_core::skills::RemoteSkill>> {
let (guard, snapshot) = {
let _coordination = skills_repo_coordination();
(manager.capture_repo_guard(repo_key)?, repo_skill_cache_snapshot(repo_key))
};
let (resolved, skills) =
codex_plus_core::skills::resolve_repo_skills(guard.repo.clone(), &snapshot.skills).await?;
anyhow::ensure!(resolved.key() == repo_key, "仓库分支已变化,请先重新检查仓库");
let _coordination = skills_repo_coordination();
anyhow::ensure!(manager.repo_guard_is_current(&guard)?, "仓库在检查期间已改变,请重试");
store_repo_skills_if_current(repo_key, &skills, &snapshot);
Ok(skills)
}

#[tauri::command]
pub async fn find_skill_repository_matches(
repo_key: String,
id: Option<String>,
local_path: Option<String>,
) -> CommandResult<codex_plus_core::skills::SkillRepositoryMatchPreview> {
let manager = default_skills_manager();
let result = async {
let skills = resolve_skill_repo_for_operation(&manager, &repo_key).await?;
let repo = manager.list_repos().into_iter().find(|repo| repo.key() == repo_key)
.ok_or_else(|| anyhow::anyhow!("仓库已不存在"))?;
tauri::async_runtime::spawn_blocking(move || {
match (id.as_deref(), local_path.as_deref()) {
(Some(id), Some(path)) => manager.preview_repository_matches_for_instance(
&repo, &skills, id, std::path::Path::new(path),
),
(None, Some(_)) => anyhow::bail!("选择安装位置时必须指定 Skill"),
(_, None) => manager.preview_repository_matches(&repo, &skills, id.as_deref()),
}
}).await?
}.await;
match result {
Ok(preview) => ok("来源候选已检查,请确认后关联。", preview),
Err(error) => failed(
&format!("匹配 Skill 来源失败:{error:#}"),
codex_plus_core::skills::SkillRepositoryMatchPreview::default(),
),
}
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillRepositoryApplyPayload {
#[serde(flatten)]
skills: SkillsPayload,
applied: Vec<codex_plus_core::skills::SkillRepositoryMatchApplied>,
errors: Vec<String>,
}

#[tauri::command]
pub async fn apply_skill_repository_matches(
selections: Vec<codex_plus_core::skills::SkillRepositoryMatchSelection>,
) -> Result<CommandResult<SkillRepositoryApplyPayload>, String> {
let manager = default_skills_manager();
let keys: std::collections::BTreeSet<_> = selections.iter()
.map(|selection| selection.repo_key.clone()).collect();
let mut remote = Vec::new();
let mut errors = Vec::new();
let mut available = std::collections::BTreeSet::new();
for key in keys {
match resolve_skill_repo_for_operation(&manager, &key).await {
Ok(skills) => {
available.insert(key);
remote.extend(skills);
}
Err(error) => errors.push(format!("{key}:{error:#}")),
}
}
let valid: Vec<_> = selections.into_iter()
.filter(|selection| available.contains(&selection.repo_key)).collect();
tauri::async_runtime::spawn_blocking(move || {
let applied = match manager.apply_repository_matches(&valid, &remote) {
Ok(outcome) => {
errors.extend(outcome.errors);
outcome.applied
}
Err(error) => {
errors.push(format!("{error:#}"));
Vec::new()
}
};
let payload = SkillRepositoryApplyPayload {
skills: current_skills_payload(&manager),
applied,
errors,
};
if payload.errors.is_empty() {
ok("已关联所选来源,更新方式未改变。", payload)
} else {
failed(&format!("部分来源未关联:{}", payload.errors.join(";")), payload)
}
}).await.map_err(|error| format!("关联 Skill 来源失败:{error}"))
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillSourceEvidencePayload {
evidence: Vec<codex_plus_core::skills::SkillSourceEvidence>,
}

#[tauri::command]
pub async fn discover_skill_source_evidence(id: String, local_path: Option<String>) -> Result<CommandResult<SkillSourceEvidencePayload>, String> {
tauri::async_runtime::spawn_blocking(move || {
let manager = default_skills_manager();
let evidence = match local_path.as_deref() {
Some(path) => manager.discover_skill_source_evidence_for_instance(id.trim(), std::path::Path::new(path)),
None => manager.discover_skill_source_evidence(id.trim()),
};
match evidence {
Ok(evidence) => ok("已检查本地来源线索。", SkillSourceEvidencePayload { evidence }),
Err(error) => failed(
&format!("检查来源线索失败:{error:#}"),
SkillSourceEvidencePayload { evidence: Vec::new() },
),
}
}).await.map_err(|error| format!("检查来源线索失败:{error}"))
}

/// 导入 cc-switch 已安装的 Skills。cc-switch 的 Skill 文件位于
/// `%USERPROFILE%/.cc-switch/skills/<id>/SKILL.md`,不在其 SQLite 数据库中。
#[tauri::command]
Expand Down
3 changes: 3 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ pub fn run() {
commands::delete_user_script,
commands::refresh_skill_catalog,
commands::list_installed_skills,
commands::find_skill_repository_matches,
commands::apply_skill_repository_matches,
commands::discover_skill_source_evidence,
commands::import_ccs_skills,
commands::import_local_skills,
commands::install_skill,
Expand Down
Loading