@@ -548,6 +548,8 @@ def install_skill_from_zip(
548548 if not zipfile .is_zipfile (zip_path ):
549549 raise ValueError ("Uploaded file is not a valid zip archive." )
550550
551+ installed_skills = []
552+
551553 with zipfile .ZipFile (zip_path ) as zf :
552554 names = [
553555 name
@@ -573,34 +575,6 @@ def install_skill_from_zip(
573575 ):
574576 raise ValueError ("Invalid skill name." )
575577
576- if root_mode :
577- archive_hint = _normalize_skill_name (
578- archive_skill_name or zip_path_obj .stem
579- )
580- if not archive_hint or not _SKILL_NAME_RE .fullmatch (archive_hint ):
581- raise ValueError ("Invalid skill name." )
582- skill_name = archive_hint
583- else :
584- top_dirs = {
585- PurePosixPath (name ).parts [0 ] for name in file_names if name .strip ()
586- }
587- if len (top_dirs ) != 1 :
588- raise ValueError (
589- "Zip archive must contain a single top-level folder."
590- )
591- archive_root_name = next (iter (top_dirs ))
592- archive_root_name_normalized = _normalize_skill_name (archive_root_name )
593- if archive_root_name in {"." , ".." , "" } or not _SKILL_NAME_RE .fullmatch (
594- archive_root_name_normalized
595- ):
596- raise ValueError ("Invalid skill folder name." )
597- if archive_skill_name :
598- if not _SKILL_NAME_RE .fullmatch (archive_skill_name ):
599- raise ValueError ("Invalid skill name." )
600- skill_name = archive_skill_name
601- else :
602- skill_name = archive_root_name_normalized
603-
604578 for name in names :
605579 if not name :
606580 continue
@@ -609,42 +583,117 @@ def install_skill_from_zip(
609583 parts = PurePosixPath (name ).parts
610584 if ".." in parts :
611585 raise ValueError ("Zip archive contains invalid relative paths." )
612- if (not root_mode ) and parts and parts [0 ] != archive_root_name :
613- raise ValueError (
614- "Zip archive contains unexpected top-level entries."
615- )
616586
617- if root_mode :
618- if "SKILL.md" not in file_names and "skill.md" not in file_names :
619- raise ValueError ("SKILL.md not found in the skill folder." )
620- else :
621- if (
622- f"{ archive_root_name } /SKILL.md" not in file_names
623- and f"{ archive_root_name } /skill.md" not in file_names
624- ):
625- raise ValueError ("SKILL.md not found in the skill folder." )
587+ if not root_mode and not overwrite :
588+ top_dirs = {PurePosixPath (n ).parts [0 ] for n in file_names if n .strip ()}
589+ conflict_dirs : list [str ] = []
590+ for src_dir_name in top_dirs :
591+ if (
592+ f"{ src_dir_name } /SKILL.md" not in file_names
593+ and f"{ src_dir_name } /skill.md" not in file_names
594+ ):
595+ continue
596+
597+ candidate_name = _normalize_skill_name (src_dir_name )
598+ if not candidate_name or not _SKILL_NAME_RE .fullmatch (
599+ candidate_name
600+ ):
601+ continue
602+
603+ if archive_skill_name and len (top_dirs ) == 1 :
604+ target_name = archive_skill_name
605+ else :
606+ target_name = candidate_name
607+
608+ dest_dir = Path (self .skills_root ) / target_name
609+ if dest_dir .exists ():
610+ conflict_dirs .append (str (dest_dir ))
611+
612+ if conflict_dirs :
613+ raise FileExistsError (
614+ "One or more skills from the archive already exist and "
615+ "overwrite=False. No skills were installed. Conflicting "
616+ f"paths: { ', ' .join (conflict_dirs )} "
617+ )
626618
627619 with tempfile .TemporaryDirectory (dir = get_astrbot_temp_path ()) as tmp_dir :
628620 for member in zf .infolist ():
629621 member_name = member .filename .replace ("\\ " , "/" )
630622 if not member_name or _is_ignored_zip_entry (member_name ):
631623 continue
632624 zf .extract (member , tmp_dir )
633- src_dir = (
634- Path (tmp_dir ) if root_mode else Path (tmp_dir ) / archive_root_name
635- )
636- normalized_path = _normalize_skill_markdown_path (src_dir )
637- if normalized_path is None :
638- raise ValueError ("SKILL.md not found in the skill folder." )
639- _normalize_skill_markdown_path (src_dir )
640- if not src_dir .exists ():
641- raise ValueError ("Skill folder not found after extraction." )
642- dest_dir = Path (self .skills_root ) / skill_name
643- if dest_dir .exists ():
644- if not overwrite :
645- raise FileExistsError ("Skill already exists." )
646- shutil .rmtree (dest_dir )
647- shutil .move (str (src_dir ), str (dest_dir ))
648-
649- self .set_skill_active (skill_name , True )
650- return skill_name
625+
626+ if root_mode :
627+ archive_hint = _normalize_skill_name (
628+ archive_skill_name or zip_path_obj .stem
629+ )
630+ if not archive_hint or not _SKILL_NAME_RE .fullmatch (archive_hint ):
631+ raise ValueError ("Invalid skill name." )
632+ skill_name = archive_hint
633+
634+ src_dir = Path (tmp_dir )
635+ normalized_path = _normalize_skill_markdown_path (src_dir )
636+ if normalized_path is None :
637+ raise ValueError (
638+ "SKILL.md not found in the root of the zip archive."
639+ )
640+
641+ dest_dir = Path (self .skills_root ) / skill_name
642+ if dest_dir .exists () and overwrite :
643+ shutil .rmtree (dest_dir )
644+ elif dest_dir .exists () and not overwrite :
645+ raise FileExistsError (f"Skill { skill_name } already exists." )
646+
647+ shutil .move (str (src_dir ), str (dest_dir ))
648+ self .set_skill_active (skill_name , True )
649+ installed_skills .append (skill_name )
650+
651+ else :
652+ top_dirs = {
653+ PurePosixPath (n ).parts [0 ] for n in file_names if n .strip ()
654+ }
655+
656+ for archive_root_name in top_dirs :
657+ archive_root_name_normalized = _normalize_skill_name (
658+ archive_root_name
659+ )
660+
661+ if (
662+ f"{ archive_root_name } /SKILL.md" not in file_names
663+ and f"{ archive_root_name } /skill.md" not in file_names
664+ ):
665+ continue
666+
667+ if archive_root_name in {"." , ".." , "" } or not (
668+ _SKILL_NAME_RE .fullmatch (archive_root_name_normalized )
669+ ):
670+ continue
671+
672+ if archive_skill_name and len (top_dirs ) == 1 :
673+ skill_name = archive_skill_name
674+ else :
675+ skill_name = archive_root_name_normalized
676+
677+ src_dir = Path (tmp_dir ) / archive_root_name
678+ normalized_path = _normalize_skill_markdown_path (src_dir )
679+ if normalized_path is None :
680+ continue
681+
682+ dest_dir = Path (self .skills_root ) / skill_name
683+ if dest_dir .exists ():
684+ if not overwrite :
685+ raise FileExistsError (
686+ f"Skill { skill_name } already exists."
687+ )
688+ shutil .rmtree (dest_dir )
689+
690+ shutil .move (str (src_dir ), str (dest_dir ))
691+ self .set_skill_active (skill_name , True )
692+ installed_skills .append (skill_name )
693+
694+ if not installed_skills :
695+ raise ValueError (
696+ "No valid SKILL.md found in any folder of the zip archive."
697+ )
698+
699+ return ", " .join (installed_skills )
0 commit comments