mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
fix: replace implicit app inference with explicit selection for Skills import and sync
Skills import previously inferred app enablement from filesystem presence, causing incorrect multi-app activation when the same skill directory existed under multiple app paths. Now the frontend submits explicit app selections via ImportSkillSelection, and schema migration preserves a snapshot of legacy app mappings to avoid lossy reconstruction. Also adds reconciliation to sync_to_app (removes disabled/orphaned symlinks) and MCP sync_all_enabled (removes disabled servers from live config).
This commit is contained in:
+2
-5
@@ -712,17 +712,14 @@ function App() {
|
||||
<UnifiedSkillsPanel
|
||||
ref={unifiedSkillsPanelRef}
|
||||
onOpenDiscovery={() => setCurrentView("skillsDiscovery")}
|
||||
currentApp={activeApp === "openclaw" ? "claude" : activeApp}
|
||||
/>
|
||||
);
|
||||
case "skillsDiscovery":
|
||||
return (
|
||||
<SkillsPage
|
||||
ref={skillsPageRef}
|
||||
initialApp={
|
||||
activeApp === "opencode" || activeApp === "openclaw"
|
||||
? "claude"
|
||||
: activeApp
|
||||
}
|
||||
initialApp={activeApp === "openclaw" ? "claude" : activeApp}
|
||||
/>
|
||||
);
|
||||
case "mcp":
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Sparkles, Trash2, ExternalLink } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import {
|
||||
type ImportSkillSelection,
|
||||
useInstalledSkills,
|
||||
useToggleSkillApp,
|
||||
useUninstallSkill,
|
||||
@@ -23,6 +24,7 @@ import { ListItemRow } from "@/components/common/ListItemRow";
|
||||
|
||||
interface UnifiedSkillsPanelProps {
|
||||
onOpenDiscovery: () => void;
|
||||
currentApp: AppId;
|
||||
}
|
||||
|
||||
export interface UnifiedSkillsPanelHandle {
|
||||
@@ -34,7 +36,7 @@ export interface UnifiedSkillsPanelHandle {
|
||||
const UnifiedSkillsPanel = React.forwardRef<
|
||||
UnifiedSkillsPanelHandle,
|
||||
UnifiedSkillsPanelProps
|
||||
>(({ onOpenDiscovery }, ref) => {
|
||||
>(({ onOpenDiscovery, currentApp }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [confirmDialog, setConfirmDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
@@ -103,9 +105,9 @@ const UnifiedSkillsPanel = React.forwardRef<
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async (directories: string[]) => {
|
||||
const handleImport = async (imports: ImportSkillSelection[]) => {
|
||||
try {
|
||||
const imported = await importMutation.mutateAsync(directories);
|
||||
const imported = await importMutation.mutateAsync(imports);
|
||||
setImportDialogOpen(false);
|
||||
toast.success(t("skills.importSuccess", { count: imported.length }), {
|
||||
closeButton: true,
|
||||
@@ -120,7 +122,6 @@ const UnifiedSkillsPanel = React.forwardRef<
|
||||
const filePath = await skillsApi.openZipFileDialog();
|
||||
if (!filePath) return;
|
||||
|
||||
const currentApp: AppId = "claude";
|
||||
const installed = await installFromZipMutation.mutateAsync({
|
||||
filePath,
|
||||
currentApp,
|
||||
@@ -310,7 +311,7 @@ interface ImportSkillsDialogProps {
|
||||
foundIn: string[];
|
||||
path: string;
|
||||
}>;
|
||||
onImport: (directories: string[]) => void;
|
||||
onImport: (imports: ImportSkillSelection[]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -323,6 +324,22 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
const [selected, setSelected] = useState<Set<string>>(
|
||||
new Set(skills.map((s) => s.directory)),
|
||||
);
|
||||
const [selectedApps, setSelectedApps] = useState<
|
||||
Record<string, ImportSkillSelection["apps"]>
|
||||
>(() =>
|
||||
Object.fromEntries(
|
||||
skills.map((skill) => [
|
||||
skill.directory,
|
||||
{
|
||||
claude: skill.foundIn.includes("claude"),
|
||||
codex: skill.foundIn.includes("codex"),
|
||||
gemini: skill.foundIn.includes("gemini"),
|
||||
opencode: skill.foundIn.includes("opencode"),
|
||||
openclaw: false,
|
||||
},
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
const toggleSelect = (directory: string) => {
|
||||
const newSelected = new Set(selected);
|
||||
@@ -335,7 +352,18 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
onImport(Array.from(selected));
|
||||
onImport(
|
||||
Array.from(selected).map((directory) => ({
|
||||
directory,
|
||||
apps: selectedApps[directory] ?? {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
},
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -348,9 +376,9 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-2 mb-4">
|
||||
{skills.map((skill) => (
|
||||
<label
|
||||
<div
|
||||
key={skill.directory}
|
||||
className="flex items-start gap-3 p-3 rounded-lg border hover:bg-muted cursor-pointer"
|
||||
className="flex items-start gap-3 p-3 rounded-lg border hover:bg-muted"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -365,6 +393,35 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
{skill.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<AppToggleGroup
|
||||
apps={
|
||||
selectedApps[skill.directory] ?? {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
}
|
||||
}
|
||||
onToggle={(app, enabled) => {
|
||||
setSelectedApps((prev) => ({
|
||||
...prev,
|
||||
[skill.directory]: {
|
||||
...(prev[skill.directory] ?? {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
}),
|
||||
[app]: enabled,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
appIds={MCP_SKILLS_APP_IDS}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="text-xs text-muted-foreground/50 mt-1 truncate"
|
||||
title={skill.path}
|
||||
@@ -372,7 +429,7 @@ const ImportSkillsDialog: React.FC<ImportSkillsDialogProps> = ({
|
||||
{skill.path}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
skillsApi,
|
||||
type DiscoverableSkill,
|
||||
type ImportSkillSelection,
|
||||
type InstalledSkill,
|
||||
} from "@/lib/api/skills";
|
||||
import type { AppId } from "@/lib/api/types";
|
||||
@@ -99,8 +100,8 @@ export function useScanUnmanagedSkills() {
|
||||
export function useImportSkillsFromApps() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (directories: string[]) =>
|
||||
skillsApi.importFromApps(directories),
|
||||
mutationFn: (imports: ImportSkillSelection[]) =>
|
||||
skillsApi.importFromApps(imports),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["skills", "installed"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["skills", "unmanaged"] });
|
||||
@@ -169,4 +170,4 @@ export function useInstallSkillsFromZip() {
|
||||
|
||||
// ========== 辅助类型 ==========
|
||||
|
||||
export type { InstalledSkill, DiscoverableSkill, AppId };
|
||||
export type { InstalledSkill, DiscoverableSkill, ImportSkillSelection, AppId };
|
||||
|
||||
+10
-2
@@ -48,6 +48,12 @@ export interface UnmanagedSkill {
|
||||
path: string;
|
||||
}
|
||||
|
||||
/** 导入已有 Skill 时提交的应用启用状态 */
|
||||
export interface ImportSkillSelection {
|
||||
directory: string;
|
||||
apps: SkillApps;
|
||||
}
|
||||
|
||||
/** 技能对象(兼容旧 API) */
|
||||
export interface Skill {
|
||||
key: string;
|
||||
@@ -103,8 +109,10 @@ export const skillsApi = {
|
||||
},
|
||||
|
||||
/** 从应用目录导入 Skills */
|
||||
async importFromApps(directories: string[]): Promise<InstalledSkill[]> {
|
||||
return await invoke("import_skills_from_apps", { directories });
|
||||
async importFromApps(
|
||||
imports: ImportSkillSelection[],
|
||||
): Promise<InstalledSkill[]> {
|
||||
return await invoke("import_skills_from_apps", { imports });
|
||||
},
|
||||
|
||||
/** 发现可安装的 Skills(从仓库获取) */
|
||||
|
||||
Reference in New Issue
Block a user