mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-02 18:41:35 +08:00
feat: add skill storage location toggle between CC Switch and ~/.agents/skills
Allow users to choose between storing skills in CC Switch's managed directory (~/.cc-switch/skills/) or the Agent Skills open standard directory (~/.agents/skills/). Includes migration logic that safely moves files before updating settings, with confirmation dialog for non-empty installations.
This commit is contained in:
@@ -7,8 +7,8 @@
|
|||||||
use crate::app_config::{AppType, InstalledSkill, UnmanagedSkill};
|
use crate::app_config::{AppType, InstalledSkill, UnmanagedSkill};
|
||||||
use crate::error::format_skill_error;
|
use crate::error::format_skill_error;
|
||||||
use crate::services::skill::{
|
use crate::services::skill::{
|
||||||
DiscoverableSkill, ImportSkillSelection, Skill, SkillBackupEntry, SkillRepo, SkillService,
|
DiscoverableSkill, ImportSkillSelection, MigrationResult, Skill, SkillBackupEntry, SkillRepo,
|
||||||
SkillUninstallResult, SkillUpdateInfo,
|
SkillService, SkillStorageLocation, SkillUninstallResult, SkillUpdateInfo,
|
||||||
};
|
};
|
||||||
use crate::store::AppState;
|
use crate::store::AppState;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -161,6 +161,15 @@ pub async fn update_skill(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 迁移 Skill 存储位置
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn migrate_skill_storage(
|
||||||
|
target: SkillStorageLocation,
|
||||||
|
app_state: State<'_, AppState>,
|
||||||
|
) -> Result<MigrationResult, String> {
|
||||||
|
SkillService::migrate_storage(&app_state.db, target).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
// ========== 兼容旧 API 的命令 ==========
|
// ========== 兼容旧 API 的命令 ==========
|
||||||
|
|
||||||
/// 获取技能列表(兼容旧 API)
|
/// 获取技能列表(兼容旧 API)
|
||||||
|
|||||||
@@ -965,6 +965,7 @@ pub fn run() {
|
|||||||
commands::discover_available_skills,
|
commands::discover_available_skills,
|
||||||
commands::check_skill_updates,
|
commands::check_skill_updates,
|
||||||
commands::update_skill,
|
commands::update_skill,
|
||||||
|
commands::migrate_skill_storage,
|
||||||
// Skill management (legacy API compatibility)
|
// Skill management (legacy API compatibility)
|
||||||
commands::get_skills,
|
commands::get_skills,
|
||||||
commands::get_skills_for_app,
|
commands::get_skills_for_app,
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ pub enum SyncMethod {
|
|||||||
Copy,
|
Copy,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Skill 存储位置(SSOT 目录选择)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SkillStorageLocation {
|
||||||
|
/// CC Switch 管理目录 (~/.cc-switch/skills/)
|
||||||
|
#[default]
|
||||||
|
CcSwitch,
|
||||||
|
/// Agent Skills 统一标准目录 (~/.agents/skills/)
|
||||||
|
Unified,
|
||||||
|
}
|
||||||
|
|
||||||
/// 可发现的技能(来自仓库)
|
/// 可发现的技能(来自仓库)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DiscoverableSkill {
|
pub struct DiscoverableSkill {
|
||||||
@@ -174,6 +185,15 @@ pub struct SkillUpdateInfo {
|
|||||||
pub remote_hash: String,
|
pub remote_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Skill 存储位置迁移结果
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MigrationResult {
|
||||||
|
pub migrated_count: usize,
|
||||||
|
pub skipped_count: usize,
|
||||||
|
pub errors: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SkillBackupEntry {
|
pub struct SkillBackupEntry {
|
||||||
@@ -403,9 +423,20 @@ impl SkillService {
|
|||||||
|
|
||||||
// ========== 路径管理 ==========
|
// ========== 路径管理 ==========
|
||||||
|
|
||||||
/// 获取 SSOT 目录(~/.cc-switch/skills/)
|
/// 获取 SSOT 目录(根据设置返回 ~/.cc-switch/skills/ 或 ~/.agents/skills/)
|
||||||
pub fn get_ssot_dir() -> Result<PathBuf> {
|
pub fn get_ssot_dir() -> Result<PathBuf> {
|
||||||
let dir = get_app_config_dir().join("skills");
|
let location = crate::settings::get_skill_storage_location();
|
||||||
|
let dir = match location {
|
||||||
|
SkillStorageLocation::CcSwitch => get_app_config_dir().join("skills"),
|
||||||
|
SkillStorageLocation::Unified => {
|
||||||
|
let home = dirs::home_dir().context(format_skill_error(
|
||||||
|
"GET_HOME_DIR_FAILED",
|
||||||
|
&[],
|
||||||
|
Some("checkPermission"),
|
||||||
|
))?;
|
||||||
|
home.join(".agents").join("skills")
|
||||||
|
}
|
||||||
|
};
|
||||||
fs::create_dir_all(&dir)?;
|
fs::create_dir_all(&dir)?;
|
||||||
Ok(dir)
|
Ok(dir)
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1083,87 @@ impl SkillService {
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 迁移 Skill 存储位置(在两个 SSOT 目录间移动文件)
|
||||||
|
///
|
||||||
|
/// 安全策略:先移文件,后改设置。中途崩溃时设置仍指向旧目录。
|
||||||
|
pub fn migrate_storage(
|
||||||
|
db: &Arc<Database>,
|
||||||
|
target: SkillStorageLocation,
|
||||||
|
) -> Result<MigrationResult> {
|
||||||
|
let current = crate::settings::get_skill_storage_location();
|
||||||
|
if current == target {
|
||||||
|
return Ok(MigrationResult {
|
||||||
|
migrated_count: 0,
|
||||||
|
skipped_count: 0,
|
||||||
|
errors: vec![],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 解析旧目录和新目录(不改设置)
|
||||||
|
let old_dir = Self::get_ssot_dir()?;
|
||||||
|
let new_dir = match target {
|
||||||
|
SkillStorageLocation::CcSwitch => get_app_config_dir().join("skills"),
|
||||||
|
SkillStorageLocation::Unified => {
|
||||||
|
let home = dirs::home_dir().context("Cannot determine home directory")?;
|
||||||
|
home.join(".agents").join("skills")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fs::create_dir_all(&new_dir)?;
|
||||||
|
|
||||||
|
// 2. 逐个移动 skill 目录
|
||||||
|
let skills = db.get_all_installed_skills()?;
|
||||||
|
let mut result = MigrationResult {
|
||||||
|
migrated_count: 0,
|
||||||
|
skipped_count: 0,
|
||||||
|
errors: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
for skill in skills.values() {
|
||||||
|
let src = old_dir.join(&skill.directory);
|
||||||
|
let dst = new_dir.join(&skill.directory);
|
||||||
|
|
||||||
|
if !src.exists() {
|
||||||
|
result.skipped_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if dst.exists() {
|
||||||
|
result.skipped_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先 rename(同文件系统原子操作),失败则 copy+delete
|
||||||
|
match fs::rename(&src, &dst) {
|
||||||
|
Ok(()) => result.migrated_count += 1,
|
||||||
|
Err(_) => match Self::copy_dir_recursive(&src, &dst) {
|
||||||
|
Ok(()) => {
|
||||||
|
let _ = fs::remove_dir_all(&src);
|
||||||
|
result.migrated_count += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
result.errors.push(format!("{}: {e}", skill.directory));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 文件移动完成后才持久化设置
|
||||||
|
crate::settings::set_skill_storage_location(target)?;
|
||||||
|
|
||||||
|
// 4. 刷新所有应用目录的 symlink(指向新 SSOT)
|
||||||
|
for app in AppType::all() {
|
||||||
|
let _ = Self::sync_to_app(db, &app);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"Skill 存储迁移完成: {} 迁移, {} 跳过, {} 错误",
|
||||||
|
result.migrated_count,
|
||||||
|
result.skipped_count,
|
||||||
|
result.errors.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_backups() -> Result<Vec<SkillBackupEntry>> {
|
pub fn list_backups() -> Result<Vec<SkillBackupEntry>> {
|
||||||
let backup_dir = Self::get_backup_dir()?;
|
let backup_dir = Self::get_backup_dir()?;
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use std::sync::{OnceLock, RwLock};
|
|||||||
|
|
||||||
use crate::app_config::AppType;
|
use crate::app_config::AppType;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::services::skill::SyncMethod;
|
use crate::services::skill::{SkillStorageLocation, SyncMethod};
|
||||||
|
|
||||||
/// 自定义端点配置(历史兼容,实际存储在 provider.meta.custom_endpoints)
|
/// 自定义端点配置(历史兼容,实际存储在 provider.meta.custom_endpoints)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -245,6 +245,9 @@ pub struct AppSettings {
|
|||||||
/// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
/// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub skill_sync_method: SyncMethod,
|
pub skill_sync_method: SyncMethod,
|
||||||
|
/// Skill 存储位置:cc_switch(默认)或 unified(~/.agents/skills/)
|
||||||
|
#[serde(default)]
|
||||||
|
pub skill_storage_location: SkillStorageLocation,
|
||||||
|
|
||||||
// ===== WebDAV 同步设置 =====
|
// ===== WebDAV 同步设置 =====
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -307,6 +310,7 @@ impl Default for AppSettings {
|
|||||||
current_provider_opencode: None,
|
current_provider_opencode: None,
|
||||||
current_provider_openclaw: None,
|
current_provider_openclaw: None,
|
||||||
skill_sync_method: SyncMethod::default(),
|
skill_sync_method: SyncMethod::default(),
|
||||||
|
skill_storage_location: SkillStorageLocation::default(),
|
||||||
webdav_sync: None,
|
webdav_sync: None,
|
||||||
webdav_backup: None,
|
webdav_backup: None,
|
||||||
backup_interval_hours: None,
|
backup_interval_hours: None,
|
||||||
@@ -642,6 +646,26 @@ pub fn get_skill_sync_method() -> SyncMethod {
|
|||||||
.skill_sync_method
|
.skill_sync_method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Skill 存储位置管理函数 =====
|
||||||
|
|
||||||
|
/// 获取 Skill 存储位置配置
|
||||||
|
pub fn get_skill_storage_location() -> SkillStorageLocation {
|
||||||
|
settings_store()
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log::warn!("设置锁已毒化,使用恢复值: {e}");
|
||||||
|
e.into_inner()
|
||||||
|
})
|
||||||
|
.skill_storage_location
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 Skill 存储位置
|
||||||
|
pub fn set_skill_storage_location(location: SkillStorageLocation) -> Result<(), AppError> {
|
||||||
|
mutate_settings(|s| {
|
||||||
|
s.skill_storage_location = location;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 备份策略管理函数 =====
|
// ===== 备份策略管理函数 =====
|
||||||
|
|
||||||
/// Get the effective auto-backup interval in hours (default 24)
|
/// Get the effective auto-backup interval in hours (default 24)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { LanguageSettings } from "@/components/settings/LanguageSettings";
|
|||||||
import { ThemeSettings } from "@/components/settings/ThemeSettings";
|
import { ThemeSettings } from "@/components/settings/ThemeSettings";
|
||||||
import { WindowSettings } from "@/components/settings/WindowSettings";
|
import { WindowSettings } from "@/components/settings/WindowSettings";
|
||||||
import { AppVisibilitySettings } from "@/components/settings/AppVisibilitySettings";
|
import { AppVisibilitySettings } from "@/components/settings/AppVisibilitySettings";
|
||||||
|
import { SkillStorageLocationSettings } from "@/components/settings/SkillStorageLocationSettings";
|
||||||
import { SkillSyncMethodSettings } from "@/components/settings/SkillSyncMethodSettings";
|
import { SkillSyncMethodSettings } from "@/components/settings/SkillSyncMethodSettings";
|
||||||
import { TerminalSettings } from "@/components/settings/TerminalSettings";
|
import { TerminalSettings } from "@/components/settings/TerminalSettings";
|
||||||
import { DirectorySettings } from "@/components/settings/DirectorySettings";
|
import { DirectorySettings } from "@/components/settings/DirectorySettings";
|
||||||
@@ -44,6 +45,7 @@ import { ModelTestConfigPanel } from "@/components/usage/ModelTestConfigPanel";
|
|||||||
import { UsageDashboard } from "@/components/usage/UsageDashboard";
|
import { UsageDashboard } from "@/components/usage/UsageDashboard";
|
||||||
import { LogConfigPanel } from "@/components/settings/LogConfigPanel";
|
import { LogConfigPanel } from "@/components/settings/LogConfigPanel";
|
||||||
import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel";
|
import { AuthCenterPanel } from "@/components/settings/AuthCenterPanel";
|
||||||
|
import { useInstalledSkills } from "@/hooks/useSkills";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import { useImportExport } from "@/hooks/useImportExport";
|
import { useImportExport } from "@/hooks/useImportExport";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -96,6 +98,8 @@ export function SettingsPage({
|
|||||||
resetStatus,
|
resetStatus,
|
||||||
} = useImportExport({ onImportSuccess });
|
} = useImportExport({ onImportSuccess });
|
||||||
|
|
||||||
|
const { data: installedSkills } = useInstalledSkills();
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<string>("general");
|
const [activeTab, setActiveTab] = useState<string>("general");
|
||||||
const [showRestartPrompt, setShowRestartPrompt] = useState(false);
|
const [showRestartPrompt, setShowRestartPrompt] = useState(false);
|
||||||
|
|
||||||
@@ -229,6 +233,13 @@ export function SettingsPage({
|
|||||||
settings={settings}
|
settings={settings}
|
||||||
onChange={handleAutoSave}
|
onChange={handleAutoSave}
|
||||||
/>
|
/>
|
||||||
|
<SkillStorageLocationSettings
|
||||||
|
value={settings.skillStorageLocation ?? "cc_switch"}
|
||||||
|
installedCount={installedSkills?.length ?? 0}
|
||||||
|
onMigrated={(location) =>
|
||||||
|
updateSettings({ skillStorageLocation: location })
|
||||||
|
}
|
||||||
|
/>
|
||||||
<SkillSyncMethodSettings
|
<SkillSyncMethodSettings
|
||||||
value={settings.skillSyncMethod ?? "auto"}
|
value={settings.skillSyncMethod ?? "auto"}
|
||||||
onChange={(method) =>
|
onChange={(method) =>
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { skillsApi, type MigrationResult } from "@/lib/api/skills";
|
||||||
|
import type { SkillStorageLocation } from "@/types";
|
||||||
|
|
||||||
|
export interface SkillStorageLocationSettingsProps {
|
||||||
|
value: SkillStorageLocation;
|
||||||
|
installedCount: number;
|
||||||
|
onMigrated: (target: SkillStorageLocation) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkillStorageLocationSettings({
|
||||||
|
value,
|
||||||
|
installedCount,
|
||||||
|
onMigrated,
|
||||||
|
}: SkillStorageLocationSettingsProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [pendingTarget, setPendingTarget] =
|
||||||
|
useState<SkillStorageLocation | null>(null);
|
||||||
|
const [isMigrating, setIsMigrating] = useState(false);
|
||||||
|
|
||||||
|
const handleSelect = (target: SkillStorageLocation) => {
|
||||||
|
if (target === value) return;
|
||||||
|
if (installedCount > 0) {
|
||||||
|
setPendingTarget(target);
|
||||||
|
} else {
|
||||||
|
doMigrate(target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doMigrate = async (target: SkillStorageLocation) => {
|
||||||
|
setIsMigrating(true);
|
||||||
|
setPendingTarget(null);
|
||||||
|
try {
|
||||||
|
const result: MigrationResult = await skillsApi.migrateStorage(target);
|
||||||
|
if (result.errors.length > 0) {
|
||||||
|
toast.warning(
|
||||||
|
t("settings.skillStorage.migrationPartial", {
|
||||||
|
migrated: result.migratedCount,
|
||||||
|
errors: result.errors.length,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
t("settings.skillStorage.migrationSuccess", {
|
||||||
|
count: result.migratedCount,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
onMigrated(target);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(String(error));
|
||||||
|
} finally {
|
||||||
|
setIsMigrating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<header className="space-y-1">
|
||||||
|
<h3 className="text-sm font-medium">
|
||||||
|
{t("settings.skillStorage.title")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{t("settings.skillStorage.description")}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div className="inline-flex gap-1 rounded-md border border-border-default bg-background p-1">
|
||||||
|
<StorageButton
|
||||||
|
active={value === "cc_switch"}
|
||||||
|
disabled={isMigrating}
|
||||||
|
onClick={() => handleSelect("cc_switch")}
|
||||||
|
>
|
||||||
|
{t("settings.skillStorage.ccSwitch")}
|
||||||
|
</StorageButton>
|
||||||
|
<StorageButton
|
||||||
|
active={value === "unified"}
|
||||||
|
disabled={isMigrating}
|
||||||
|
onClick={() => handleSelect("unified")}
|
||||||
|
>
|
||||||
|
{isMigrating && value !== "unified" ? (
|
||||||
|
<Loader2 size={14} className="mr-1 animate-spin" />
|
||||||
|
) : null}
|
||||||
|
{t("settings.skillStorage.unified")}
|
||||||
|
</StorageButton>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{value === "unified"
|
||||||
|
? t("settings.skillStorage.unifiedHint")
|
||||||
|
: t("settings.skillStorage.ccSwitchHint")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 迁移确认对话框 */}
|
||||||
|
<Dialog
|
||||||
|
open={pendingTarget !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setPendingTarget(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-md" zIndex="alert">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("settings.skillStorage.confirmTitle")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t("settings.skillStorage.confirmMessage", {
|
||||||
|
count: installedCount,
|
||||||
|
})}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setPendingTarget(null)}>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => pendingTarget && doMigrate(pendingTarget)}>
|
||||||
|
{t("common.confirm")}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StorageButtonProps {
|
||||||
|
active: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StorageButton({
|
||||||
|
active,
|
||||||
|
disabled,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: StorageButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
size="sm"
|
||||||
|
variant={active ? "default" : "ghost"}
|
||||||
|
className={cn(
|
||||||
|
"min-w-[96px]",
|
||||||
|
active
|
||||||
|
? "shadow-sm"
|
||||||
|
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -478,6 +478,18 @@
|
|||||||
"geminiDesc": "Google Gemini CLI",
|
"geminiDesc": "Google Gemini CLI",
|
||||||
"opencodeDesc": "OpenCode CLI"
|
"opencodeDesc": "OpenCode CLI"
|
||||||
},
|
},
|
||||||
|
"skillStorage": {
|
||||||
|
"title": "Skill Storage Location",
|
||||||
|
"description": "Choose where CC Switch stores the master copies of your skills",
|
||||||
|
"ccSwitch": "CC Switch",
|
||||||
|
"unified": "~/.agents/skills",
|
||||||
|
"ccSwitchHint": "Skills are stored in ~/.cc-switch/skills/ and synced to each app via symlink or copy.",
|
||||||
|
"unifiedHint": "Skills are stored in ~/.agents/skills/, the Agent Skills open standard. Compatible tools (Claude Code, Codex, Gemini CLI, etc.) discover skills here natively.",
|
||||||
|
"confirmTitle": "Migrate Skill Storage",
|
||||||
|
"confirmMessage": "{{count}} skill(s) will be moved to the new location. Continue?",
|
||||||
|
"migrationSuccess": "Successfully migrated {{count}} skill(s)",
|
||||||
|
"migrationPartial": "Migrated {{migrated}} skill(s), {{errors}} error(s). Check logs for details."
|
||||||
|
},
|
||||||
"skillSync": {
|
"skillSync": {
|
||||||
"title": "Skill Sync Method",
|
"title": "Skill Sync Method",
|
||||||
"description": "Choose how to sync Skills files",
|
"description": "Choose how to sync Skills files",
|
||||||
|
|||||||
@@ -478,6 +478,18 @@
|
|||||||
"geminiDesc": "Google Gemini CLI",
|
"geminiDesc": "Google Gemini CLI",
|
||||||
"opencodeDesc": "OpenCode CLI"
|
"opencodeDesc": "OpenCode CLI"
|
||||||
},
|
},
|
||||||
|
"skillStorage": {
|
||||||
|
"title": "スキル保存場所",
|
||||||
|
"description": "CC Switch がスキルのマスターコピーを保存するディレクトリを選択します",
|
||||||
|
"ccSwitch": "CC Switch",
|
||||||
|
"unified": "~/.agents/skills",
|
||||||
|
"ccSwitchHint": "スキルは ~/.cc-switch/skills/ に保存され、シンボリックリンクまたはコピーで各アプリに同期されます。",
|
||||||
|
"unifiedHint": "スキルは ~/.agents/skills/ に保存されます(Agent Skills オープン標準)。対応ツール(Claude Code、Codex、Gemini CLI など)はこのディレクトリのスキルを直接検出します。",
|
||||||
|
"confirmTitle": "スキル保存場所の移行",
|
||||||
|
"confirmMessage": "{{count}} 個のスキルを新しい場所に移動します。続行しますか?",
|
||||||
|
"migrationSuccess": "{{count}} 個のスキルを移行しました",
|
||||||
|
"migrationPartial": "{{migrated}} 個のスキルを移行、{{errors}} 個のエラー。詳細はログを確認してください。"
|
||||||
|
},
|
||||||
"skillSync": {
|
"skillSync": {
|
||||||
"title": "スキル同期方式",
|
"title": "スキル同期方式",
|
||||||
"description": "スキルファイルの同期方法を選択",
|
"description": "スキルファイルの同期方法を選択",
|
||||||
|
|||||||
@@ -478,6 +478,18 @@
|
|||||||
"geminiDesc": "Google Gemini CLI",
|
"geminiDesc": "Google Gemini CLI",
|
||||||
"opencodeDesc": "OpenCode CLI"
|
"opencodeDesc": "OpenCode CLI"
|
||||||
},
|
},
|
||||||
|
"skillStorage": {
|
||||||
|
"title": "技能存储位置",
|
||||||
|
"description": "选择 CC Switch 存放技能主副本的目录",
|
||||||
|
"ccSwitch": "CC Switch",
|
||||||
|
"unified": "~/.agents/skills",
|
||||||
|
"ccSwitchHint": "技能存储在 ~/.cc-switch/skills/,由 CC Switch 统一管理并同步到各应用。",
|
||||||
|
"unifiedHint": "技能存储在 ~/.agents/skills/,遵循 Agent Skills 开放标准。兼容的工具(Claude Code、Codex、Gemini CLI 等)可直接发现此目录中的技能。",
|
||||||
|
"confirmTitle": "迁移技能存储",
|
||||||
|
"confirmMessage": "将移动 {{count}} 个技能到新位置,是否继续?",
|
||||||
|
"migrationSuccess": "已成功迁移 {{count}} 个技能",
|
||||||
|
"migrationPartial": "迁移了 {{migrated}} 个技能,{{errors}} 个失败,请查看日志"
|
||||||
|
},
|
||||||
"skillSync": {
|
"skillSync": {
|
||||||
"title": "Skill 同步方式",
|
"title": "Skill 同步方式",
|
||||||
"description": "选择 Skills 的文件同步策略",
|
"description": "选择 Skills 的文件同步策略",
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ export interface SkillUpdateInfo {
|
|||||||
remoteHash: string;
|
remoteHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 存储位置迁移结果 */
|
||||||
|
export interface MigrationResult {
|
||||||
|
migratedCount: number;
|
||||||
|
skippedCount: number;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/** 仓库配置 */
|
/** 仓库配置 */
|
||||||
export interface SkillRepo {
|
export interface SkillRepo {
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -169,6 +176,13 @@ export const skillsApi = {
|
|||||||
return await invoke("update_skill", { id });
|
return await invoke("update_skill", { id });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** 迁移 Skill 存储位置 */
|
||||||
|
async migrateStorage(
|
||||||
|
target: "cc_switch" | "unified",
|
||||||
|
): Promise<MigrationResult> {
|
||||||
|
return await invoke("migrate_skill_storage", { target });
|
||||||
|
},
|
||||||
|
|
||||||
// ========== 兼容旧 API ==========
|
// ========== 兼容旧 API ==========
|
||||||
|
|
||||||
/** 获取技能列表(兼容旧 API) */
|
/** 获取技能列表(兼容旧 API) */
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const settingsSchema = z.object({
|
|||||||
|
|
||||||
// Skill 同步设置
|
// Skill 同步设置
|
||||||
skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(),
|
skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(),
|
||||||
|
skillStorageLocation: z.enum(["cc_switch", "unified"]).optional(),
|
||||||
|
|
||||||
// WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取)
|
// WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取)
|
||||||
webdavSync: z
|
webdavSync: z
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ export interface ProviderMeta {
|
|||||||
// Skill 同步方式
|
// Skill 同步方式
|
||||||
export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
export type SkillSyncMethod = "auto" | "symlink" | "copy";
|
||||||
|
|
||||||
|
// Skill 存储位置
|
||||||
|
export type SkillStorageLocation = "cc_switch" | "unified";
|
||||||
|
|
||||||
// Claude API 格式类型
|
// Claude API 格式类型
|
||||||
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
// - "anthropic": 原生 Anthropic Messages API 格式,直接透传
|
||||||
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
// - "openai_chat": OpenAI Chat Completions 格式,需要格式转换
|
||||||
@@ -292,6 +295,8 @@ export interface Settings {
|
|||||||
// ===== Skill 同步设置 =====
|
// ===== Skill 同步设置 =====
|
||||||
// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
// Skill 同步方式:auto(默认,优先 symlink)、symlink、copy
|
||||||
skillSyncMethod?: SkillSyncMethod;
|
skillSyncMethod?: SkillSyncMethod;
|
||||||
|
// Skill 存储位置:cc_switch(默认)或 unified(~/.agents/skills/)
|
||||||
|
skillStorageLocation?: SkillStorageLocation;
|
||||||
|
|
||||||
// ===== WebDAV v2 同步设置 =====
|
// ===== WebDAV v2 同步设置 =====
|
||||||
webdavSync?: WebDavSyncSettings;
|
webdavSync?: WebDavSyncSettings;
|
||||||
|
|||||||
Reference in New Issue
Block a user