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 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> children = new ArrayList<>(); + Map> directoryMap = new HashMap<>(); + + // 一次性读取所有条目并处理 + try (RandomAccessFile randomAccessFile = new RandomAccessFile(rarFile, "r"); + IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))) { + // 使用简单接口 + ISimpleInArchive simpleInArchive = archive.getSimpleInterface(); + ISimpleInArchiveItem[] archiveItems = simpleInArchive.getArchiveItems(); + + // 首先处理所有目录 + for (ISimpleInArchiveItem item : archiveItems) { + if (item.isFolder()) { + String path = item.getPath(); + // 确保路径以/结尾,与 zip 处理一致 + if (!path.endsWith("/")) { + path = path + "/"; + } + String[] pathParts = path.split("/"); + createDirectoryTree(children, directoryMap, pathParts, path); + } + } + + // 然后处理所有文件 + for (ISimpleInArchiveItem item : archiveItems) { + if (!item.isFolder()) { + String path = item.getPath(); + String[] pathParts = path.split("/"); + String fileName = pathParts[pathParts.length - 1]; + + // 读取文件内容 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + item.extractSlow(data -> { + try { + outputStream.write(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + return data.length; + }); + + String fileContent = outputStream.toString(StandardCharsets.UTF_8); + addFileToTreeFromRar(children, directoryMap, pathParts, fileName, fileContent, path); + } + } + } + + // 确保包含skills.md文件 + boolean hasSkillsMd = false; + for (Map child : children) { + if ("skills.md".equals(child.get("name")) && "file".equals(child.get("type"))) { + hasSkillsMd = true; + break; + } + } + if (!hasSkillsMd) { + Map skillsMdNode = createSkillsMdNode(skillName, skillDescription, skillTags); + children.add(skillsMdNode); + } + + // 确保包含scripts目录 + boolean hasScriptsDir = false; + for (Map child : children) { + if ("scripts".equals(child.get("name")) && "directory".equals(child.get("type"))) { + hasSkillsMd = true; + break; + } + } + if (!hasScriptsDir) { + Map scriptsDirNode = createScriptsDirNode(); + children.add(scriptsDirNode); + } + + structure.put("children", children); + skillStructure.put("structure", structure); + + return skillStructure; + } + + /** + * 将 rar 文件添加到目录树中 + * @param children 子节点列表 + * @param directoryMap 目录映射 + * @param pathParts 路径部分 + * @param item rar 文件项 + * @param fileName 文件名 + * @param fileContent 文件内容 + * @param filePath 文件路径 + * @throws IOException 读取文件内容时的 IO 异常 + */ + private static void addFileToTreeFromRar(List> children, Map> directoryMap, String[] pathParts, String fileName, String fileContent, String filePath) throws IOException { + if (pathParts.length == 0) return; + + // 构建目录路径 + StringBuilder directoryPath = new StringBuilder(); + for (int i = 0; i < pathParts.length - 1; i++) { + String part = pathParts[i]; + if (!part.isEmpty()) { + directoryPath.append(part).append("/"); + } + } + + // 找到文件所在的目录节点 + List> targetChildren = children; + if (directoryPath.length() > 0) { + Map directoryNode = directoryMap.get(directoryPath.toString()); + if (directoryNode != null) { + targetChildren = (List>) directoryNode.get("children"); + } + } + + // 创建文件节点并添加到目标目录 + Map fileNode = createFileNodeFromRar(fileName, fileContent, filePath); + targetChildren.add(fileNode); + } + + /** + * 从文件信息创建文件节点 + * @param fileName 文件名 + * @param fileContent 文件内容 + * @param filePath 文件路径 + * @return 文件节点 + * @throws IOException 读取文件内容时的 IO 异常 + */ + private static Map createFileNodeFromRar(String fileName, String fileContent, String filePath) throws IOException { + Map fileNode = new LinkedHashMap<>(); + + fileNode.put("name", fileName); + fileNode.put("type", "file"); + fileNode.put("path", filePath); + fileNode.put("format", getFileFormat(fileName)); + fileNode.put("description", fileName + " file"); + fileNode.put("content", fileContent); + + return fileNode; + } + + /** + * 生成技能包结构 + * @param zipFile zip文件对象 + * @param skillName 技能包名称 + * @param skillDescription 技能包描述 + * @param skillTags 技能包标签 + * @param author 作者 + * @return 技能包结构 + * @throws IOException 解析过程中的IO异常 + */ + private static Map generateSkillStructure(ZipFile zipFile, String skillName, String skillDescription, List skillTags, String author) throws IOException { + 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> children = new ArrayList<>(); + Map> directoryMap = new HashMap<>(); + + // 首先处理所有目录 + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.isDirectory()) { + String path = entry.getName(); + String[] pathParts = path.split("/"); + createDirectoryTree(children, directoryMap, pathParts, path); + } + } + + // 然后处理所有文件 + entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory()) { + String path = entry.getName(); + String[] pathParts = path.split("/"); + addFileToTree(children, directoryMap, pathParts, entry, zipFile); + } + } + + // 确保包含skills.md文件 + boolean hasSkillsMd = false; + for (Map child : children) { + if ("skills.md".equals(child.get("name")) && "file".equals(child.get("type"))) { + hasSkillsMd = true; + break; + } + } + if (!hasSkillsMd) { + Map skillsMdNode = createSkillsMdNode(skillName, skillDescription, skillTags); + children.add(skillsMdNode); + } + + // 确保包含scripts目录 + boolean hasScriptsDir = false; + for (Map child : children) { + if ("scripts".equals(child.get("name")) && "directory".equals(child.get("type"))) { + hasScriptsDir = true; + break; + } + } + if (!hasScriptsDir) { + Map scriptsDirNode = createScriptsDirNode(); + children.add(scriptsDirNode); + } + + structure.put("children", children); + skillStructure.put("structure", structure); + + return skillStructure; + } + + /** + * 创建目录树结构 + * @param children 子节点列表 + * @param directoryMap 目录映射 + * @param pathParts 路径部分 + * @param fullPath 完整路径 + */ + private static void createDirectoryTree(List> children, Map> directoryMap, String[] pathParts, String fullPath) { + List> currentChildren = children; + StringBuilder currentPath = new StringBuilder(); + + for (int i = 0; i < pathParts.length; i++) { + String part = pathParts[i]; + if (part.isEmpty()) continue; + + currentPath.append(part).append("/"); + String pathKey = currentPath.toString(); + + // 检查当前目录是否已存在 + Map directoryNode = directoryMap.get(pathKey); + if (directoryNode == null) { + // 创建新目录节点 + directoryNode = new LinkedHashMap<>(); + directoryNode.put("name", part); + directoryNode.put("type", "directory"); + directoryNode.put("path", pathKey); + directoryNode.put("format", "directory"); + directoryNode.put("description", part + " directory"); + directoryNode.put("children", new ArrayList<>()); + + // 添加到当前子节点列表 + currentChildren.add(directoryNode); + directoryMap.put(pathKey, directoryNode); + } + + // 进入下一级目录 + currentChildren = (List>) directoryNode.get("children"); + } + } + + /** + * 将文件添加到目录树中 + * @param children 子节点列表 + * @param directoryMap 目录映射 + * @param pathParts 路径部分 + * @param entry zip条目 + * @param zipFile zip文件对象 + * @throws IOException 读取文件内容时的IO异常 + */ + private static void addFileToTree(List> children, Map> directoryMap, String[] pathParts, ZipEntry entry, ZipFile zipFile) throws IOException { + if (pathParts.length == 0) return; + + // 获取文件名 + String fileName = pathParts[pathParts.length - 1]; + + // 构建目录路径 + StringBuilder directoryPath = new StringBuilder(); + for (int i = 0; i < pathParts.length - 1; i++) { + String part = pathParts[i]; + if (!part.isEmpty()) { + directoryPath.append(part).append("/"); + } + } + + // 找到文件所在的目录节点 + List> targetChildren = children; + if (directoryPath.length() > 0) { + Map directoryNode = directoryMap.get(directoryPath.toString()); + if (directoryNode != null) { + targetChildren = (List>) directoryNode.get("children"); + } + } + + // 创建文件节点并添加到目标目录 + Map fileNode = createFileNode(entry, zipFile); + targetChildren.add(fileNode); + } + + /** + * 创建目录节点 + * @param entry zip条目 + * @param zipFile zip文件对象 + * @return 目录节点 + */ + private static Map createDirectoryNode(ZipEntry entry, ZipFile zipFile) { + Map directoryNode = new LinkedHashMap<>(); + String name = entry.getName().replaceAll("/$", ""); + name = name.substring(name.lastIndexOf("/") + 1); + + directoryNode.put("name", name); + directoryNode.put("type", "directory"); + directoryNode.put("path", entry.getName()); + directoryNode.put("format", "directory"); + directoryNode.put("description", name + " directory"); + directoryNode.put("children", new ArrayList<>()); + + return directoryNode; + } + + /** + * 创建文件节点 + * @param entry zip条目 + * @param zipFile zip文件对象 + * @return 文件节点 + * @throws IOException 读取文件内容时的IO异常 + */ + private static Map createFileNode(ZipEntry entry, ZipFile zipFile) throws IOException { + Map fileNode = new LinkedHashMap<>(); + String name = entry.getName().substring(entry.getName().lastIndexOf("/") + 1); + + fileNode.put("name", name); + fileNode.put("type", "file"); + fileNode.put("path", entry.getName()); + fileNode.put("format", getFileFormat(name)); + fileNode.put("description", name + " file"); + + // 读取文件内容 + 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"); + } + fileNode.put("content", content.toString()); + } + + return fileNode; + } + + /** + * 创建skills.md文件节点 + * @param skillName 技能包名称 + * @param skillDescription 技能包描述 + * @param skillTags 技能包标签 + * @return skills.md文件节点 + */ + private static Map createSkillsMdNode(String skillName, String skillDescription, List skillTags) { + Map skillsMdNode = new LinkedHashMap<>(); + + skillsMdNode.put("name", "skills.md"); + skillsMdNode.put("type", "file"); + skillsMdNode.put("path", "skills.md"); + skillsMdNode.put("format", "markdown"); + skillsMdNode.put("description", "Skills metadata file"); + + // 生成skills.md内容 + StringBuilder content = new StringBuilder(); + content.append("# " + skillName).append("\n\n"); + content.append("## Description").append("\n"); + content.append(skillDescription).append("\n\n"); + content.append("## Tags").append("\n"); + if (skillTags != null && !skillTags.isEmpty()) { + for (String tag : skillTags) { + content.append("- " + tag).append("\n"); + } + } else { + content.append("- general\n").append("- skill\n"); + } + content.append("\n"); + content.append("## Usage").append("\n"); + content.append("This skill provides functionality for " + skillName).append("\n"); + + skillsMdNode.put("content", content.toString()); + + return skillsMdNode; + } + + /** + * 创建scripts目录节点 + * @return scripts目录节点 + */ + private static Map createScriptsDirNode() { + Map scriptsDirNode = new LinkedHashMap<>(); + + scriptsDirNode.put("name", "scripts"); + scriptsDirNode.put("type", "directory"); + scriptsDirNode.put("path", "scripts/"); + scriptsDirNode.put("format", "directory"); + scriptsDirNode.put("description", "Scripts directory"); + + // 添加示例脚本 + List> scriptChildren = new ArrayList<>(); + + // 示例Python脚本 + Map pythonScriptNode = new LinkedHashMap<>(); + pythonScriptNode.put("name", "example.py"); + pythonScriptNode.put("type", "file"); + pythonScriptNode.put("path", "scripts/example.py"); + pythonScriptNode.put("format", "python"); + pythonScriptNode.put("description", "Example Python script"); + pythonScriptNode.put("content", "#!/usr/bin/env python3\n" + + "\"\"\"\n" + + "Example script for " + "skill" + "\n" + + "\"\"\"\n\n" + + "def main():\n" + + " print('Hello from skill script!')\n\n" + + "if __name__ == '__main__':\n" + + " main()\n"); + scriptChildren.add(pythonScriptNode); + + scriptsDirNode.put("children", scriptChildren); + + return scriptsDirNode; + } + + /** + * 获取文件格式 + * @param fileName 文件名 + * @return 文件格式 + */ + private static String getFileFormat(String fileName) { + if (fileName.endsWith(".md")) { + return "markdown"; + } else if (fileName.endsWith(".py")) { + return "python"; + } else if (fileName.endsWith(".js")) { + return "javascript"; + } else if (fileName.endsWith(".json")) { + return "json"; + } else if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) { + return "yaml"; + } else if (fileName.endsWith(".txt")) { + return "text"; + } else if (fileName.endsWith(".sh")) { + return "shell"; + } else { + return "other"; + } + } + + /** + * 生成yaml + * @param skillStructure 技能包结构 + * @return 生成的yaml描述 + */ + private static String generateYaml(Map skillStructure) { + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setIndent(2); + options.setPrettyFlow(true); + + Yaml yaml = new Yaml(options); + return yaml.dump(skillStructure); + } + +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/controller/CmsContentController.java b/src/main/java/com/kexue/skills/controller/CmsContentController.java index 5143f37..75b94ac 100644 --- a/src/main/java/com/kexue/skills/controller/CmsContentController.java +++ b/src/main/java/com/kexue/skills/controller/CmsContentController.java @@ -299,4 +299,17 @@ public class CmsContentController { String content = cmsContentService.getContent(queryContentDto.getContentId(), queryContentDto.getLanguageType()); return CommonResult.success(content); } + + /** + * 获取CmsContent的title字段内容 + * + * @param contentId 内容ID + * @return title字段的内容 + */ + @PostMapping("/getTitle/{contentId}") + @Operation(summary = "获取标题", description = "根据contentId获取title字段的内容") + public CommonResult getTitle(@PathVariable("contentId") Long contentId) { + String title = cmsContentService.getTitle(contentId); + return CommonResult.success(title); + } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/controller/SkillGenController.java b/src/main/java/com/kexue/skills/controller/SkillGenController.java index 10368ea..46fcc55 100644 --- a/src/main/java/com/kexue/skills/controller/SkillGenController.java +++ b/src/main/java/com/kexue/skills/controller/SkillGenController.java @@ -11,6 +11,8 @@ import com.kexue.skills.entity.response.SkillResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + import javax.annotation.Resource; /** @@ -93,4 +95,24 @@ public class SkillGenController { public CommonResult uploadSkill(@RequestBody SkillUploadDto skillUploadDto) { return CommonResult.success(skillGenService.uploadSkill(skillUploadDto.getUrl())); } + + /** + * 上传本地技能压缩包V2 + * + * @param file 技能压缩包文件 + * @return 生成的技能内容 + */ + @PostMapping("/uploadSkillV2") + @Operation(summary = "上传本地技能压缩包V2", description = "上传本地zip或rar文件并生成技能") + public CommonResult uploadSkillV2( + @RequestParam("file") MultipartFile file) { + try { + byte[] fileBytes = file.getBytes(); + String fileName = file.getOriginalFilename(); + CmsContent cmsContent = skillGenService.uploadSkillV2(fileBytes, fileName); + return CommonResult.success(cmsContent); + } catch (Exception e) { + return CommonResult.failed("上传失败:" + e.getMessage()); + } + } } diff --git a/src/main/java/com/kexue/skills/controller/SysUserController.java b/src/main/java/com/kexue/skills/controller/SysUserController.java index 73863bc..72f2499 100644 --- a/src/main/java/com/kexue/skills/controller/SysUserController.java +++ b/src/main/java/com/kexue/skills/controller/SysUserController.java @@ -215,6 +215,13 @@ public class SysUserController { throw new BizException("请先登录认证后操作"); } + // 使用Sa-Token检查token是否有效 + try { + cn.dev33.satoken.stp.StpUtil.checkLogin(); + } catch (Exception e) { + throw new BizException("无效的token,请重新登录"); + } + // 从Redis缓存中获取LoginUser对象 String loginUserJson = (String)redissonClient.getBucket("loginUser:" + token).get(); if (loginUserJson == null || loginUserJson.isEmpty()) { diff --git a/src/main/java/com/kexue/skills/service/CmsContentService.java b/src/main/java/com/kexue/skills/service/CmsContentService.java index 7e68ab8..44c80c4 100644 --- a/src/main/java/com/kexue/skills/service/CmsContentService.java +++ b/src/main/java/com/kexue/skills/service/CmsContentService.java @@ -183,4 +183,12 @@ public interface CmsContentService extends BaseService { * @return content或contentEn字段的内容 */ String getContent(Long contentId, Integer languageType); + + /** + * 获取CmsContent的title字段内容 + * + * @param contentId 内容ID + * @return title字段的内容 + */ + String getTitle(Long contentId); } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/SkillGenService.java b/src/main/java/com/kexue/skills/service/SkillGenService.java index cad89f5..a8038e8 100644 --- a/src/main/java/com/kexue/skills/service/SkillGenService.java +++ b/src/main/java/com/kexue/skills/service/SkillGenService.java @@ -51,4 +51,13 @@ public interface SkillGenService { * @return 生成的技能内容 */ CmsContent uploadSkill(String skillUrl); + + /** + * 上传本地技能压缩包并生成技能V2 + * + * @param fileBytes 技能压缩包字节数组 + * @param fileName 文件名 + * @return 生成的技能内容 + */ + CmsContent uploadSkillV2(byte[] fileBytes, String fileName); } diff --git a/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java index 3000678..6aeabd1 100644 --- a/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java @@ -932,4 +932,14 @@ public class CmsContentServiceImpl implements CmsContentService { return cmsContent.getContent(); } } + + @Override + public String getTitle(Long contentId) { + Assert.notNull(contentId, "内容ID不能为空"); + CmsContent cmsContent = this.cmsContentMapper.queryById(contentId); + if (cmsContent == null) { + return null; + } + return cmsContent.getTitle(); + } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java index ed04c1e..8064039 100644 --- a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java @@ -26,10 +26,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.Resource; +import java.io.File; +import java.io.FileOutputStream; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; import java.util.stream.Collectors; /** @@ -415,4 +417,152 @@ public class SkillGenServiceImpl implements SkillGenService { return null; } + + @Override + public CmsContent uploadSkillV2(byte[] fileBytes, String fileName) { + log.info("上传本地技能压缩包请求: {}", fileName); + + try { + // 创建临时文件 + String fileExtension = ""; + if (fileName.contains(".")) { + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex != -1) { + fileExtension = fileName.substring(dotIndex); + } + } + File tempFile = File.createTempFile("skill", fileExtension); + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + fos.write(fileBytes); + } + + // 生成默认参数 + String author = StpUtil.getLoginIdAsString(); + // 从文件名中提取技能名称 + String defaultSkillName = fileName; + if (fileName.contains(".")) { + int dotIndex = fileName.lastIndexOf("."); + if (dotIndex != -1) { + defaultSkillName = fileName.substring(0, dotIndex); + } + } + + // 1. 提取压缩包中的skillMdText + Map skillInfo = com.kexue.skills.common.util.SkillZipParser.extractSkillMdText(tempFile.getAbsolutePath(), defaultSkillName); + String skillMdText = (String) skillInfo.get("skillMdText"); + + // 从数据库中读取cms_tag表的标签信息 + CmsTagDto tagDto = new CmsTagDto(); + tagDto.setDeleteFlag(0); + tagDto.setStatus(1); + List tags = cmsTagService.getList(tagDto); + + // 2. 解析skillMdText获取SkillGenRequest + SkillGenRequest request = null; + if (skillMdText != null && !skillMdText.isEmpty()) { + request = parseSkillMdText(skillMdText, tags); + } else { + // 如果没有找到md文件,使用默认值 + request = new SkillGenRequest(); + request.setName(defaultSkillName); + request.setDescription("未知"); + request.setIntroduce("未知"); + request.setTags(Arrays.asList("10001", "10002")); + } + + // 3. 使用SkillGenRequest中的信息生成yaml + String yamlContent = com.kexue.skills.common.util.SkillZipParser.generateYamlFromSkillInfo( + tempFile.getAbsolutePath(), + author, + request.getName(), + request.getDescription(), + request.getTags() + ); + log.info("解析出skill 压缩包内容:{}", yamlContent); + + // 4. 生成CmsContent对象 + CmsContent cmsContent = getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), ""); + List list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList(); + if (CollectionUtil.isNotEmpty(list)){ + cmsContent.setIcon(list.get(0).getIcon()); + } + // 删除临时文件 + tempFile.delete(); + + return cmsContent; + } catch (Exception e) { + log.error("上传本地技能压缩包失败: {}", e.getMessage(), e); + throw new BizException("上传本地技能压缩包失败:" + e.getMessage()); + } + } + + public SkillGenRequest parseSkillMdText(String skillMdText,List tags) { + String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; + + // 将标签名称拼接成逗号分隔的字符串 + StringBuilder tagsList = new StringBuilder(); + for (int i = 0; i < tags.size(); i++) { + CmsTag tag = tags.get(i); + tagsList.append(tag.getTagId()+"."+tag.getTagName()); + if (i < tags.size() - 1) { + tagsList.append(","); + } + } + + // 构建系统消息内容 + String systemContent = """ + 你是一位专业的AI技能包解析助手,需完成以下任务: + 1. 我会提供一个技能包压缩包SKILL.md文件的具体内容; + 2. 基于SKILL.md内容,提炼出: + - skill的核心标题(name); + - skill的核心描述(description); + - 详细功能介绍(introduce); + - 从指定标签列表中选取至少3个适配标签(tagList),标签范围:TAG_LIST; + - 选择标签的时候只需要输出对应的标签编号 + 请将最终结果以JSON格式输出,JSON结构必须包含:name(技能名称),description(技能描述)、introduce(功能介绍)、tagList(标签列表),无需额外说明,仅输出符合要求的JSON内容。 + """; + systemContent = systemContent.replace("TAG_LIST", tagsList.toString()); + + // 创建技能请求 + SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, skillMdText, 0.5, 8192, "json_object"); + + String deepseekResponse = ""; + try { + // 发送HTTP请求到DeepSeek API + deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null); + log.info("Deepseek API响应: {}", deepseekResponse); + + // 解析返回结果 + JSONObject responseJson = JSON.parseObject(deepseekResponse); + List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); + + if (choices != null && !choices.isEmpty()) { + // 获取最新的choice + JSONObject latestChoice = choices.get(0); + JSONObject message = latestChoice.getJSONObject("message"); + String content = message.getString("content"); + + // 解析content中的JSON + JSONObject skillJson = JSON.parseObject(content); + + // 构建技能生成请求 + SkillGenRequest request = new SkillGenRequest(); + request.setName(skillJson.getString("name")); + request.setDescription(skillJson.getString("description")); + request.setIntroduce(skillJson.getString("introduce")); + // 处理tagList可能不存在的情况 + if (skillJson.containsKey("tagList")) { + request.setTags(skillJson.getJSONArray("tagList").toJavaList(String.class)); + } else { + request.setTags(Arrays.asList("10001", "10002")); + } + return request; + } + } catch (Exception e) { + log.error("调用Deepseek API失败: {}", e.getMessage(), e); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); + } + + return null; + } }