@@ -21,6 +21,19 @@ use crate::error::format_skill_error;
2121
2222// ========== 数据结构 ==========
2323
24+ /// Skill 同步方式
25+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Serialize , Deserialize , Default ) ]
26+ #[ serde( rename_all = "lowercase" ) ]
27+ pub enum SyncMethod {
28+ /// 自动选择:优先 symlink,失败时回退到 copy
29+ #[ default]
30+ Auto ,
31+ /// 符号链接(推荐,节省磁盘空间)
32+ Symlink ,
33+ /// 文件复制(兼容模式)
34+ Copy ,
35+ }
36+
2437/// 可发现的技能(来自仓库)
2538#[ derive( Debug , Clone , Serialize , Deserialize ) ]
2639pub struct DiscoverableSkill {
@@ -305,7 +318,7 @@ impl SkillService {
305318 db. save_skill ( & installed_skill) ?;
306319
307320 // 同步到当前应用目录
308- Self :: copy_to_app ( & install_name, current_app) ?;
321+ Self :: sync_to_app_dir ( & install_name, current_app) ?;
309322
310323 log:: info!(
311324 "Skill {} 安装成功,已启用 {:?}" ,
@@ -368,7 +381,7 @@ impl SkillService {
368381
369382 // 同步文件
370383 if enabled {
371- Self :: copy_to_app ( & skill. directory , app) ?;
384+ Self :: sync_to_app_dir ( & skill. directory , app) ?;
372385 } else {
373386 Self :: remove_from_app ( & skill. directory , app) ?;
374387 }
@@ -566,8 +579,41 @@ impl SkillService {
566579
567580 // ========== 文件同步方法 ==========
568581
569- /// 复制 Skill 到应用目录
570- pub fn copy_to_app ( directory : & str , app : & AppType ) -> Result < ( ) > {
582+ /// 创建符号链接(跨平台)
583+ ///
584+ /// - Unix: 使用 std::os::unix::fs::symlink
585+ /// - Windows: 使用 std::os::windows::fs::symlink_dir
586+ #[ cfg( unix) ]
587+ fn create_symlink ( src : & Path , dest : & Path ) -> Result < ( ) > {
588+ std:: os:: unix:: fs:: symlink ( src, dest)
589+ . with_context ( || format ! ( "创建符号链接失败: {} -> {}" , src. display( ) , dest. display( ) ) )
590+ }
591+
592+ #[ cfg( windows) ]
593+ fn create_symlink ( src : & Path , dest : & Path ) -> Result < ( ) > {
594+ std:: os:: windows:: fs:: symlink_dir ( src, dest)
595+ . with_context ( || format ! ( "创建符号链接失败: {} -> {}" , src. display( ) , dest. display( ) ) )
596+ }
597+
598+ /// 检查路径是否为符号链接
599+ fn is_symlink ( path : & Path ) -> bool {
600+ path. symlink_metadata ( )
601+ . map ( |m| m. file_type ( ) . is_symlink ( ) )
602+ . unwrap_or ( false )
603+ }
604+
605+ /// 获取当前同步方式配置
606+ fn get_sync_method ( ) -> SyncMethod {
607+ crate :: settings:: get_skill_sync_method ( )
608+ }
609+
610+ /// 同步 Skill 到应用目录(使用 symlink 或 copy)
611+ ///
612+ /// 根据配置和平台选择最佳同步方式:
613+ /// - Auto: 优先尝试 symlink,失败时回退到 copy
614+ /// - Symlink: 仅使用 symlink
615+ /// - Copy: 仅使用文件复制
616+ pub fn sync_to_app_dir ( directory : & str , app : & AppType ) -> Result < ( ) > {
571617 let ssot_dir = Self :: get_ssot_dir ( ) ?;
572618 let source = ssot_dir. join ( directory) ;
573619
@@ -580,25 +626,77 @@ impl SkillService {
580626
581627 let dest = app_dir. join ( directory) ;
582628
583- // 如果已存在则先删除
584- if dest. exists ( ) {
585- fs :: remove_dir_all ( & dest) ?;
629+ // 如果已存在则先删除(无论是 symlink 还是真实目录)
630+ if dest. exists ( ) || Self :: is_symlink ( & dest ) {
631+ Self :: remove_path ( & dest) ?;
586632 }
587633
588- Self :: copy_dir_recursive ( & source , & dest ) ? ;
634+ let sync_method = Self :: get_sync_method ( ) ;
589635
590- log:: debug!( "Skill {directory} 已复制到 {app:?}" ) ;
636+ match sync_method {
637+ SyncMethod :: Auto => {
638+ // 优先尝试 symlink
639+ match Self :: create_symlink ( & source, & dest) {
640+ Ok ( ( ) ) => {
641+ log:: debug!( "Skill {directory} 已通过 symlink 同步到 {app:?}" ) ;
642+ return Ok ( ( ) ) ;
643+ }
644+ Err ( err) => {
645+ log:: warn!(
646+ "Symlink 创建失败,将回退到文件复制: {} -> {}. 错误: {err:#}" ,
647+ source. display( ) ,
648+ dest. display( )
649+ ) ;
650+ }
651+ }
652+ // Fallback 到 copy
653+ Self :: copy_dir_recursive ( & source, & dest) ?;
654+ log:: debug!( "Skill {directory} 已通过复制同步到 {app:?}" ) ;
655+ }
656+ SyncMethod :: Symlink => {
657+ Self :: create_symlink ( & source, & dest) ?;
658+ log:: debug!( "Skill {directory} 已通过 symlink 同步到 {app:?}" ) ;
659+ }
660+ SyncMethod :: Copy => {
661+ Self :: copy_dir_recursive ( & source, & dest) ?;
662+ log:: debug!( "Skill {directory} 已通过复制同步到 {app:?}" ) ;
663+ }
664+ }
591665
592666 Ok ( ( ) )
593667 }
594668
595- /// 从应用目录删除 Skill
669+ /// 复制 Skill 到应用目录(保留用于向后兼容)
670+ #[ deprecated( note = "请使用 sync_to_app_dir() 代替" ) ]
671+ pub fn copy_to_app ( directory : & str , app : & AppType ) -> Result < ( ) > {
672+ Self :: sync_to_app_dir ( directory, app)
673+ }
674+
675+ /// 删除路径(支持 symlink 和真实目录)
676+ fn remove_path ( path : & Path ) -> Result < ( ) > {
677+ if Self :: is_symlink ( path) {
678+ // 符号链接:仅删除链接本身,不影响源文件
679+ #[ cfg( unix) ]
680+ fs:: remove_file ( path) ?;
681+ #[ cfg( windows) ]
682+ fs:: remove_dir ( path) ?; // Windows 的目录 symlink 需要用 remove_dir
683+ } else if path. is_dir ( ) {
684+ // 真实目录:递归删除
685+ fs:: remove_dir_all ( path) ?;
686+ } else if path. exists ( ) {
687+ // 普通文件
688+ fs:: remove_file ( path) ?;
689+ }
690+ Ok ( ( ) )
691+ }
692+
693+ /// 从应用目录删除 Skill(支持 symlink 和真实目录)
596694 pub fn remove_from_app ( directory : & str , app : & AppType ) -> Result < ( ) > {
597695 let app_dir = Self :: get_app_skills_dir ( app) ?;
598696 let skill_path = app_dir. join ( directory) ;
599697
600- if skill_path. exists ( ) {
601- fs :: remove_dir_all ( & skill_path) ?;
698+ if skill_path. exists ( ) || Self :: is_symlink ( & skill_path ) {
699+ Self :: remove_path ( & skill_path) ?;
602700 log:: debug!( "Skill {directory} 已从 {app:?} 删除" ) ;
603701 }
604702
@@ -611,7 +709,7 @@ impl SkillService {
611709
612710 for skill in skills. values ( ) {
613711 if skill. apps . is_enabled_for ( app) {
614- Self :: copy_to_app ( & skill. directory , app) ?;
712+ Self :: sync_to_app_dir ( & skill. directory , app) ?;
615713 }
616714 }
617715
0 commit comments