feat(controller): 添加根据技能描述生成介绍和上传yaml内容功能
- 新增 genIntroduceByDescription 接口用于根据技能描述生成技能介绍 - 新增 uploadSkillV3 接口支持直接上传yaml内容生成技能 - 添加 YamlContentDto 数据传输对象 - 实现 genIntroduceByDescription 服务方法调用Deepseek API生成技能介绍 - 实现 uploadSkillV3 方法解析yaml内容并保存到数据库 - 添加 YamlToMapUtil 工具类用于yaml文件和字符串解析 - 修改数据库插入逻辑,添加默认图标获取功能
This commit is contained in:
parent
fd0e1c893f
commit
b548bfbc14
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 嵌套 Map(key: 字段名,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 为 Map(SnakeYAML 自动识别结构)
|
||||||
|
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 扁平化 Map(key: 拼接路径,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)) {
|
||||||
|
// 数组/列表(如 List),key 加索引(如 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue