feat(skill): add database migration and Gemini support for multi-app skills

- Refactor skills table from single key to (directory, app_type) composite primary key
- Add migration logic to convert existing skill records
- Support skill installation/uninstallation for Claude/Codex/Gemini independently
- Add new Tauri commands: get_skills_for_app, install_skill_for_app, uninstall_skill_for_app
- Update frontend API and components to support app-specific skill operations
This commit is contained in:
YoVinchen
2025-12-09 16:50:08 +08:00
parent 56b40bdad2
commit e6514ec759
7 changed files with 248 additions and 40 deletions
+14 -5
View File
@@ -19,11 +19,17 @@ import { RefreshCw, Search } from "lucide-react";
import { toast } from "sonner";
import { SkillCard } from "./SkillCard";
import { RepoManagerPanel } from "./RepoManagerPanel";
import { skillsApi, type Skill, type SkillRepo } from "@/lib/api/skills";
import {
skillsApi,
type Skill,
type SkillRepo,
type AppType,
} from "@/lib/api/skills";
import { formatSkillError } from "@/lib/errors/skillErrorParser";
interface SkillsPageProps {
onClose?: () => void;
initialApp?: AppType;
}
export interface SkillsPageHandle {
@@ -32,7 +38,7 @@ export interface SkillsPageHandle {
}
export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
({ onClose: _onClose }, ref) => {
({ onClose: _onClose, initialApp = "claude" }, ref) => {
const { t } = useTranslation();
const [skills, setSkills] = useState<Skill[]>([]);
const [repos, setRepos] = useState<SkillRepo[]>([]);
@@ -42,11 +48,13 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const [filterStatus, setFilterStatus] = useState<
"all" | "installed" | "uninstalled"
>("all");
// 使用 initialApp,不允许切换
const selectedApp = initialApp;
const loadSkills = async (afterLoad?: (data: Skill[]) => void) => {
try {
setLoading(true);
const data = await skillsApi.getAll();
const data = await skillsApi.getAll(selectedApp);
setSkills(data);
if (afterLoad) {
afterLoad(data);
@@ -84,6 +92,7 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
useEffect(() => {
Promise.all([loadSkills(), loadRepos()]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useImperativeHandle(ref, () => ({
@@ -93,7 +102,7 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const handleInstall = async (directory: string) => {
try {
await skillsApi.install(directory);
await skillsApi.install(directory, selectedApp);
toast.success(t("skills.installSuccess", { name: directory }));
await loadSkills();
} catch (error) {
@@ -122,7 +131,7 @@ export const SkillsPage = forwardRef<SkillsPageHandle, SkillsPageProps>(
const handleUninstall = async (directory: string) => {
try {
await skillsApi.uninstall(directory);
await skillsApi.uninstall(directory, selectedApp);
toast.success(t("skills.uninstallSuccess", { name: directory }));
await loadSkills();
} catch (error) {
+20 -6
View File
@@ -19,17 +19,31 @@ export interface SkillRepo {
enabled: boolean;
}
export type AppType = "claude" | "codex" | "gemini";
export const skillsApi = {
async getAll(): Promise<Skill[]> {
return await invoke("get_skills");
async getAll(app: AppType = "claude"): Promise<Skill[]> {
if (app === "claude") {
return await invoke("get_skills");
}
return await invoke("get_skills_for_app", { app });
},
async install(directory: string): Promise<boolean> {
return await invoke("install_skill", { directory });
async install(directory: string, app: AppType = "claude"): Promise<boolean> {
if (app === "claude") {
return await invoke("install_skill", { directory });
}
return await invoke("install_skill_for_app", { app, directory });
},
async uninstall(directory: string): Promise<boolean> {
return await invoke("uninstall_skill", { directory });
async uninstall(
directory: string,
app: AppType = "claude",
): Promise<boolean> {
if (app === "claude") {
return await invoke("uninstall_skill", { directory });
}
return await invoke("uninstall_skill_for_app", { app, directory });
},
async getRepos(): Promise<SkillRepo[]> {