|
|
|
|
@ -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;
|
|
|
|
|
}
|
|
|
|
|
}
|