mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
feat(skills): add install from ZIP file feature
- Add open_zip_file_dialog command for selecting ZIP files
- Add install_from_zip service method with recursive skill scanning
- Add install_skills_from_zip Tauri command
- Add frontend API methods and useInstallSkillsFromZip hook
- Add "Install from ZIP" button in Skills management page
- Support local skill ID format: local:{directory}
- Add i18n translations for new feature and error messages
This commit is contained in:
@@ -109,3 +109,17 @@ pub async fn open_file_dialog<R: tauri::Runtime>(
|
|||||||
|
|
||||||
Ok(result.map(|p| p.to_string()))
|
Ok(result.map(|p| p.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 打开 ZIP 文件选择对话框
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_zip_file_dialog<R: tauri::Runtime>(
|
||||||
|
app: tauri::AppHandle<R>,
|
||||||
|
) -> Result<Option<String>, String> {
|
||||||
|
let dialog = app.dialog();
|
||||||
|
let result = dialog
|
||||||
|
.file()
|
||||||
|
.add_filter("ZIP", &["zip"])
|
||||||
|
.blocking_pick_file();
|
||||||
|
|
||||||
|
Ok(result.map(|p| p.to_string()))
|
||||||
|
}
|
||||||
|
|||||||
@@ -249,3 +249,16 @@ pub fn remove_skill_repo(
|
|||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 从 ZIP 文件安装 Skills
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn install_skills_from_zip(
|
||||||
|
file_path: String,
|
||||||
|
current_app: String,
|
||||||
|
app_state: State<'_, AppState>,
|
||||||
|
) -> Result<Vec<InstalledSkill>, String> {
|
||||||
|
let app_type = parse_app_type(¤t_app)?;
|
||||||
|
let path = std::path::Path::new(&file_path);
|
||||||
|
|
||||||
|
SkillService::install_from_zip(&app_state.db, path, &app_type).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -850,6 +850,7 @@ pub fn run() {
|
|||||||
commands::import_config_from_file,
|
commands::import_config_from_file,
|
||||||
commands::save_file_dialog,
|
commands::save_file_dialog,
|
||||||
commands::open_file_dialog,
|
commands::open_file_dialog,
|
||||||
|
commands::open_zip_file_dialog,
|
||||||
commands::sync_current_providers_live,
|
commands::sync_current_providers_live,
|
||||||
// Deep link import
|
// Deep link import
|
||||||
commands::parse_deeplink,
|
commands::parse_deeplink,
|
||||||
@@ -879,6 +880,7 @@ pub fn run() {
|
|||||||
commands::get_skill_repos,
|
commands::get_skill_repos,
|
||||||
commands::add_skill_repo,
|
commands::add_skill_repo,
|
||||||
commands::remove_skill_repo,
|
commands::remove_skill_repo,
|
||||||
|
commands::install_skills_from_zip,
|
||||||
// Auto launch
|
// Auto launch
|
||||||
commands::set_auto_launch,
|
commands::set_auto_launch,
|
||||||
commands::get_auto_launch_status,
|
commands::get_auto_launch_status,
|
||||||
|
|||||||
@@ -1110,6 +1110,193 @@ impl SkillService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 从 ZIP 文件安装 ==========
|
||||||
|
|
||||||
|
/// 从本地 ZIP 文件安装 Skills
|
||||||
|
///
|
||||||
|
/// 流程:
|
||||||
|
/// 1. 解压 ZIP 到临时目录
|
||||||
|
/// 2. 扫描目录查找包含 SKILL.md 的技能
|
||||||
|
/// 3. 复制到 SSOT 并保存到数据库
|
||||||
|
/// 4. 同步到当前应用目录
|
||||||
|
pub fn install_from_zip(
|
||||||
|
db: &Arc<Database>,
|
||||||
|
zip_path: &Path,
|
||||||
|
current_app: &AppType,
|
||||||
|
) -> Result<Vec<InstalledSkill>> {
|
||||||
|
// 解压到临时目录
|
||||||
|
let temp_dir = Self::extract_local_zip(zip_path)?;
|
||||||
|
|
||||||
|
// 扫描所有包含 SKILL.md 的目录
|
||||||
|
let skill_dirs = Self::scan_skills_in_dir(&temp_dir)?;
|
||||||
|
|
||||||
|
if skill_dirs.is_empty() {
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
return Err(anyhow!(format_skill_error(
|
||||||
|
"NO_SKILLS_IN_ZIP",
|
||||||
|
&[],
|
||||||
|
Some("checkZipContent"),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ssot_dir = Self::get_ssot_dir()?;
|
||||||
|
let mut installed = Vec::new();
|
||||||
|
let existing_skills = db.get_all_installed_skills()?;
|
||||||
|
|
||||||
|
for skill_dir in skill_dirs {
|
||||||
|
// 获取目录名称作为安装名
|
||||||
|
let install_name = skill_dir
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
// 检查是否已有同名 directory 的 skill
|
||||||
|
let conflict = existing_skills
|
||||||
|
.values()
|
||||||
|
.find(|s| s.directory.eq_ignore_ascii_case(&install_name));
|
||||||
|
|
||||||
|
if let Some(existing) = conflict {
|
||||||
|
log::warn!(
|
||||||
|
"Skill directory '{}' already exists (from {}), skipping",
|
||||||
|
install_name,
|
||||||
|
existing.id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析元数据
|
||||||
|
let skill_md = skill_dir.join("SKILL.md");
|
||||||
|
let (name, description) = if skill_md.exists() {
|
||||||
|
match Self::parse_skill_metadata_static(&skill_md) {
|
||||||
|
Ok(meta) => (
|
||||||
|
meta.name.unwrap_or_else(|| install_name.clone()),
|
||||||
|
meta.description,
|
||||||
|
),
|
||||||
|
Err(_) => (install_name.clone(), None),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(install_name.clone(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制到 SSOT
|
||||||
|
let dest = ssot_dir.join(&install_name);
|
||||||
|
if dest.exists() {
|
||||||
|
let _ = fs::remove_dir_all(&dest);
|
||||||
|
}
|
||||||
|
Self::copy_dir_recursive(&skill_dir, &dest)?;
|
||||||
|
|
||||||
|
// 创建 InstalledSkill 记录
|
||||||
|
let skill = InstalledSkill {
|
||||||
|
id: format!("local:{install_name}"),
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
directory: install_name.clone(),
|
||||||
|
repo_owner: None,
|
||||||
|
repo_name: None,
|
||||||
|
repo_branch: None,
|
||||||
|
readme_url: None,
|
||||||
|
apps: SkillApps::only(current_app),
|
||||||
|
installed_at: chrono::Utc::now().timestamp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
db.save_skill(&skill)?;
|
||||||
|
|
||||||
|
// 同步到当前应用目录
|
||||||
|
Self::sync_to_app_dir(&install_name, current_app)?;
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Skill {} installed from ZIP, enabled for {:?}",
|
||||||
|
skill.name,
|
||||||
|
current_app
|
||||||
|
);
|
||||||
|
installed.push(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理临时目录
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
|
||||||
|
Ok(installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解压本地 ZIP 文件到临时目录
|
||||||
|
fn extract_local_zip(zip_path: &Path) -> Result<PathBuf> {
|
||||||
|
let file = fs::File::open(zip_path)
|
||||||
|
.with_context(|| format!("Failed to open ZIP file: {}", zip_path.display()))?;
|
||||||
|
|
||||||
|
let mut archive = zip::ZipArchive::new(file)
|
||||||
|
.with_context(|| format!("Failed to read ZIP file: {}", zip_path.display()))?;
|
||||||
|
|
||||||
|
if archive.is_empty() {
|
||||||
|
return Err(anyhow!(format_skill_error(
|
||||||
|
"EMPTY_ARCHIVE",
|
||||||
|
&[],
|
||||||
|
Some("checkZipContent"),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_dir = tempfile::tempdir()?;
|
||||||
|
let temp_path = temp_dir.path().to_path_buf();
|
||||||
|
let _ = temp_dir.keep(); // Keep the directory, we'll clean up later
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
let file_path = match file.enclosed_name() {
|
||||||
|
Some(path) => path.to_owned(),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outpath = temp_path.join(&file_path);
|
||||||
|
|
||||||
|
if file.is_dir() {
|
||||||
|
fs::create_dir_all(&outpath)?;
|
||||||
|
} else {
|
||||||
|
if let Some(parent) = outpath.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut outfile = fs::File::create(&outpath)?;
|
||||||
|
std::io::copy(&mut file, &mut outfile)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(temp_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 递归扫描目录查找包含 SKILL.md 的技能目录
|
||||||
|
fn scan_skills_in_dir(dir: &Path) -> Result<Vec<PathBuf>> {
|
||||||
|
let mut skill_dirs = Vec::new();
|
||||||
|
Self::scan_skills_recursive(dir, &mut skill_dirs)?;
|
||||||
|
Ok(skill_dirs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 递归扫描辅助函数
|
||||||
|
fn scan_skills_recursive(current: &Path, results: &mut Vec<PathBuf>) -> Result<()> {
|
||||||
|
// 检查当前目录是否包含 SKILL.md
|
||||||
|
let skill_md = current.join("SKILL.md");
|
||||||
|
if skill_md.exists() {
|
||||||
|
results.push(current.to_path_buf());
|
||||||
|
// 找到后不再递归子目录(一个 skill 目录)
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归子目录
|
||||||
|
if let Ok(entries) = fs::read_dir(current) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
// 跳过隐藏目录
|
||||||
|
let dir_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if dir_name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Self::scan_skills_recursive(&path, results)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 仓库管理(保留原有逻辑)==========
|
// ========== 仓库管理(保留原有逻辑)==========
|
||||||
|
|
||||||
/// 列出仓库
|
/// 列出仓库
|
||||||
|
|||||||
+12
@@ -15,6 +15,7 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Download,
|
Download,
|
||||||
BarChart2,
|
BarChart2,
|
||||||
|
FolderArchive,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { Provider, VisibleApps } from "@/types";
|
import type { Provider, VisibleApps } from "@/types";
|
||||||
import type { EnvConflict } from "@/types/env";
|
import type { EnvConflict } from "@/types/env";
|
||||||
@@ -829,6 +830,17 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
{currentView === "skills" && (
|
{currentView === "skills" && (
|
||||||
<>
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
unifiedSkillsPanelRef.current?.openInstallFromZip()
|
||||||
|
}
|
||||||
|
className="hover:bg-black/5 dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
<FolderArchive className="w-4 h-4 mr-2" />
|
||||||
|
{t("skills.installFromZip.button")}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import {
|
|||||||
useUninstallSkill,
|
useUninstallSkill,
|
||||||
useScanUnmanagedSkills,
|
useScanUnmanagedSkills,
|
||||||
useImportSkillsFromApps,
|
useImportSkillsFromApps,
|
||||||
|
useInstallSkillsFromZip,
|
||||||
type InstalledSkill,
|
type InstalledSkill,
|
||||||
type AppType,
|
type AppType,
|
||||||
} from "@/hooks/useSkills";
|
} from "@/hooks/useSkills";
|
||||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||||
import { settingsApi } from "@/lib/api";
|
import { settingsApi, skillsApi } from "@/lib/api";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface UnifiedSkillsPanelProps {
|
interface UnifiedSkillsPanelProps {
|
||||||
@@ -27,6 +28,7 @@ interface UnifiedSkillsPanelProps {
|
|||||||
export interface UnifiedSkillsPanelHandle {
|
export interface UnifiedSkillsPanelHandle {
|
||||||
openDiscovery: () => void;
|
openDiscovery: () => void;
|
||||||
openImport: () => void;
|
openImport: () => void;
|
||||||
|
openInstallFromZip: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UnifiedSkillsPanel = React.forwardRef<
|
const UnifiedSkillsPanel = React.forwardRef<
|
||||||
@@ -49,6 +51,7 @@ const UnifiedSkillsPanel = React.forwardRef<
|
|||||||
const { data: unmanagedSkills, refetch: scanUnmanaged } =
|
const { data: unmanagedSkills, refetch: scanUnmanaged } =
|
||||||
useScanUnmanagedSkills();
|
useScanUnmanagedSkills();
|
||||||
const importMutation = useImportSkillsFromApps();
|
const importMutation = useImportSkillsFromApps();
|
||||||
|
const installFromZipMutation = useInstallSkillsFromZip();
|
||||||
|
|
||||||
// Count enabled skills per app
|
// Count enabled skills per app
|
||||||
const enabledCounts = useMemo(() => {
|
const enabledCounts = useMemo(() => {
|
||||||
@@ -127,9 +130,52 @@ const UnifiedSkillsPanel = React.forwardRef<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInstallFromZip = async () => {
|
||||||
|
try {
|
||||||
|
// 打开文件选择对话框
|
||||||
|
const filePath = await skillsApi.openZipFileDialog();
|
||||||
|
if (!filePath) {
|
||||||
|
// 用户取消选择
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认使用 claude 作为当前应用
|
||||||
|
const currentApp: AppType = "claude";
|
||||||
|
|
||||||
|
// 安装 Skills
|
||||||
|
const installed = await installFromZipMutation.mutateAsync({
|
||||||
|
filePath,
|
||||||
|
currentApp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (installed.length === 0) {
|
||||||
|
toast.info(t("skills.installFromZip.noSkillsFound"), {
|
||||||
|
closeButton: true,
|
||||||
|
});
|
||||||
|
} else if (installed.length === 1) {
|
||||||
|
toast.success(
|
||||||
|
t("skills.installFromZip.successSingle", { name: installed[0].name }),
|
||||||
|
{ closeButton: true },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
t("skills.installFromZip.successMultiple", {
|
||||||
|
count: installed.length,
|
||||||
|
}),
|
||||||
|
{ closeButton: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t("skills.installFailed"), {
|
||||||
|
description: String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
openDiscovery: onOpenDiscovery,
|
openDiscovery: onOpenDiscovery,
|
||||||
openImport: handleOpenImport,
|
openImport: handleOpenImport,
|
||||||
|
openInstallFromZip: handleInstallFromZip,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -147,6 +147,26 @@ export function useRemoveSkillRepo() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 ZIP 文件安装 Skills
|
||||||
|
*/
|
||||||
|
export function useInstallSkillsFromZip() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({
|
||||||
|
filePath,
|
||||||
|
currentApp,
|
||||||
|
}: {
|
||||||
|
filePath: string;
|
||||||
|
currentApp: AppType;
|
||||||
|
}) => skillsApi.installFromZip(filePath, currentApp),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 辅助类型 ==========
|
// ========== 辅助类型 ==========
|
||||||
|
|
||||||
export type { InstalledSkill, DiscoverableSkill, AppType };
|
export type { InstalledSkill, DiscoverableSkill, AppType };
|
||||||
|
|||||||
@@ -1054,6 +1054,7 @@
|
|||||||
"http429": "Too many requests, please wait and retry",
|
"http429": "Too many requests, please wait and retry",
|
||||||
"parseMetadataFailed": "Failed to parse skill metadata",
|
"parseMetadataFailed": "Failed to parse skill metadata",
|
||||||
"getHomeDirFailed": "Unable to get user home directory",
|
"getHomeDirFailed": "Unable to get user home directory",
|
||||||
|
"noSkillsInZip": "No skills found in ZIP file (requires SKILL.md file)",
|
||||||
"networkError": "Network error",
|
"networkError": "Network error",
|
||||||
"fsError": "File system error",
|
"fsError": "File system error",
|
||||||
"unknownError": "Unknown error",
|
"unknownError": "Unknown error",
|
||||||
@@ -1064,7 +1065,8 @@
|
|||||||
"checkRepoUrl": "Please check repository URL and branch name",
|
"checkRepoUrl": "Please check repository URL and branch name",
|
||||||
"checkDiskSpace": "Please check disk space",
|
"checkDiskSpace": "Please check disk space",
|
||||||
"checkPermission": "Please check directory permissions",
|
"checkPermission": "Please check directory permissions",
|
||||||
"uninstallFirst": "Please uninstall the existing skill with the same name first"
|
"uninstallFirst": "Please uninstall the existing skill with the same name first",
|
||||||
|
"checkZipContent": "Please verify the ZIP file contains valid skill directories (with SKILL.md files)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
@@ -1113,6 +1115,13 @@
|
|||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"opencode": "OpenCode"
|
"opencode": "OpenCode"
|
||||||
|
},
|
||||||
|
"installFromZip": {
|
||||||
|
"button": "Install from ZIP",
|
||||||
|
"installing": "Installing...",
|
||||||
|
"successSingle": "Skill {{name}} installed",
|
||||||
|
"successMultiple": "Successfully installed {{count}} skills",
|
||||||
|
"noSkillsFound": "No skills found in ZIP file (requires SKILL.md file)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deeplink": {
|
"deeplink": {
|
||||||
|
|||||||
@@ -1054,6 +1054,7 @@
|
|||||||
"http429": "请求过于频繁,请等待后重试",
|
"http429": "请求过于频繁,请等待后重试",
|
||||||
"parseMetadataFailed": "解析技能元数据失败",
|
"parseMetadataFailed": "解析技能元数据失败",
|
||||||
"getHomeDirFailed": "无法获取用户主目录",
|
"getHomeDirFailed": "无法获取用户主目录",
|
||||||
|
"noSkillsInZip": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)",
|
||||||
"networkError": "网络错误",
|
"networkError": "网络错误",
|
||||||
"fsError": "文件系统错误",
|
"fsError": "文件系统错误",
|
||||||
"unknownError": "未知错误",
|
"unknownError": "未知错误",
|
||||||
@@ -1064,7 +1065,8 @@
|
|||||||
"checkRepoUrl": "请检查仓库地址和分支名称",
|
"checkRepoUrl": "请检查仓库地址和分支名称",
|
||||||
"checkDiskSpace": "请检查磁盘空间",
|
"checkDiskSpace": "请检查磁盘空间",
|
||||||
"checkPermission": "请检查目录权限",
|
"checkPermission": "请检查目录权限",
|
||||||
"uninstallFirst": "请先卸载已安装的同名技能"
|
"uninstallFirst": "请先卸载已安装的同名技能",
|
||||||
|
"checkZipContent": "请确认 ZIP 文件包含有效的技能目录(含 SKILL.md 文件)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"repo": {
|
"repo": {
|
||||||
@@ -1113,6 +1115,13 @@
|
|||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"gemini": "Gemini",
|
"gemini": "Gemini",
|
||||||
"opencode": "OpenCode"
|
"opencode": "OpenCode"
|
||||||
|
},
|
||||||
|
"installFromZip": {
|
||||||
|
"button": "从 ZIP 安装",
|
||||||
|
"installing": "安装中...",
|
||||||
|
"successSingle": "技能 {{name}} 已安装",
|
||||||
|
"successMultiple": "成功安装 {{count}} 个技能",
|
||||||
|
"noSkillsFound": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deeplink": {
|
"deeplink": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { providersApi, universalProvidersApi } from "./providers";
|
|||||||
export { settingsApi } from "./settings";
|
export { settingsApi } from "./settings";
|
||||||
export { mcpApi } from "./mcp";
|
export { mcpApi } from "./mcp";
|
||||||
export { promptsApi } from "./prompts";
|
export { promptsApi } from "./prompts";
|
||||||
|
export { skillsApi } from "./skills";
|
||||||
export { usageApi } from "./usage";
|
export { usageApi } from "./usage";
|
||||||
export { vscodeApi } from "./vscode";
|
export { vscodeApi } from "./vscode";
|
||||||
export { proxyApi } from "./proxy";
|
export { proxyApi } from "./proxy";
|
||||||
|
|||||||
@@ -159,4 +159,19 @@ export const skillsApi = {
|
|||||||
async removeRepo(owner: string, name: string): Promise<boolean> {
|
async removeRepo(owner: string, name: string): Promise<boolean> {
|
||||||
return await invoke("remove_skill_repo", { owner, name });
|
return await invoke("remove_skill_repo", { owner, name });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ========== ZIP 安装 ==========
|
||||||
|
|
||||||
|
/** 打开 ZIP 文件选择对话框 */
|
||||||
|
async openZipFileDialog(): Promise<string | null> {
|
||||||
|
return await invoke("open_zip_file_dialog");
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 从 ZIP 文件安装 Skills */
|
||||||
|
async installFromZip(
|
||||||
|
filePath: string,
|
||||||
|
currentApp: AppType,
|
||||||
|
): Promise<InstalledSkill[]> {
|
||||||
|
return await invoke("install_skills_from_zip", { filePath, currentApp });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ function getErrorI18nKey(code: string): string {
|
|||||||
SKILL_DIRECTORY_CONFLICT: "skills.error.directoryConflict",
|
SKILL_DIRECTORY_CONFLICT: "skills.error.directoryConflict",
|
||||||
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
||||||
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
||||||
|
NO_SKILLS_IN_ZIP: "skills.error.noSkillsInZip",
|
||||||
};
|
};
|
||||||
|
|
||||||
return mapping[code] || "skills.error.unknownError";
|
return mapping[code] || "skills.error.unknownError";
|
||||||
@@ -54,6 +55,7 @@ function getSuggestionI18nKey(suggestion: string): string {
|
|||||||
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
||||||
checkPermission: "skills.error.suggestion.checkPermission",
|
checkPermission: "skills.error.suggestion.checkPermission",
|
||||||
uninstallFirst: "skills.error.suggestion.uninstallFirst",
|
uninstallFirst: "skills.error.suggestion.uninstallFirst",
|
||||||
|
checkZipContent: "skills.error.suggestion.checkZipContent",
|
||||||
http403: "skills.error.http403",
|
http403: "skills.error.http403",
|
||||||
http404: "skills.error.http404",
|
http404: "skills.error.http404",
|
||||||
http429: "skills.error.http429",
|
http429: "skills.error.http429",
|
||||||
|
|||||||
Reference in New Issue
Block a user