mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +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:
+12
@@ -15,6 +15,7 @@ import {
|
||||
Search,
|
||||
Download,
|
||||
BarChart2,
|
||||
FolderArchive,
|
||||
} from "lucide-react";
|
||||
import type { Provider, VisibleApps } from "@/types";
|
||||
import type { EnvConflict } from "@/types/env";
|
||||
@@ -829,6 +830,17 @@ function App() {
|
||||
)}
|
||||
{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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
useUninstallSkill,
|
||||
useScanUnmanagedSkills,
|
||||
useImportSkillsFromApps,
|
||||
useInstallSkillsFromZip,
|
||||
type InstalledSkill,
|
||||
type AppType,
|
||||
} from "@/hooks/useSkills";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
import { settingsApi, skillsApi } from "@/lib/api";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UnifiedSkillsPanelProps {
|
||||
@@ -27,6 +28,7 @@ interface UnifiedSkillsPanelProps {
|
||||
export interface UnifiedSkillsPanelHandle {
|
||||
openDiscovery: () => void;
|
||||
openImport: () => void;
|
||||
openInstallFromZip: () => void;
|
||||
}
|
||||
|
||||
const UnifiedSkillsPanel = React.forwardRef<
|
||||
@@ -49,6 +51,7 @@ const UnifiedSkillsPanel = React.forwardRef<
|
||||
const { data: unmanagedSkills, refetch: scanUnmanaged } =
|
||||
useScanUnmanagedSkills();
|
||||
const importMutation = useImportSkillsFromApps();
|
||||
const installFromZipMutation = useInstallSkillsFromZip();
|
||||
|
||||
// Count enabled skills per app
|
||||
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, () => ({
|
||||
openDiscovery: onOpenDiscovery,
|
||||
openImport: handleOpenImport,
|
||||
openInstallFromZip: handleInstallFromZip,
|
||||
}));
|
||||
|
||||
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 };
|
||||
|
||||
@@ -1054,6 +1054,7 @@
|
||||
"http429": "Too many requests, please wait and retry",
|
||||
"parseMetadataFailed": "Failed to parse skill metadata",
|
||||
"getHomeDirFailed": "Unable to get user home directory",
|
||||
"noSkillsInZip": "No skills found in ZIP file (requires SKILL.md file)",
|
||||
"networkError": "Network error",
|
||||
"fsError": "File system error",
|
||||
"unknownError": "Unknown error",
|
||||
@@ -1064,7 +1065,8 @@
|
||||
"checkRepoUrl": "Please check repository URL and branch name",
|
||||
"checkDiskSpace": "Please check disk space",
|
||||
"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": {
|
||||
@@ -1113,6 +1115,13 @@
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini",
|
||||
"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": {
|
||||
|
||||
@@ -1054,6 +1054,7 @@
|
||||
"http429": "请求过于频繁,请等待后重试",
|
||||
"parseMetadataFailed": "解析技能元数据失败",
|
||||
"getHomeDirFailed": "无法获取用户主目录",
|
||||
"noSkillsInZip": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)",
|
||||
"networkError": "网络错误",
|
||||
"fsError": "文件系统错误",
|
||||
"unknownError": "未知错误",
|
||||
@@ -1064,7 +1065,8 @@
|
||||
"checkRepoUrl": "请检查仓库地址和分支名称",
|
||||
"checkDiskSpace": "请检查磁盘空间",
|
||||
"checkPermission": "请检查目录权限",
|
||||
"uninstallFirst": "请先卸载已安装的同名技能"
|
||||
"uninstallFirst": "请先卸载已安装的同名技能",
|
||||
"checkZipContent": "请确认 ZIP 文件包含有效的技能目录(含 SKILL.md 文件)"
|
||||
}
|
||||
},
|
||||
"repo": {
|
||||
@@ -1113,6 +1115,13 @@
|
||||
"codex": "Codex",
|
||||
"gemini": "Gemini",
|
||||
"opencode": "OpenCode"
|
||||
},
|
||||
"installFromZip": {
|
||||
"button": "从 ZIP 安装",
|
||||
"installing": "安装中...",
|
||||
"successSingle": "技能 {{name}} 已安装",
|
||||
"successMultiple": "成功安装 {{count}} 个技能",
|
||||
"noSkillsFound": "ZIP 文件中未找到技能(需包含 SKILL.md 文件)"
|
||||
}
|
||||
},
|
||||
"deeplink": {
|
||||
|
||||
@@ -3,6 +3,7 @@ export { providersApi, universalProvidersApi } from "./providers";
|
||||
export { settingsApi } from "./settings";
|
||||
export { mcpApi } from "./mcp";
|
||||
export { promptsApi } from "./prompts";
|
||||
export { skillsApi } from "./skills";
|
||||
export { usageApi } from "./usage";
|
||||
export { vscodeApi } from "./vscode";
|
||||
export { proxyApi } from "./proxy";
|
||||
|
||||
@@ -159,4 +159,19 @@ export const skillsApi = {
|
||||
async removeRepo(owner: string, name: string): Promise<boolean> {
|
||||
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",
|
||||
EMPTY_ARCHIVE: "skills.error.emptyArchive",
|
||||
GET_HOME_DIR_FAILED: "skills.error.getHomeDirFailed",
|
||||
NO_SKILLS_IN_ZIP: "skills.error.noSkillsInZip",
|
||||
};
|
||||
|
||||
return mapping[code] || "skills.error.unknownError";
|
||||
@@ -54,6 +55,7 @@ function getSuggestionI18nKey(suggestion: string): string {
|
||||
checkRepoUrl: "skills.error.suggestion.checkRepoUrl",
|
||||
checkPermission: "skills.error.suggestion.checkPermission",
|
||||
uninstallFirst: "skills.error.suggestion.uninstallFirst",
|
||||
checkZipContent: "skills.error.suggestion.checkZipContent",
|
||||
http403: "skills.error.http403",
|
||||
http404: "skills.error.http404",
|
||||
http429: "skills.error.http429",
|
||||
|
||||
Reference in New Issue
Block a user