diff --git a/pom.xml b/pom.xml
index f2796c1..5f44968 100644
--- a/pom.xml
+++ b/pom.xml
@@ -282,6 +282,24 @@
4.38.0.ALL
+
+
+ org.yaml
+ snakeyaml
+ 2.2
+
+
+
+ net.sf.sevenzipjbinding
+ sevenzipjbinding
+ 16.02-2.01
+
+
+ net.sf.sevenzipjbinding
+ sevenzipjbinding-all-platforms
+ 16.02-2.01
+
+
diff --git a/src/main/java/com/kexue/skills/common/util/SkillZipParser.java b/src/main/java/com/kexue/skills/common/util/SkillZipParser.java
new file mode 100644
index 0000000..36ab985
--- /dev/null
+++ b/src/main/java/com/kexue/skills/common/util/SkillZipParser.java
@@ -0,0 +1,843 @@
+package com.kexue.skills.common.util;
+
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import net.sf.sevenzipjbinding.*;
+import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
+import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
+import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
+
+/**
+ * 技能包解析工具类
+ * 用于解析skill zip包并生成符合要求的yaml描述
+ */
+public class SkillZipParser {
+
+ /**
+ * 提取压缩包中的skillMdText
+ * @param filePath 压缩文件路径
+ * @param defaultSkillName 默认技能名称
+ * @return 包含skillMdText的Map
+ * @throws IOException 解析过程中的IO异常
+ * @throws SevenZipException 解析RAR文件时的异常
+ */
+ public static Map extractSkillMdText(String filePath, String defaultSkillName) throws IOException, SevenZipException {
+ // 检查文件扩展名
+ String fileExtension = "";
+ if (filePath.contains(".")) {
+ int dotIndex = filePath.lastIndexOf(".");
+ fileExtension = filePath.substring(dotIndex).toLowerCase();
+ }
+
+ // 根据文件类型选择不同的解析方法
+ if (".rar".equals(fileExtension)) {
+ return extractSkillInfoFromRar(filePath, defaultSkillName);
+ } else {
+ // 默认为zip文件
+ try (ZipFile zipFile = new ZipFile(filePath, StandardCharsets.UTF_8)) {
+ return extractSkillInfo(zipFile, defaultSkillName);
+ }
+ }
+ }
+
+ /**
+ * 生成技能包的yaml描述
+ * @param filePath 压缩文件路径
+ * @param author 作者
+ * @param skillName 技能名称
+ * @param skillDescription 技能描述
+ * @param skillTags 技能标签
+ * @return 生成的yaml描述
+ * @throws IOException 解析过程中的IO异常
+ * @throws SevenZipException 解析RAR文件时的异常
+ */
+ public static String generateYamlFromSkillInfo(String filePath, String author, String skillName, String skillDescription, List skillTags) throws IOException, SevenZipException {
+ // 检查文件扩展名
+ String fileExtension = "";
+ if (filePath.contains(".")) {
+ int dotIndex = filePath.lastIndexOf(".");
+ fileExtension = filePath.substring(dotIndex).toLowerCase();
+ }
+
+ // 根据文件类型选择不同的解析方法
+ Map skillStructure;
+ if (".rar".equals(fileExtension)) {
+ skillStructure = generateSkillStructureFromRar(filePath, skillName, skillDescription, skillTags, author);
+ } else {
+ // 默认为zip文件
+ try (ZipFile zipFile = new ZipFile(filePath, StandardCharsets.UTF_8)) {
+ skillStructure = generateSkillStructure(zipFile, skillName, skillDescription, skillTags, author);
+ }
+ }
+
+ // 生成yaml
+ return generateYaml(skillStructure);
+ }
+
+ /**
+ * 解析skill zip包并生成yaml描述
+ * @param zipFilePath zip文件路径
+ * @param author 作者
+ * @param defaultSkillName 默认技能名称
+ * @return 包含技能信息和yaml描述的Map
+ * @throws IOException 解析过程中的IO异常
+ * @throws SevenZipException 解析RAR文件时的异常
+ */
+ public static Map parseSkillZip(String zipFilePath, String author, String defaultSkillName) throws IOException, SevenZipException {
+ // 检查文件扩展名
+ String fileExtension = "";
+ if (zipFilePath.contains(".")) {
+ int dotIndex = zipFilePath.lastIndexOf(".");
+ fileExtension = zipFilePath.substring(dotIndex).toLowerCase();
+ }
+
+ // 根据文件类型选择不同的解析方法
+ if (".rar".equals(fileExtension)) {
+ return parseRarFile(zipFilePath, author, defaultSkillName);
+ } else {
+ // 默认为zip文件
+ try (ZipFile zipFile = new ZipFile(zipFilePath, StandardCharsets.UTF_8)) {
+ // 从压缩文件中提取技能信息
+ Map skillInfo = extractSkillInfo(zipFile, defaultSkillName);
+ String skillName = (String) skillInfo.get("name");
+ String skillDescription = (String) skillInfo.get("description");
+ List skillTags = (List) skillInfo.get("tags");
+
+ // 生成技能包结构
+ Map skillStructure = generateSkillStructure(zipFile, skillName, skillDescription, skillTags, author);
+
+ // 生成yaml
+ String yamlContent = generateYaml(skillStructure);
+
+ // 构建返回结果
+ Map result = new LinkedHashMap<>();
+ result.put("name", skillName);
+ result.put("description", skillDescription);
+ result.put("tags", skillTags);
+ result.put("yamlContent", yamlContent);
+ // 包含md文件的完整内容
+ if (skillInfo.containsKey("skillMdText")) {
+ result.put("skillMdText", skillInfo.get("skillMdText"));
+ }
+
+ return result;
+ }
+ }
+ }
+
+ /**
+ * 解析rar文件
+ * @param rarFilePath rar文件路径
+ * @param author 作者
+ * @param defaultSkillName 默认技能名称
+ * @return 技能信息
+ * @throws IOException 解析过程中的IO异常
+ * @throws SevenZipException 解析RAR文件时的异常
+ */
+ private static Map parseRarFile(String rarFilePath, String author, String defaultSkillName) throws IOException, SevenZipException {
+ // 从rar文件中提取技能信息
+ Map skillInfo = extractSkillInfoFromRar(rarFilePath, defaultSkillName);
+ String skillName = (String) skillInfo.get("name");
+ String skillDescription = (String) skillInfo.get("description");
+ List skillTags = (List) skillInfo.get("tags");
+
+ // 生成技能包结构
+ Map skillStructure = generateSkillStructureFromRar(rarFilePath, skillName, skillDescription, skillTags, author);
+
+ // 生成yaml
+ String yamlContent = generateYaml(skillStructure);
+
+ // 构建返回结果
+ Map result = new LinkedHashMap<>();
+ result.put("name", skillName);
+ result.put("description", skillDescription);
+ result.put("tags", skillTags);
+ result.put("yamlContent", yamlContent);
+ // 包含md文件的完整内容
+ if (skillInfo.containsKey("skillMdText")) {
+ result.put("skillMdText", skillInfo.get("skillMdText"));
+ }
+
+ return result;
+ }
+
+ /**
+ * 从rar文件中提取技能信息
+ * @param rarFilePath rar文件路径
+ * @param defaultSkillName 默认技能名称
+ * @return 技能信息
+ * @throws IOException 解析过程中的IO异常
+ * @throws Exception 解析过程中的异常
+ */
+ private static Map extractSkillInfoFromRar(String rarFilePath, String defaultSkillName) throws IOException, SevenZipException {
+ Map skillInfo = new LinkedHashMap<>();
+ boolean foundMdFile = false;
+
+ // 验证文件是否存在
+ File rarFile = new File(rarFilePath);
+ if (!rarFile.exists()) {
+ throw new FileNotFoundException("RAR file not found: " + rarFilePath);
+ }
+ if (!rarFile.canRead()) {
+ throw new IOException("Cannot read RAR file: " + rarFilePath);
+ }
+
+ try (RandomAccessFile randomAccessFile = new RandomAccessFile(rarFile, "r");
+ IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))) {
+ // 使用简单接口
+ ISimpleInArchive simpleInArchive = archive.getSimpleInterface();
+
+ // 遍历所有文件条目
+ for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
+ // 检查是否是根目录下的md文件
+ String path = item.getPath();
+ if (!item.isFolder() && path.endsWith(".md") && !path.contains("/") && !path.contains("\\")) {
+ // 读取文件内容
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ item.extractSlow(data -> {
+ try {
+ outputStream.write(data);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return data.length;
+ });
+
+ String content = outputStream.toString(StandardCharsets.UTF_8);
+
+ // 存储md文件的完整内容
+ skillInfo.put("skillMdText", content);
+
+ // 解析md内容
+ parseSkillsMd(content, skillInfo);
+ foundMdFile = true;
+ break; // 找到一个md文件后就停止
+ }
+ }
+ }
+
+ // 如果没有找到 md 文件,使用默认值
+ if (!foundMdFile) {
+ skillInfo.put("name", defaultSkillName);
+ skillInfo.put("description", "Skill uploaded via uploadSkillV2");
+ skillInfo.put("tags", Arrays.asList("10001", "10002"));
+ } else {
+ // 确保 tags 不为 null
+ if (!skillInfo.containsKey("tags")) {
+ skillInfo.put("tags", Arrays.asList("10001"));
+ }
+ }
+
+ return skillInfo;
+ }
+
+ /**
+ * 从压缩文件中提取技能信息
+ * @param zipFile zip文件对象
+ * @param defaultSkillName 默认技能名称
+ * @return 技能信息
+ * @throws IOException 解析过程中的IO异常
+ */
+ private static Map extractSkillInfo(ZipFile zipFile, String defaultSkillName) throws IOException {
+ Map skillInfo = new LinkedHashMap<>();
+ boolean foundMdFile = false;
+
+ // 尝试从zip根目录的md文件中提取信息
+ Enumeration extends ZipEntry> entries = zipFile.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ // 检查是否是根目录下的md文件
+ if (!entry.isDirectory() && entry.getName().endsWith(".md") && !entry.getName().contains("/")) {
+ try (InputStream inputStream = zipFile.getInputStream(entry);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+ StringBuilder content = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ content.append(line).append("\n");
+ }
+
+ // 存储md文件的完整内容
+ skillInfo.put("skillMdText", content.toString());
+
+ // 解析md内容
+ parseSkillsMd(content.toString(), skillInfo);
+ foundMdFile = true;
+ break; // 找到一个md文件后就停止
+ }
+ }
+ }
+
+ // 如果没有找到 md 文件,使用默认值
+ if (!foundMdFile) {
+ skillInfo.put("name", defaultSkillName);
+ skillInfo.put("description", "Skill uploaded ");
+ skillInfo.put("tags", Arrays.asList("10001", "10002"));
+ } else {
+ // 确保 tags 不为 null
+ if (!skillInfo.containsKey("tags")) {
+ skillInfo.put("tags", Arrays.asList("10001"));
+ }
+ }
+
+ return skillInfo;
+ }
+
+ /**
+ * 解析skills.md文件内容
+ * @param content skills.md文件内容
+ * @param skillInfo 技能信息Map
+ */
+ private static void parseSkillsMd(String content, Map skillInfo) {
+ // 解析技能名称
+ Pattern namePattern = Pattern.compile("#\s+(.*)");
+ Matcher nameMatcher = namePattern.matcher(content);
+ if (nameMatcher.find()) {
+ skillInfo.put("name", nameMatcher.group(1).trim());
+ }
+
+ // 解析技能描述
+ Pattern descPattern = Pattern.compile("##\s+Description\s+(.*?)(?=##|$)", Pattern.DOTALL);
+ Matcher descMatcher = descPattern.matcher(content);
+ if (descMatcher.find()) {
+ String description = descMatcher.group(1).trim();
+ skillInfo.put("description", description);
+ }
+
+ // 解析技能标签
+ Pattern tagPattern = Pattern.compile("##\s+Tags\s+(.*?)(?=##|$)", Pattern.DOTALL);
+ Matcher tagMatcher = tagPattern.matcher(content);
+ if (tagMatcher.find()) {
+ String tagsSection = tagMatcher.group(1);
+ Pattern tagItemPattern = Pattern.compile("-\s+(.*)");
+ Matcher tagItemMatcher = tagItemPattern.matcher(tagsSection);
+ List tags = new ArrayList<>();
+ while (tagItemMatcher.find()) {
+ tags.add(tagItemMatcher.group(1).trim());
+ }
+ if (!tags.isEmpty()) {
+ skillInfo.put("tags", tags);
+ }
+ }
+ }
+
+ /**
+ * 从rar文件生成技能包结构
+ * @param rarFilePath rar文件路径
+ * @param skillName 技能包名称
+ * @param skillDescription 技能包描述
+ * @param skillTags 技能包标签
+ * @param author 作者
+ * @return 技能包结构
+ * @throws IOException 解析过程中的IO异常
+ * @throws Exception 解析过程中的异常
+ */
+ private static Map generateSkillStructureFromRar(String rarFilePath, String skillName, String skillDescription, List skillTags, String author) throws IOException, SevenZipException {
+ // 验证文件是否存在
+ File rarFile = new File(rarFilePath);
+ if (!rarFile.exists()) {
+ throw new FileNotFoundException("RAR file not found: " + rarFilePath);
+ }
+ if (!rarFile.canRead()) {
+ throw new IOException("Cannot read RAR file: " + rarFilePath);
+ }
+
+ Map skillStructure = new LinkedHashMap<>();
+
+ // 核心概要
+ skillStructure.put("name", skillName);
+ skillStructure.put("version", "1.0.0");
+ skillStructure.put("description", skillDescription);
+ skillStructure.put("author", author);
+ skillStructure.put("created", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+ skillStructure.put("tags", skillTags);
+
+ // 核心节点 structure
+ Map structure = new LinkedHashMap<>();
+ structure.put("name", skillName);
+ structure.put("type", "directory");
+ structure.put("path", ".");
+ structure.put("format", "directory");
+ structure.put("description", skillDescription);
+
+ // 构建目录树结构
+ List