feat(controller): 添加上传本地技能压缩包V2接口

- 在SkillGenController中新增uploadSkillV4方法,支持上传zip或rar格式技能包
- 新增CmsContent uploadSkillV4接口实现技能包解析和内容生成
- 集成SevenZipJBinding库支持rar格式解压
- 实现压缩包目录树结构解析功能
- 添加YAML内容生成和技能信息提取功能
- 完善异常处理和错误信息返回机制
This commit is contained in:
wangzhiwei 2026-04-10 15:47:34 +08:00
parent b548bfbc14
commit a5631caab3
6 changed files with 900 additions and 163 deletions

View File

@ -128,4 +128,24 @@ public class SkillGenController {
return CommonResult.success(cmsContent);
}
/**
* 上传本地技能压缩包V2
*
* @param file 技能压缩包文件
* @return 生成的技能内容
*/
@PostMapping("/uploadSkillV4")
@Operation(summary = "上传本地技能压缩包V2", description = "上传本地zip或rar文件并生成技能")
public CommonResult<CmsContent> uploadSkillV4(
@RequestParam("file") MultipartFile file) {
try {
byte[] fileBytes = file.getBytes();
String fileName = file.getOriginalFilename();
CmsContent cmsContent = skillGenService.uploadSkillV4(fileBytes, fileName);
return CommonResult.success(cmsContent);
} catch (Exception e) {
return CommonResult.failed("上传失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,53 @@
package com.kexue.skills.entity.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能包信息DTO
*
* @author AI技能生成助手
* @since 2026-04-10
*/
@Data
public class SkillPackageInfoDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 技能名称
*/
private String name;
/**
* 版本号
*/
private String version;
/**
* 技能描述
*/
private String description;
/**
* 作者
*/
private String author;
/**
* 创建日期
*/
private String created;
/**
* 标签列表
*/
private List<String> tags;
/**
* 目录结构
*/
private List<SkillStructureNodeDto> structure;
}

View File

@ -0,0 +1,53 @@
package com.kexue.skills.entity.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能包目录结构节点DTO
*
* @author AI技能生成助手
* @since 2026-04-10
*/
@Data
public class SkillStructureNodeDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点名称文件或目录名
*/
private String name;
/**
* 节点类型directory file
*/
private String type;
/**
* 父级路径
*/
private String path;
/**
* 格式dirmarkdownpython
*/
private String format;
/**
* 节点描述
*/
private String description;
/**
* 子节点列表目录类型使用
*/
private List<SkillStructureNodeDto> children;
/**
* 文件内容文件类型使用
*/
private String content;
}

View File

@ -69,5 +69,7 @@ public interface SkillGenService {
*/
CmsContent uploadSkillV2(byte[] fileBytes, String fileName);
CmsContent uploadSkillV4(byte[] fileBytes, String fileName);
CmsContent uploadSkillV3(String yamlContent);
}

View File

@ -11,6 +11,8 @@ import com.kexue.skills.config.GlmConfig;
import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.CmsTag;
import com.kexue.skills.entity.dto.CmsTagDto;
import com.kexue.skills.entity.dto.SkillPackageInfoDto;
import com.kexue.skills.entity.dto.SkillStructureNodeDto;
import com.kexue.skills.entity.request.SkillAnalyzeRequest;
import com.kexue.skills.entity.request.SkillGenRequest;
import com.kexue.skills.entity.request.SkillPreGenRequest;
@ -22,20 +24,42 @@ import com.kexue.skills.service.CmsTagService;
import com.kexue.skills.service.SkillGenService;
import com.kexue.skills.utils.EscapeCharacterUtils;
import com.kexue.skills.utils.YamlToMapUtil;
import com.kexue.skills.utils.YamlUtil;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.error.YAMLException;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* 技能生成服务实现
@ -80,7 +104,7 @@ public class SkillGenServiceImpl implements SkillGenService {
StringBuilder tagsList = new StringBuilder();
for (int i = 0; i < tags.size(); i++) {
CmsTag tag = tags.get(i);
tagsList.append(tag.getTagId()+"."+tag.getTagName());
tagsList.append(tag.getTagId() + "." + tag.getTagName());
if (i < tags.size() - 1) {
tagsList.append("");
}
@ -118,7 +142,7 @@ public class SkillGenServiceImpl implements SkillGenService {
}
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
return null;
@ -143,7 +167,7 @@ public class SkillGenServiceImpl implements SkillGenService {
StringBuilder tagsList = new StringBuilder();
for (int i = 0; i < tags.size(); i++) {
CmsTag tag = tags.get(i);
tagsList.append(tag.getTagId()+"."+tag.getTagName());
tagsList.append(tag.getTagId() + "." + tag.getTagName());
if (i < tags.size() - 1) {
tagsList.append("");
}
@ -193,7 +217,7 @@ public class SkillGenServiceImpl implements SkillGenService {
}
} catch (Exception e) {
log.error("调用API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ response);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + response);
}
return null;
}
@ -221,8 +245,8 @@ public class SkillGenServiceImpl implements SkillGenService {
String defaultIcon = "";
for (int i = 0; i < tags.size(); i++) {
CmsTag tag = tags.get(i);
if (tags1.contains(tag.getTagId()+"")) {
if (StringUtil.isEmpty(defaultIcon)){
if (tags1.contains(tag.getTagId() + "")) {
if (StringUtil.isEmpty(defaultIcon)) {
defaultIcon = tag.getIcon();
}
tagsList.append(tag.getTagName());
@ -324,7 +348,7 @@ public class SkillGenServiceImpl implements SkillGenService {
tagsList.toString(),
request.getRequirement()
);
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),systemContent,userContent,deepSeekConfig.getChat().getTemperature(), 8192,"text");
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, userContent, deepSeekConfig.getChat().getTemperature(), 8192, "text");
String deepseekResponse = "";
try {
// 发送HTTP请求到deepseek API
@ -332,10 +356,10 @@ public class SkillGenServiceImpl implements SkillGenService {
log.info("Deepseek API响应: {}", deepseekResponse);
JSONObject responseJson = JSON.parseObject(deepseekResponse);
String content = responseJson.getJSONArray("choices").toJavaList(JSONObject.class).get(0).getJSONObject("message").getString("content");
content = EscapeCharacterUtils.removeEscapeCharacters( content);//去除转义字符
CmsContent cmsContent = getCmsContent(request, content, StpUtil.getLoginIdAsLong(),defaultIcon);
content = EscapeCharacterUtils.removeEscapeCharacters(content);//去除转义字符
CmsContent cmsContent = getCmsContent(request, content, StpUtil.getLoginIdAsLong(), defaultIcon);
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList();
if (CollectionUtil.isNotEmpty( list)){
if (CollectionUtil.isNotEmpty(list)) {
cmsContent.setIcon(list.get(0).getIcon());
}
// 保存到数据库
@ -343,11 +367,11 @@ public class SkillGenServiceImpl implements SkillGenService {
return cmsContent;
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
}
private CmsContent getCmsContent(SkillGenRequest request, String CmsContent,Long userId,String defaultIcon){
private CmsContent getCmsContent(SkillGenRequest request, String CmsContent, Long userId, String defaultIcon) {
CmsContent cmsContent = new CmsContent();
cmsContent.setTitle(request.getName());
cmsContent.setDescription(request.getDescription());
@ -369,8 +393,8 @@ public class SkillGenServiceImpl implements SkillGenService {
cmsContent.setCreateTime(new Date());
cmsContent.setUpdateTime(new Date());
cmsContent.setContentType(1);
cmsContent.setCreateBy(userId+"");
cmsContent.setUpdateBy(userId+"");
cmsContent.setCreateBy(userId + "");
cmsContent.setUpdateBy(userId + "");
cmsContent.setDeleteFlag(0);
cmsContent.setIcon(defaultIcon);
cmsContent.setRequirement(request.getRequirement());
@ -413,11 +437,11 @@ public class SkillGenServiceImpl implements SkillGenService {
if (choices != null && !choices.isEmpty()) {
JSONObject latestChoice = choices.get(0);
JSONObject message = latestChoice.getJSONObject("message");
return EscapeCharacterUtils.removeEscapeCharacters( message.getString("content"));//去除转义字符
return EscapeCharacterUtils.removeEscapeCharacters(message.getString("content"));//去除转义字符
}
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
return null;
@ -443,11 +467,11 @@ public class SkillGenServiceImpl implements SkillGenService {
if (choices != null && !choices.isEmpty()) {
JSONObject latestChoice = choices.get(0);
JSONObject message = latestChoice.getJSONObject("message");
return EscapeCharacterUtils.removeEscapeCharacters( message.getString("content"));//去除转义字符
return EscapeCharacterUtils.removeEscapeCharacters(message.getString("content"));//去除转义字符
}
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
return null;
@ -468,7 +492,7 @@ public class SkillGenServiceImpl implements SkillGenService {
StringBuilder tagsList = new StringBuilder();
for (int i = 0; i < tags.size(); i++) {
CmsTag tag = tags.get(i);
tagsList.append(tag.getTagId()+"."+tag.getTagName());
tagsList.append(tag.getTagId() + "." + tag.getTagName());
if (i < tags.size() - 1) {
tagsList.append("");
}
@ -531,7 +555,7 @@ public class SkillGenServiceImpl implements SkillGenService {
// 生成CmsContent对象
CmsContent cmsContent = getCmsContent(request, skillJson.getString("content"), StpUtil.getLoginIdAsLong(), "");
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList();
if (CollectionUtil.isNotEmpty( list)){
if (CollectionUtil.isNotEmpty(list)) {
cmsContent.setIcon(list.get(0).getIcon());
}
// 保存到数据库
@ -540,7 +564,7 @@ public class SkillGenServiceImpl implements SkillGenService {
}
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
return null;
@ -620,7 +644,7 @@ public class SkillGenServiceImpl implements SkillGenService {
}
long finalL = l;
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == finalL).toList();
if (CollectionUtil.isNotEmpty(list)){
if (CollectionUtil.isNotEmpty(list)) {
cmsContent.setIcon(list.get(0).getIcon());
}
}
@ -642,7 +666,7 @@ public class SkillGenServiceImpl implements SkillGenService {
try {
Map<String, Object> yamlMap = YamlToMapUtil.yamlTextToMap(yamlContent);
Map<String, Object> packageMap = (Map<String, Object>)yamlMap.get("package");
Map<String, Object> packageMap = (Map<String, Object>) yamlMap.get("package");
String skillName = (String) packageMap.get("name");
String skillDescription = (String) packageMap.get("description");
List<?> tagList = (List<?>) packageMap.get("tags");
@ -655,11 +679,11 @@ public class SkillGenServiceImpl implements SkillGenService {
request.setIntroduce(genIntroduceByDescription(skillDescription));
return getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), skillIcon);
} catch (YAMLException e) {
throw new BizException("yaml解析失败:"+ e.getMessage());
throw new BizException("yaml解析失败:" + e.getMessage());
}
}
private String getDefaultIcon(String tagId){
private String getDefaultIcon(String tagId) {
CmsTagDto tagDto = new CmsTagDto();
tagDto.setDeleteFlag(0);
tagDto.setStatus(1);
@ -668,8 +692,8 @@ public class SkillGenServiceImpl implements SkillGenService {
String defaultIcon = "";
for (int i = 0; i < tagList.size(); i++) {
CmsTag tag = tagList.get(i);
if (tagId.contains(tag.getTagId()+"")) {
if (StringUtil.isEmpty(defaultIcon)){
if (tagId.contains(tag.getTagId() + "")) {
if (StringUtil.isEmpty(defaultIcon)) {
defaultIcon = tag.getIcon();
break;
}
@ -678,14 +702,14 @@ public class SkillGenServiceImpl implements SkillGenService {
return defaultIcon;
}
public SkillGenRequest parseSkillMdText(String skillMdText,List<CmsTag> tags) {
public SkillGenRequest parseSkillMdText(String skillMdText, List<CmsTag> 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());
tagsList.append(tag.getTagId() + "." + tag.getTagName());
if (i < tags.size() - 1) {
tagsList.append("");
}
@ -742,9 +766,576 @@ public class SkillGenServiceImpl implements SkillGenService {
}
} catch (Exception e) {
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse);
}
return null;
}
@Override
public CmsContent uploadSkillV4(byte[] fileBytes, String fileName) {
log.info("上传技能压缩包V4请求: {}", fileName);
try {
// 1. 从文件名提取技能名称
String skillName = extractSkillName(fileName);
// 2. 解析压缩包构建目录树结构
List<SkillStructureNodeDto> structureNodes = parseArchiveToStructure(fileBytes, fileName);
// 3. 从SKILL.md或README.md中提取描述信息如果存在
String description = extractDescriptionFromStructure(structureNodes);
if (description == null || description.isEmpty()) {
description = "AI生成的技能包";
}
// 4. 提取标签如果没有则使用默认标签
List<String> tags = extractTagsFromStructure(structureNodes);
if (tags == null || tags.isEmpty()) {
tags = Arrays.asList("通用");
}
// 5. 构建packageInfo
SkillPackageInfoDto packageInfo = new SkillPackageInfoDto();
packageInfo.setName(skillName);
packageInfo.setVersion("1.0.0");
packageInfo.setDescription(description);
packageInfo.setAuthor("AI技能生成助手");
packageInfo.setCreated(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
packageInfo.setTags(tags);
packageInfo.setStructure(structureNodes);
// 6. 生成YAML内容
String yamlContent = generateYamlContent(packageInfo);
log.info("生成的YAML内容:\n{}", yamlContent);
// 7. 构建SkillGenRequest
SkillGenRequest request = new SkillGenRequest();
request.setName(skillName);
request.setDescription(description);
// request.setIntroduce(genIntroduceByDescription(description));
request.setTags(tags);
// 8. 生成CmsContent
CmsContent cmsContent = getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), "");
// 9. 设置图标
setSkillIcon(cmsContent, tags);
cmsContentMapper.insert(cmsContent);
return cmsContent;
} catch (Exception e) {
log.error("上传技能压缩包V4失败: {}", e.getMessage(), e);
throw new BizException("上传技能压缩包V4失败:" + e.getMessage());
}
}
/**
* 解析压缩包为目录树结构
*/
private List<SkillStructureNodeDto> parseArchiveToStructure(byte[] fileBytes, String fileName) {
Map<String, byte[]> fileMap = new HashMap<>();
// 解压文件
if (fileName.toLowerCase().endsWith(".zip")) {
extractZipFiles(fileBytes, fileMap);
} else if (fileName.toLowerCase().endsWith(".rar")) {
extractRarFiles(fileBytes, fileMap);
} else {
throw new BizException("不支持的压缩包格式,仅支持 zip 或 rar");
}
// 构建目录树
return buildDirectoryTree(fileMap);
}
/**
* 构建目录树结构
*/
private List<SkillStructureNodeDto> buildDirectoryTree(Map<String, byte[]> fileMap) {
log.info("开始构建目录树,文件数量: {}", fileMap.size());
// 收集所有唯一的目录路径
Set<String> directorySet = new TreeSet<>();
for (String filePath : fileMap.keySet()) {
String normalizedPath = filePath.replace("\\", "/");
// 从文件路径提取所有父目录
int lastSlashIndex = normalizedPath.lastIndexOf("/");
while (lastSlashIndex > 0) {
String parentDir = normalizedPath.substring(0, lastSlashIndex);
directorySet.add(parentDir);
lastSlashIndex = parentDir.lastIndexOf("/");
}
}
log.info("检测到目录: {}", directorySet);
// 创建节点映射path -> node
Map<String, SkillStructureNodeDto> pathToNodeMap = new HashMap<>();
// 先创建所有目录节点
for (String dirPath : directorySet) {
String[] parts = dirPath.split("/");
String dirName = parts[parts.length - 1];
String parentPath = dirPath.contains("/") ? dirPath.substring(0, dirPath.lastIndexOf("/")) : "";
SkillStructureNodeDto dirNode = new SkillStructureNodeDto();
dirNode.setName(dirName);
dirNode.setType("directory");
dirNode.setPath(parentPath.isEmpty() ? "/" : parentPath);
dirNode.setFormat("dir");
dirNode.setDescription(dirName + " 目录");
dirNode.setChildren(new ArrayList<>());
pathToNodeMap.put(dirPath, dirNode);
log.debug("创建目录节点: name={}, path={}", dirName, dirNode.getPath());
}
// 将目录节点添加到父节点
List<SkillStructureNodeDto> rootChildren = new ArrayList<>();
for (Map.Entry<String, SkillStructureNodeDto> entry : pathToNodeMap.entrySet()) {
String dirPath = entry.getKey();
SkillStructureNodeDto node = entry.getValue();
String parentPath = dirPath.contains("/") ? dirPath.substring(0, dirPath.lastIndexOf("/")) : "";
if (parentPath.isEmpty()) {
// 一级目录直接添加到根节点
rootChildren.add(node);
} else {
// 找到父目录并添加
SkillStructureNodeDto parentNode = pathToNodeMap.get(parentPath);
if (parentNode != null && parentNode.getChildren() != null) {
parentNode.getChildren().add(node);
} else {
log.warn("找不到父目录: {}, 将{}添加到根节点", parentPath, dirPath);
rootChildren.add(node);
}
}
}
// 再处理所有文件
for (Map.Entry<String, byte[]> entry : fileMap.entrySet()) {
String filePath = entry.getKey().replace("\\", "/");
if (filePath.endsWith("/")) {
continue; // 跳过目录条目
}
int lastSlashIndex = filePath.lastIndexOf("/");
String fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath;
String parentPath = lastSlashIndex >= 0 ? filePath.substring(0, lastSlashIndex) : "";
SkillStructureNodeDto fileNode = new SkillStructureNodeDto();
fileNode.setName(fileName);
fileNode.setType("file");
fileNode.setPath(parentPath.isEmpty() ? "/" : parentPath);
fileNode.setFormat(getFileFormat(fileName));
fileNode.setDescription(fileName + " 文件");
// 读取文件内容
try {
String content = new String(entry.getValue(), StandardCharsets.UTF_8);
fileNode.setContent(content);
} catch (Exception e) {
log.warn("读取文件内容失败: {}", filePath, e);
fileNode.setContent("");
}
// 添加到父节点
if (parentPath.isEmpty()) {
// 根目录下的文件
rootChildren.add(fileNode);
} else {
SkillStructureNodeDto parentNode = pathToNodeMap.get(parentPath);
if (parentNode != null && parentNode.getChildren() != null) {
parentNode.getChildren().add(fileNode);
} else {
log.warn("找不到文件的父目录: {}, 将{}添加到根节点", parentPath, filePath);
rootChildren.add(fileNode);
}
}
log.debug("创建文件节点: name={}, path={}", fileName, fileNode.getPath());
}
// 删除不再强制生成skills.md保持压缩包原有结构
// 之前的代码ensureSkillsMdExists(rootChildren);
// 如果只有一个根目录节点将其children提升到根层级去掉最外层技能包目录
if (rootChildren.size() == 1 && "directory".equals(rootChildren.get(0).getType())) {
SkillStructureNodeDto rootDir = rootChildren.get(0);
String removedDirName = rootDir.getName();
log.info("检测到单个根目录节点: {}将其children提升到根层级", removedDirName);
// 获取其children作为新的根节点
List<SkillStructureNodeDto> newRootChildren = rootDir.getChildren();
if (newRootChildren != null && !newRootChildren.isEmpty()) {
rootChildren = newRootChildren;
// 递归更新所有节点的path去除被移除的目录名前缀
updatePathsAfterFlattening(rootChildren, removedDirName);
}
}
log.info("目录树构建完成,根节点数量: {}", rootChildren.size());
for (SkillStructureNodeDto node : rootChildren) {
log.info(" 根节点: {} ({})", node.getName(), node.getType());
}
return rootChildren;
}
/**
* 扁平化后递归更新所有节点的path
*/
private void updatePathsAfterFlattening(List<SkillStructureNodeDto> nodes, String removedDirPrefix) {
if (nodes == null || nodes.isEmpty()) {
return;
}
for (SkillStructureNodeDto node : nodes) {
String oldPath = node.getPath();
String newPath;
// 如果path是被移除的目录名改为 "/"
if (removedDirPrefix.equals(oldPath)) {
newPath = "/";
} else if (oldPath.startsWith(removedDirPrefix + "/")) {
// 如果被移除目录名是前缀去掉它
newPath = "/" + oldPath.substring(removedDirPrefix.length() + 1);
} else {
// 其他情况保持不变
newPath = oldPath;
}
node.setPath(newPath);
// 递归处理子节点
if ("directory".equals(node.getType()) && node.getChildren() != null) {
updatePathsAfterFlattening(node.getChildren(), removedDirPrefix);
}
}
}
/**
* 确保根目录下有skills.md文件
*/
private void ensureSkillsMdExists(List<SkillStructureNodeDto> rootChildren) {
boolean hasSkillsMd = false;
for (SkillStructureNodeDto node : rootChildren) {
if ("skills.md".equals(node.getName())) {
hasSkillsMd = true;
break;
}
}
if (!hasSkillsMd) {
SkillStructureNodeDto skillsMd = new SkillStructureNodeDto();
skillsMd.setName("skills.md");
skillsMd.setType("file");
skillsMd.setPath("/");
skillsMd.setFormat("markdown");
skillsMd.setDescription("技能说明文档");
skillsMd.setContent("# 技能说明\n\n请在此处填写技能的详细说明。\n");
rootChildren.add(skillsMd);
}
}
/**
* 从目录结构中提取描述
*/
private String extractDescriptionFromStructure(List<SkillStructureNodeDto> structureNodes) {
// 查找SKILL.md或README.md文件
for (SkillStructureNodeDto node : structureNodes) {
if ("file".equals(node.getType())) {
String lowerName = node.getName().toLowerCase();
if ((lowerName.equals("skill.md") || lowerName.equals("readme.md")) && node.getContent() != null) {
// 提取前200个字符作为描述
String content = node.getContent();
if (content.length() > 200) {
return content.substring(0, 200);
}
return content;
}
} else if ("directory".equals(node.getType()) && node.getChildren() != null) {
// 递归查找子目录
String desc = extractDescriptionFromStructure(node.getChildren());
if (desc != null && !desc.isEmpty()) {
return desc;
}
}
}
return null;
}
/**
* 从目录结构中提取标签
*/
private List<String> extractTagsFromStructure(List<SkillStructureNodeDto> structureNodes) {
// 这里可以解析SKILL.md中的front matter提取标签
// 暂时返回空列表由调用方设置默认值
return new ArrayList<>();
}
/**
* 生成YAML内容
*/
private String generateYamlContent(SkillPackageInfoDto packageInfo) {
StringBuilder yaml = new StringBuilder();
yaml.append("package:\n");
yaml.append(" name: ").append(quoteIfNecessary(packageInfo.getName())).append("\n");
yaml.append(" version: ").append(quoteIfNecessary(packageInfo.getVersion())).append("\n");
// description可能包含特殊字符使用块标量
String description = packageInfo.getDescription();
if (description != null && !description.isEmpty()) {
yaml.append(" description: |\n");
String[] lines = description.split("\n");
for (String line : lines) {
yaml.append(" ").append(line).append("\n");
}
} else {
yaml.append(" description: \"\"\n");
}
yaml.append(" author: ").append(quoteIfNecessary(packageInfo.getAuthor())).append("\n");
yaml.append(" created: ").append(quoteIfNecessary(packageInfo.getCreated())).append("\n");
// tags数组
yaml.append(" tags:\n");
if (packageInfo.getTags() != null) {
for (String tag : packageInfo.getTags()) {
yaml.append(" - ").append(quoteIfNecessary(tag)).append("\n");
}
}
// structure目录树
yaml.append(" structure:\n");
if (packageInfo.getStructure() != null) {
appendStructureNodes(yaml, packageInfo.getStructure(), 2);
}
return yaml.toString();
}
/**
* 判断是否需要引号需要则加双引号
*/
private String quoteIfNecessary(String value) {
if (value == null) {
return "\"\"";
}
// 如果包含特殊字符或可能产生歧义加双引号
if (value.contains(":") || value.contains("#") || value.contains("{") ||
value.contains("}") || value.contains("[") || value.contains("]") ||
value.contains(",") || value.contains("&") || value.contains("*") ||
value.contains("?") || value.contains("|") || value.contains("-") ||
value.contains("<") || value.contains(">") || value.contains("=") ||
value.contains("!") || value.contains("%") || value.contains("@") ||
value.contains("`") || value.startsWith("'") || value.startsWith(" ") ||
value.endsWith(" ")) {
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
return value;
}
/**
* 递归追加structure节点到YAML
*/
private void appendStructureNodes(StringBuilder yaml, List<SkillStructureNodeDto> nodes, int indentLevel) {
if (nodes == null || nodes.isEmpty()) {
return;
}
String indent = " ".repeat(indentLevel);
for (SkillStructureNodeDto node : nodes) {
yaml.append(indent).append("- name: ").append(node.getName()).append("\n");
yaml.append(indent).append(" type: ").append(node.getType()).append("\n");
yaml.append(indent).append(" path: ").append(node.getPath()).append("\n");
yaml.append(indent).append(" format: ").append(node.getFormat()).append("\n");
if ("file".equals(node.getType())) {
// 文件类型添加content
if (node.getContent() != null && !node.getContent().isEmpty()) {
String rawContent = node.getContent();
// 将Tab替换为4个空格YAML不允许Tab
rawContent = rawContent.replace("\t", " ");
// 分割成行
String[] lines = rawContent.split("\n", -1);
// 去除末尾的空行
int lastNonEmpty = lines.length - 1;
while (lastNonEmpty >= 0 && lines[lastNonEmpty].trim().isEmpty()) {
lastNonEmpty--;
}
if (lastNonEmpty >= 0) {
yaml.append(indent).append(" content: |");
// 如果原内容有尾随换行保留一个
if (lastNonEmpty < lines.length - 1) {
yaml.append("\n");
} else {
yaml.append("-\n"); // |- 表示去掉末尾换行
}
// 输出内容行每行添加统一的缩进
for (int i = 0; i <= lastNonEmpty; i++) {
yaml.append(indent).append(" ").append(lines[i]).append("\n");
}
}
}
} else if ("directory".equals(node.getType())) {
// 目录类型添加children
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
yaml.append(indent).append(" children:\n");
appendStructureNodes(yaml, node.getChildren(), indentLevel + 1);
} else {
yaml.append(indent).append(" children: []\n");
}
}
}
}
/**
* 转义YAML字符串中的特殊字符
*/
private String escapeYamlString(String str) {
if (str == null) {
return "";
}
// 转义双引号反斜杠等特殊字符
return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
/**
* 获取文件格式
*/
private String getFileFormat(String fileName) {
if (fileName == null) {
return "unknown";
}
String lowerName = fileName.toLowerCase();
if (lowerName.endsWith(".md")) {
return "markdown";
} else if (lowerName.endsWith(".py")) {
return "python";
} else if (lowerName.endsWith(".txt")) {
return "text";
} else if (lowerName.endsWith(".json")) {
return "json";
} else if (lowerName.endsWith(".yaml") || lowerName.endsWith(".yml")) {
return "yaml";
}
return "unknown";
}
/**
* 设置技能图标
*/
private void setSkillIcon(CmsContent cmsContent, List<String> tags) {
if (tags == null || tags.isEmpty()) {
return;
}
try {
String firstTagId = tags.get(0);
CmsTagDto tagDto = new CmsTagDto();
tagDto.setDeleteFlag(0);
tagDto.setStatus(1);
List<CmsTag> tagList = cmsTagService.getList(tagDto);
for (CmsTag tag : tagList) {
if (firstTagId.contains(tag.getTagId() + "")) {
cmsContent.setIcon(tag.getIcon());
break;
}
}
} catch (Exception e) {
log.warn("设置技能图标失败: {}", e.getMessage());
}
}
/**
* 解压 ZIP 文件
*/
private void extractZipFiles(byte[] fileBytes, Map<String, byte[]> fileMap) {
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory()) {
String entryName = entry.getName();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
fileMap.put(entryName, baos.toByteArray());
}
zis.closeEntry();
}
} catch (Exception e) {
log.error("解压 ZIP 文件失败: {}", e.getMessage(), e);
throw new BizException("解压 ZIP 文件失败:" + e.getMessage());
}
}
/**
* 解压 RAR 文件
*/
private void extractRarFiles(byte[] fileBytes, Map<String, byte[]> fileMap) {
File tempFile = null;
try {
// 创建临时文件
tempFile = File.createTempFile("skill_rar_", ".rar");
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
fos.write(fileBytes);
}
// 使用 SevenZipJBinding 解压 RAR
RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "r");
IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));
int itemCount = archive.getNumberOfItems();
for (int i = 0; i < itemCount; i++) {
Object isFolder = archive.getProperty(i, net.sf.sevenzipjbinding.PropID.IS_FOLDER);
if (!(isFolder instanceof Boolean) || !((Boolean) isFolder)) {
String path = (String) archive.getProperty(i, net.sf.sevenzipjbinding.PropID.PATH);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
archive.extractSlow(i, data -> {
try {
baos.write(data);
return data.length;
} catch (Exception e) {
throw new RuntimeException(e);
}
});
fileMap.put(path, baos.toByteArray());
}
}
archive.close();
randomAccessFile.close();
} catch (Exception e) {
log.error("解压 RAR 文件失败: {}", e.getMessage(), e);
throw new BizException("解压 RAR 文件失败:" + e.getMessage());
} finally {
if (tempFile != null && tempFile.exists()) {
tempFile.delete();
}
}
}
/**
* 从文件名提取技能名称
*/
private String extractSkillName(String fileName) {
if (fileName.contains(".")) {
int dotIndex = fileName.lastIndexOf(".");
return fileName.substring(0, dotIndex);
}
return fileName;
}
}

View File

@ -0,0 +1,18 @@
package com.kexue.skills.utils;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
public class YamlUtil {
/**
* 对象转 YAML 字符串
*/
public static String toYaml(Object obj) {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setPrettyFlow(true);
Yaml yaml = new Yaml(options);
return yaml.dump(obj);
}
}