feat(controller): 添加根据技能描述生成介绍和上传yaml内容功能

- 新增 genIntroduceByDescription 接口用于根据技能描述生成技能介绍
- 新增 uploadSkillV3 接口支持直接上传yaml内容生成技能
- 添加 YamlContentDto 数据传输对象
- 实现 genIntroduceByDescription 服务方法调用Deepseek API生成技能介绍
- 实现 uploadSkillV3 方法解析yaml内容并保存到数据库
- 添加 YamlToMapUtil 工具类用于yaml文件和字符串解析
- 修改数据库插入逻辑,添加默认图标获取功能
This commit is contained in:
wangzhiwei 2026-04-10 09:32:34 +08:00
parent fd0e1c893f
commit b548bfbc14
5 changed files with 283 additions and 4 deletions

View File

@ -1,8 +1,8 @@
package com.kexue.skills.controller; package com.kexue.skills.controller;
import com.alibaba.fastjson.JSONObject;
import com.kexue.skills.common.CommonResult; import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.CmsContent; import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.dto.YamlContentDto;
import com.kexue.skills.entity.request.GenIntroduceRequest; import com.kexue.skills.entity.request.GenIntroduceRequest;
import com.kexue.skills.entity.request.SkillGenRequest; import com.kexue.skills.entity.request.SkillGenRequest;
import com.kexue.skills.entity.request.SkillPreGenRequest; import com.kexue.skills.entity.request.SkillPreGenRequest;
@ -72,6 +72,18 @@ public class SkillGenController {
return CommonResult.success(skillGenService.genIntroduce(request.getContent())); return CommonResult.success(skillGenService.genIntroduce(request.getContent()));
} }
/**
* 根据技能描述生成技能介绍
*
* @param request 技能描述
* @return 技能介绍
*/
@PostMapping("/genIntroduceByDescription")
@Operation(summary = "根据技能描述生成技能介绍", description = "根据技能描述生成技能介绍")
public CommonResult<String> genIntroduceByDescription(@RequestBody GenIntroduceRequest request) {
return CommonResult.success(skillGenService.genIntroduceByDescription(request.getContent()));
}
/** /**
* 上传技能压缩包 * 上传技能压缩包
* *
@ -103,5 +115,17 @@ public class SkillGenController {
return CommonResult.failed("上传失败:" + e.getMessage()); return CommonResult.failed("上传失败:" + e.getMessage());
} }
} }
/**
* 上传本地技能压缩包V3 直接传入yamlContent
*
* @param yamlContentDto 技能压缩包文件
* @return 生成的技能内容
*/
@PostMapping("/uploadSkillV3")
@Operation(summary = "上传本地技能压缩包V3,直接传入yamlContent", description = "直接传入yamlContent")
public CommonResult<CmsContent> uploadSkillV3(@RequestBody YamlContentDto yamlContentDto) {
CmsContent cmsContent = skillGenService.uploadSkillV3(yamlContentDto.getYamlContent());
return CommonResult.success(cmsContent);
}
} }

View File

@ -0,0 +1,14 @@
package com.kexue.skills.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "yaml内容")
public class YamlContentDto implements Serializable {
@Schema(description = "yaml内容")
private String yamlContent;
}

View File

@ -44,6 +44,14 @@ public interface SkillGenService {
*/ */
String genIntroduce(String content); String genIntroduce(String content);
/**
* 根据技能描述生成技能介绍
*
* @param description 技能描述
* @return 技能介绍
*/
String genIntroduceByDescription(String description);
/** /**
* 上传技能压缩包并生成技能 * 上传技能压缩包并生成技能
* *
@ -60,5 +68,6 @@ public interface SkillGenService {
* @return 生成的技能内容 * @return 生成的技能内容
*/ */
CmsContent uploadSkillV2(byte[] fileBytes, String fileName); CmsContent uploadSkillV2(byte[] fileBytes, String fileName);
CmsContent uploadSkillV3(String yamlContent);
} }

View File

@ -21,10 +21,12 @@ import com.kexue.skills.mapper.CmsContentMapper;
import com.kexue.skills.service.CmsTagService; import com.kexue.skills.service.CmsTagService;
import com.kexue.skills.service.SkillGenService; import com.kexue.skills.service.SkillGenService;
import com.kexue.skills.utils.EscapeCharacterUtils; import com.kexue.skills.utils.EscapeCharacterUtils;
import com.kexue.skills.utils.YamlToMapUtil;
import jodd.util.StringUtil; import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.error.YAMLException;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.File; import java.io.File;
@ -420,6 +422,36 @@ public class SkillGenServiceImpl implements SkillGenService {
return null; return null;
} }
@Override
public String genIntroduceByDescription(String description) {
log.info("根据技能描述生成技能介绍请求: {}", description);
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
String systemContent = "你是一个专业的AI技能设计助手。我会给你提供一个skill的描述请你基于这个描述生成一段详细的技能介绍包括技能的作用、能够解决的问题、使用场景等输出一段完整的描述文本";
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, description, 0.3, 500, "text");
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<JSONObject> choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class);
if (choices != null && !choices.isEmpty()) {
JSONObject latestChoice = choices.get(0);
JSONObject message = latestChoice.getJSONObject("message");
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);
}
return null;
}
@Override @Override
public CmsContent uploadSkill(String skillUrl) { public CmsContent uploadSkill(String skillUrl) {
@ -593,7 +625,7 @@ public class SkillGenServiceImpl implements SkillGenService {
} }
} }
// 保存到数据库 // 保存到数据库
cmsContentMapper.insert(cmsContent); // cmsContentMapper.insert(cmsContent);
// 删除临时文件 // 删除临时文件
tempFile.delete(); tempFile.delete();
@ -604,6 +636,48 @@ public class SkillGenServiceImpl implements SkillGenService {
} }
} }
@Override
public CmsContent uploadSkillV3(String yamlContent) {
log.info("uploadSkillV3 上传技能压缩包请求: {}", yamlContent);
try {
Map<String, Object> yamlMap = YamlToMapUtil.yamlTextToMap(yamlContent);
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");
List<String> tagsStrList = tagList.stream().map(String::valueOf).toList();
String skillIcon = getDefaultIcon(tagsStrList.get(0));
SkillGenRequest request = new SkillGenRequest();
request.setName(skillName);
request.setDescription(skillDescription);
request.setTags(tagsStrList);
request.setIntroduce(genIntroduceByDescription(skillDescription));
return getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), skillIcon);
} catch (YAMLException e) {
throw new BizException("yaml解析失败:"+ e.getMessage());
}
}
private String getDefaultIcon(String tagId){
CmsTagDto tagDto = new CmsTagDto();
tagDto.setDeleteFlag(0);
tagDto.setStatus(1);
List<CmsTag> tagList = cmsTagService.getList(tagDto);
String defaultIcon = "";
for (int i = 0; i < tagList.size(); i++) {
CmsTag tag = tagList.get(i);
if (tagId.contains(tag.getTagId()+"")) {
if (StringUtil.isEmpty(defaultIcon)){
defaultIcon = tag.getIcon();
break;
}
}
}
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"; String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";

View File

@ -0,0 +1,158 @@
package com.kexue.skills.utils;
import org.apache.commons.io.FileUtils;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.error.YAMLException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* 通用 YAML 解析工具YAML 文件/文本 Map支持嵌套结构
*/
public class YamlToMapUtil {
// 初始化 YAML 解析器线程安全可全局复用
private static final Yaml YAML_PARSER = new Yaml();
/**
* 1. 读取 YAML 文件解析成 Map支持嵌套结构
* @param yamlFilePath YAML 文件路径 "D:/config.yaml" "classpath:config.yaml"
* @return 嵌套 Mapkey: 字段名value: 字段值嵌套结构保留
* @throws IOException 文件不存在/读取失败
* @throws YAMLException YAML 格式错误
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> yamlFileToMap(String yamlFilePath) throws IOException, YAMLException {
// 处理 classpath 下的文件 resources/config.yaml
if (yamlFilePath.startsWith("classpath:")) {
String path = yamlFilePath.replace("classpath:", "");
// 通过类加载器获取输入流避免路径问题
try (InputStream inputStream = YamlToMapUtil.class.getClassLoader().getResourceAsStream(path)) {
if (inputStream == null) {
throw new IOException("Classpath 下未找到 YAML 文件:" + path);
}
// 解析 YAML MapSnakeYAML 自动识别结构
return YAML_PARSER.loadAs(inputStream, Map.class);
}
}
// 处理本地绝对路径文件 D:/config.yaml
File yamlFile = new File(yamlFilePath);
if (!yamlFile.exists()) {
throw new IOException("YAML 文件不存在:" + yamlFilePath);
}
// 读取文件内容并解析
String yamlContent = FileUtils.readFileToString(yamlFile, StandardCharsets.UTF_8);
return yamlTextToMap(yamlContent);
}
/**
* 2. 解析 YAML 字符串生成 Map支持嵌套结构
* @param yamlText YAML 字符串 "name: test\nage: 18"
* @return 嵌套 Map
* @throws YAMLException YAML 格式错误
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> yamlTextToMap(String yamlText) throws YAMLException {
if (yamlText == null || yamlText.trim().isEmpty()) {
return new LinkedHashMap<>(); // 返回空 Map避免空指针
}
// 解析 YAML 文本SnakeYAML 会自动转换类型数字Integer/Long布尔Boolean
Object result = YAML_PARSER.load(yamlText);
// YAML 是纯数组 "- a\n- b"返回 key "list" Map否则直接返回 Map
if (result instanceof Map) {
return (Map<String, Object>) result;
} else if (result instanceof Iterable) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("list", result);
return map;
} else {
// 纯单个值 "test"返回 key "value" Map
Map<String, Object> map = new LinkedHashMap<>();
map.put("value", result);
return map;
}
}
/**
* 3. 可选工具将嵌套 Map 扁平化 a.b.c: 123方便直接取值
* @param nestedMap 嵌套 Map来自上述两个方法的返回值
* @return 扁平化 Mapkey: 拼接路径value: 原始值
*/
public static Map<String, Object> flattenMap(Map<String, Object> nestedMap) {
Map<String, Object> flatMap = new LinkedHashMap<>();
flattenMapRecursive(nestedMap, "", flatMap);
return flatMap;
}
/**
* 递归处理嵌套 Map生成扁平化 key私有辅助方法
*/
@SuppressWarnings("unchecked")
private static void flattenMapRecursive(Map<String, Object> map, String parentKey, Map<String, Object> flatMap) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
String currentKey = parentKey.isEmpty() ? entry.getKey() : parentKey + "." + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
// 嵌套 Map递归处理
flattenMapRecursive((Map<String, Object>) value, currentKey, flatMap);
} else if (value instanceof Iterable && !(value instanceof String)) {
// 数组/列表 Listkey 加索引 list.0, list.1
int index = 0;
for (Object item : (Iterable<?>) value) {
String listKey = currentKey + "." + index;
if (item instanceof Map) {
flattenMapRecursive((Map<String, Object>) item, listKey, flatMap);
} else {
flatMap.put(listKey, item);
}
index++;
}
} else {
// 基本类型字符串数字布尔直接放入扁平化 Map
flatMap.put(currentKey, value);
}
}
}
// ==================== 测试示例 ====================
public static void main(String[] args) {
try {
// 示例1解析 YAML 文件classpath 下的 config.yaml
Map<String, Object> fileMap = yamlFileToMap("classpath:config.yaml");
System.out.println("【嵌套 Map文件解析" + fileMap);
// 示例2解析 YAML 字符串含嵌套结构
String yamlText = """
name: "ppt_gen_direct"
description: "Create PPT"
settings:
timeout: 300
enable: true
colors:
- red
- blue
""";
Map<String, Object> textMap = yamlTextToMap(yamlText);
System.out.println("【嵌套 Map文本解析" + textMap);
// 示例3扁平化 Map方便取值
Map<String, Object> flatMap = flattenMap(textMap);
System.out.println("【扁平化 Map】" + flatMap);
// 直接通过拼接 key 取值
System.out.println("settings.timeout = " + flatMap.get("settings.timeout"));
System.out.println("settings.colors.0 = " + flatMap.get("settings.colors.0"));
} catch (Exception e) {
e.printStackTrace();
}
}
}