diff --git a/src/main/java/com/kexue/skills/controller/SkillGenController.java b/src/main/java/com/kexue/skills/controller/SkillGenController.java index d0cbd48..a241179 100644 --- a/src/main/java/com/kexue/skills/controller/SkillGenController.java +++ b/src/main/java/com/kexue/skills/controller/SkillGenController.java @@ -1,11 +1,19 @@ package com.kexue.skills.controller; import com.kexue.skills.common.CommonResult; +import com.kexue.skills.entity.request.SkillRequest; import com.kexue.skills.entity.response.SkillResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + import javax.annotation.Resource; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; /** * 技能生成控制器 @@ -13,6 +21,7 @@ import javax.annotation.Resource; * @author 维哥 * @since 2026-01-28 */ +@Slf4j @RestController @RequestMapping("api/skillGen") @Tag(name = "技能生成 Api") @@ -23,17 +32,65 @@ public class SkillGenController { private com.kexue.skills.service.SkillGenService skillGenService; /** - * 生成技能 + * 预生成skill,产生相关的描述信息 * * @param request 生成请求 * @return 生成结果 */ - @PostMapping("/generate") - @Operation(summary = "生成技能", description = "生成技能") - public CommonResult generate(@RequestBody com.kexue.skills.entity.request.SkillGenRequest request) { - return CommonResult.success(skillGenService.generateSkill(request)); + @PostMapping("/preGenerate") + @Operation(summary = "预生成技能", description = "预生成技能") + public CommonResult preGenerate(com.kexue.skills.entity.request.SkillGenRequest request, + @RequestPart(value = "file", required = false) MultipartFile file) { + try { + // 如果上传了文件,读取文件内容 + if (file != null && !file.isEmpty()) { + String fileContent = readTextFile(file); + + // 将文件内容设置到请求对象中(需要修改 SkillGenRequest) +// request.setFileContent(fileContent); +// request.setFileName(file.getOriginalFilename()); +// request.setFileSize(file.getSize()); +// request.setFileType(file.getContentType()); + + log.info("文件上传成功: {}, 大小: {} bytes, 类型: {}", + file.getOriginalFilename(), file.getSize(), file.getContentType()); + return CommonResult.success(skillGenService.preGenerateSkill(request,fileContent)); + } + + return CommonResult.success(skillGenService.preGenerateSkill(request,null)); + + } catch (Exception e) { + log.error("处理文件上传失败", e); + return CommonResult.failed("文件处理失败: " + e.getMessage()); + } } + /** + * 读取文本文件内容为字符串 + * @param file 上传的文件 + * @return 文件内容字符串 + */ + private String readTextFile(MultipartFile file) throws IOException { + // 使用 Apache Commons IO + try (InputStream inputStream = file.getInputStream()) { + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } + } + + + + /** + * 生成技能 + * + * @param + * @return 分析结果 + */ + @PostMapping("/generate") + @Operation(summary = "生成技能", description = "生成技能") + public CommonResult generate(@RequestBody SkillRequest request) { + Integer skillId=request.getSkillId(); + return CommonResult.success(skillGenService.generateSkill(skillId)); + } /** * 分析技能 * diff --git a/src/main/java/com/kexue/skills/entity/CmsContent.java b/src/main/java/com/kexue/skills/entity/CmsContent.java index 47f72ef..47b62f7 100644 --- a/src/main/java/com/kexue/skills/entity/CmsContent.java +++ b/src/main/java/com/kexue/skills/entity/CmsContent.java @@ -132,6 +132,17 @@ public class CmsContent extends BaseEntity implements Serializable { @Schema(description ="父分类ID") private Long parentCategoryId; + @Schema(description ="用户上传的文件内容") + private String userUpload; + + @Schema(description ="标签列表,逗号分隔") + private String tagIds; + + @Schema(description ="技能的文件地址") + private String skillPath; + + @Schema(description ="技能文件内容") + private String skillContent; // 用于接收前端发送的分类ID数组 @JsonProperty("categoryIds") public void setCategoryIdsFromArray(List categoryIdList) { diff --git a/src/main/java/com/kexue/skills/entity/dto/CmsContentDto.java b/src/main/java/com/kexue/skills/entity/dto/CmsContentDto.java index 2589883..a3eeec0 100644 --- a/src/main/java/com/kexue/skills/entity/dto/CmsContentDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/CmsContentDto.java @@ -44,4 +44,7 @@ public class CmsContentDto extends BaseQueryDto { private Long parentCategoryId; + private String userLoad; + + private String tagIds; } diff --git a/src/main/java/com/kexue/skills/entity/request/SkillRequest.java b/src/main/java/com/kexue/skills/entity/request/SkillRequest.java index 587bd8f..1b560fe 100644 --- a/src/main/java/com/kexue/skills/entity/request/SkillRequest.java +++ b/src/main/java/com/kexue/skills/entity/request/SkillRequest.java @@ -19,6 +19,8 @@ public class SkillRequest implements Serializable { private double temperature; private int max_tokens; private ResponseFormat response_format; + private int skillId; + private int contentId; public SkillRequest(boolean useDefaultSettings) { if (useDefaultSettings) { @@ -52,7 +54,7 @@ public class SkillRequest implements Serializable { systemMessage.setRole("system"); systemMessage.setContent("你是一个专业的AI技能设计助手。请严格按照指定的JSON格式输出,仅包含要求的字段,以中文形式返回。"); this.messages.add(systemMessage); - + //获取系统存在的标签 Message userMessage = new Message(); userMessage.setRole("user"); userMessage.setContent("主题:"+ prompt +"。请根据agent skills撰写规范帮我生成这个skill的名称、描述,并从以下标签列表中选择一个或者多个标签:\"软件开发,系统集成,网络工程,云计算,大数据,人工智能,物联网,区块链,信息安全,运维服务,测试认证,IT 咨询,外包服务,电商技术,移动开发,前端开发,后端开发,全栈开发,数据库管理\"。输出json格式,仅输出以上所提到的名称、描述、标签,节点名称分别为name、description、tags,节点内容以中文形式返回。"); @@ -65,6 +67,53 @@ public class SkillRequest implements Serializable { this.response_format.setType("json_object"); } } +// String systemPrompt = """ +// 请严格按照以下格式输出信息: +// +// ## 标题 ## +// [这里填写标题] +// +// ## 关键点 ## +// - 第一点 +// - 第二点 +// - 第三点 +// +// ## 详细说明 ## +// [这里填写详细说明] +// +// ## 建议 ## +// [这里填写建议] +// +// 不要添加任何其他内容,严格按照上述格式输出。 +// """; + + public SkillRequest(boolean useDefaultSettings, String model, Double temperature, Integer maxTokens,String prompt,String description,String tags,String userUpload) { + String systemPrompt2="你是一个专业的AI技能设计助手。请基于用户提供的Skill名称、描述、标签,生成完整的skills.md文档内容,仅输出skills.md本体内容,无需其他额外说明。"; + if (useDefaultSettings) { + this.model = model; + this.messages = new ArrayList<>(); + + Message systemMessage = new Message(); + systemMessage.setRole("system"); + systemMessage.setContent(systemPrompt2); + this.messages.add(systemMessage); + + Message userMessage = new Message(); + userMessage.setRole("user"); + if(userUpload!=null&&!userUpload.equals("")){ + userMessage.setContent("请根据以下Skill信息生成skills.md文档内容:Skill名称:"+prompt+"Skill描述:"+description+"Skill标签:"+tags+"。请仅输出skills.md本体内容,无需其他额外说明。内容的生成需要参考如下内容:"+userUpload); + }else{ + userMessage.setContent("请根据以下Skill信息生成skills.md文档内容:Skill名称:"+prompt+"Skill描述:"+description+"Skill标签:"+tags+"。请仅输出skills.md本体内容,无需其他额外说明。"); + } + this.messages.add(userMessage); + + this.temperature = temperature; + this.max_tokens = maxTokens; + +// this.response_format = new ResponseFormat(); +// this.response_format.setType("json_object"); + } + } public static SkillRequest createDefault() { return new SkillRequest(true); diff --git a/src/main/java/com/kexue/skills/entity/response/SkillResponse.java b/src/main/java/com/kexue/skills/entity/response/SkillResponse.java index d9f55dc..03b495c 100644 --- a/src/main/java/com/kexue/skills/entity/response/SkillResponse.java +++ b/src/main/java/com/kexue/skills/entity/response/SkillResponse.java @@ -1,5 +1,6 @@ package com.kexue.skills.entity.response; +import com.kexue.skills.entity.CmsContent; import lombok.Data; import java.io.Serializable; @@ -12,4 +13,6 @@ public class SkillResponse implements Serializable { private String name; private String description; private List tags; + private Long skillId; + private List cmsContents; } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/SkillGenService.java b/src/main/java/com/kexue/skills/service/SkillGenService.java index 7b3c205..d06e19d 100644 --- a/src/main/java/com/kexue/skills/service/SkillGenService.java +++ b/src/main/java/com/kexue/skills/service/SkillGenService.java @@ -13,12 +13,16 @@ import com.kexue.skills.entity.response.SkillResponse; public interface SkillGenService { /** - * 生成技能 + * 预生成技能 * * @param request 生成请求 + * @param fileContent 上传的文件内容 * @return 生成结果 */ - SkillResponse generateSkill(SkillGenRequest request); + SkillResponse preGenerateSkill(SkillGenRequest request,String fileContent); + + + SkillResponse generateSkill(Integer skillId); /** * 分析技能 diff --git a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java index aa24312..944d78d 100644 --- a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java @@ -4,16 +4,29 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.kexue.skills.common.util.HttpUtil; import com.kexue.skills.config.DeepSeekConfig; +import com.kexue.skills.entity.CmsContent; +import com.kexue.skills.entity.CmsTag; +import com.kexue.skills.entity.dto.CmsContentDto; +import com.kexue.skills.entity.dto.CmsTagDto; import com.kexue.skills.entity.request.SkillAnalyzeRequest; import com.kexue.skills.entity.request.SkillGenRequest; import com.kexue.skills.entity.request.SkillRequest; import com.kexue.skills.entity.response.SkillResponse; +import com.kexue.skills.mapper.CmsContentMapper; +import com.kexue.skills.service.CmsContentService; +import com.kexue.skills.service.CmsTagService; import com.kexue.skills.service.SkillGenService; +import com.kexue.skills.utils.FileManager; +import com.kexue.skills.utils.MarkdownProcessor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * 技能生成服务实现 @@ -28,6 +41,10 @@ public class SkillGenServiceImpl implements SkillGenService { @Autowired private DeepSeekConfig deepSeekConfig; + @Resource + private CmsContentService cmsContentService; + @Resource + private CmsTagService cmsTagService; /** * 生成技能 * @@ -35,8 +52,10 @@ public class SkillGenServiceImpl implements SkillGenService { * @return 生成结果 */ @Override - public SkillResponse generateSkill(SkillGenRequest request) { - log.info("生成技能请求: {}", request); + public SkillResponse preGenerateSkill(SkillGenRequest request,String fileContent) { + log.info("预生成技能请求: {}", request); + // List tags = cmsTagService.getList(new CmsTagDto()); + String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), deepSeekConfig.getChat().getTemperature(), deepSeekConfig.getChat().getMaxTokens(),request.getPrompt()); @@ -61,9 +80,57 @@ public class SkillGenServiceImpl implements SkillGenService { SkillResponse skillResponse = new SkillResponse(); skillResponse.setName(skillJson.getString("name")); skillResponse.setDescription(skillJson.getString("description")); - skillResponse.setTags(skillJson.getJSONArray("tags").toJavaList(String.class)); - + final List tags = skillJson.getJSONArray("tags").toJavaList(String.class); + //tags需要解析出id +// List targetTagIds = new ArrayList<>(); +// List resultContents =new ArrayList<>(); +// for (String tag : tags) { +// CmsTagDto tagDto = new CmsTagDto(); +// tagDto.setTagName(tag); +// List list = cmsTagService.getList(tagDto); +// if (list != null && !list.isEmpty()) { +// targetTagIds.add(list.get(0).getTagId()); +// } +// } + skillResponse.setTags(tags); log.info("解析技能响应: {}", skillResponse); + //skill需要记录到数据库? + CmsContent cmsContent = new CmsContent(); + cmsContent.setTitle(skillJson.getString("name")); + cmsContent.setSummary(skillJson.getString("description")); + cmsContent.setContentType(1); + cmsContent.setContent(skillJson.getString("content")); + cmsContent.setUserUpload(fileContent); + if(!tags.isEmpty()){ + String result=tags.stream() + //.map(String::valueOf) + .collect(Collectors.joining(",")); + cmsContent.setTagIds(result); + }else{ + cmsContent.setTagIds("通用技能,"); + } + //记录到数据库 + final CmsContent insert = cmsContentService.insert(cmsContent); + //根据tags在去匹配skill,作为推荐,推荐已经发布的skill + List resultContents =new ArrayList<>(); + CmsContentDto cmsContentDto = new CmsContentDto(); + cmsContentDto.setPublishStatus(2); + final List list = cmsContentService.getList(cmsContentDto); + for (CmsContent c:list) { + List currentTags = Arrays.stream(c.getTagIds().split(",")) + //.map(Long::valueOf) + .toList(); + if(tags.stream().anyMatch(currentTags::contains)) { + //存在交集,需要被推荐 + resultContents.add(c); + } + } + if(resultContents.isEmpty()&& !list.isEmpty()) { + //如果没匹配到,就先给第一个 + resultContents.add(list.get(0)); + } + skillResponse.setSkillId(insert.getContentId()); + skillResponse.setCmsContents(resultContents); return skillResponse; } } catch (Exception e) { @@ -73,6 +140,63 @@ public class SkillGenServiceImpl implements SkillGenService { return null; } + @Override + public SkillResponse generateSkill(Integer skillId) { + //根据skillId获取用户预创建时的信息,以此来生成skill文档 + final CmsContent cmsContent = cmsContentService.queryById(Long.valueOf(skillId)); + String name = cmsContent.getTitle(); + String description = cmsContent.getSummary(); + String userUpload = cmsContent.getUserUpload(); + String tags=cmsContent.getTagIds(); + + + log.info("生成技能请求: {}", skillId); + String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; + SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), + deepSeekConfig.getChat().getTemperature(), deepSeekConfig.getChat().getMaxTokens(),name,description,tags,userUpload); + try { + // 发送HTTP请求到deepseek API + String deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null); + log.info("Deepseek API响应: {}", deepseekResponse); + + // 解析deepseek返回结果 + JSONObject responseJson = JSON.parseObject(deepseekResponse); + List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); +// + if (choices != null && !choices.isEmpty()) { + // 获取最新的choice(这里是第一个,因为只有一个) + JSONObject latestChoice = choices.get(0); + JSONObject message = latestChoice.getJSONObject("message"); + String content = message.getString("content"); + log.info("输出结果:{}",content); + FileManager manager = new FileManager(); + FileManager.FileInfo fileInfo = manager.processAndWrite(content, name); + String storedFilePath = ""; + if (fileInfo != null) { + log.info("文件信息: " + fileInfo); + // 可以将文件路径存储到数据库或其他地方 + storedFilePath = fileInfo.getFilePath(); + log.info("存储的文件路径: " + storedFilePath); + } + CmsContent c = new CmsContent(); + c.setSkillPath(storedFilePath); + c.setSkillContent(content); + c.setContentId(Long.valueOf(skillId)); + cmsContentService.update(c); + // 解析content中的JSON + SkillResponse skillResponse = new SkillResponse(); + skillResponse.setDescription(MarkdownProcessor.unescapeString(content)); + skillResponse.setSkillId(Long.valueOf(skillId)); + return skillResponse; + + } + } catch (Exception e) { + log.error("调用Deepseek API失败: {}", e.getMessage(), e); + } + + + return null; + } /** diff --git a/src/main/java/com/kexue/skills/utils/FileManager.java b/src/main/java/com/kexue/skills/utils/FileManager.java new file mode 100644 index 0000000..fc3a362 --- /dev/null +++ b/src/main/java/com/kexue/skills/utils/FileManager.java @@ -0,0 +1,107 @@ +package com.kexue.skills.utils; + +import java.io.*; +import java.nio.file.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public class FileManager { + private String outputDirectory; + private String fileExtension; + + public FileManager() { + this.outputDirectory = "skill_output"; + this.fileExtension = ".md"; + } + + public FileManager(String outputDirectory, String fileExtension) { + this.outputDirectory = outputDirectory; + this.fileExtension = fileExtension; + } + + // 处理内容并写入文件 + public FileInfo processAndWrite(String content, String customFileName) { + try { + // 生成文件名 + String fileName = generateFileName(customFileName); + + // 写入文件 + String filePath = writeToFile(content, fileName); + + // 返回文件信息 + return new FileInfo(filePath, fileName); + + } catch (IOException e) { + System.err.println("写入文件失败: " + e.getMessage()); + return null; + } + } + + private List parseTagIds(String tagIds) { + if (tagIds == null || tagIds.trim().isEmpty()) { + return Collections.emptyList(); + } + + return Arrays.stream(tagIds.split(",")) + .map(String::trim) + .filter(tag -> !tag.isEmpty()) + .toList(); + } + + private String generateFileName(String customName) { + if (customName != null && !customName.trim().isEmpty()) { + return customName.endsWith(fileExtension) ? customName : customName + fileExtension; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); + String timestamp = LocalDateTime.now().format(formatter); + return "skills_" + timestamp + fileExtension; + } + + + + private String writeToFile(String content, String fileName) throws IOException { + // 创建输出目录 + Path dirPath = Paths.get(outputDirectory); + if (!Files.exists(dirPath)) { + Files.createDirectories(dirPath); + } + + // 构建完整文件路径 + Path filePath = dirPath.resolve(fileName); + + // 写入文件 + Files.writeString( + filePath, + content, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE + ); + + return filePath.toAbsolutePath().toString(); + } + + // 文件信息类 + public static class FileInfo { + private final String filePath; + private final String fileName; + + public FileInfo(String filePath, String fileName) { + this.filePath = filePath; + this.fileName = fileName; + } + + // getters + public String getFilePath() { return filePath; } + public String getFileName() { return fileName; } + + + @Override + public String toString() { + return String.format("文件: %s, 路径: %s", + fileName, filePath); + } + } +} diff --git a/src/main/java/com/kexue/skills/utils/MarkdownProcessor.java b/src/main/java/com/kexue/skills/utils/MarkdownProcessor.java new file mode 100644 index 0000000..9b2a3f8 --- /dev/null +++ b/src/main/java/com/kexue/skills/utils/MarkdownProcessor.java @@ -0,0 +1,18 @@ +package com.kexue.skills.utils; + +import org.apache.commons.lang3.StringEscapeUtils; + +public class MarkdownProcessor { + + public static String unescapeString(String escapedString) { + // 将 \n 转换为真正的换行符 + return escapedString.replace("\\n", "\n") + .replace("\\t", "\t") + .replace("\\\"", "\""); + } + + public static String convertToMarkdown(String escapedString) { + // 使用Apache Commons Text库 + return StringEscapeUtils.unescapeJava(escapedString); + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 36e5486..a693798 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -2,7 +2,7 @@ - + @@ -13,10 +13,10 @@ - ${LOG_HOME}/hyxp-portal.log + ${LOG_HOME}/skill.log - ${LOG_HOME}/hyxp-portal.%d{yyyy-MM-dd}.log + ${LOG_HOME}/skill.%d{yyyy-MM-dd}.log 30 diff --git a/src/main/resources/mapper/CmsContentMapper.xml b/src/main/resources/mapper/CmsContentMapper.xml index 625c176..5a4b599 100644 --- a/src/main/resources/mapper/CmsContentMapper.xml +++ b/src/main/resources/mapper/CmsContentMapper.xml @@ -118,7 +118,7 @@ select content_id, title, subtitle, parent_category_id, content_type, category_ids, summary, content, cover_image, author_id, author_name, reviewer_id, reviewer_name, audit_status, audit_comment, publish_status, publish_time, - view_count, like_count, comment_count, sort, is_paid, price, required_points, support_points_pay, is_official, share_count, file_url, icon, background, create_time, update_time, create_by, update_by, delete_flag + view_count, like_count, comment_count, sort, is_paid, price, required_points, support_points_pay, is_official, share_count, file_url, icon, background, create_time, update_time, create_by, update_by, delete_flag,user_upload,tag_ids from cms_content @@ -161,6 +161,12 @@ and parent_category_id = #{parentCategoryId} + + and userLoad = #{userLoad} + + + and tagIds = #{tagIds} + order by sort asc, create_time desc @@ -168,11 +174,13 @@ insert into cms_content(title, subtitle, parent_category_id, content_type, category_ids, summary, content, cover_image, author_id, author_name, - reviewer_id, reviewer_name, audit_status, audit_comment, publish_status, publish_time, - view_count, like_count, comment_count, sort, is_paid, price, required_points, support_points_pay, is_official, share_count, file_url, icon, background, create_time, update_time, create_by, update_by, delete_flag) + reviewer_id, reviewer_name, audit_status, audit_comment, publish_status, publish_time, + view_count, like_count, comment_count, sort, is_paid, price, required_points, support_points_pay, is_official, share_count, file_url, icon, background, create_time, update_time, create_by, update_by, delete_flag,user_upload,tag_ids) values (#{title}, #{subtitle}, #{parentCategoryId}, #{contentType}, #{categoryIds}, #{summary}, #{content}, #{coverImage}, #{authorId}, #{authorName}, #{reviewerId}, #{reviewerName}, #{auditStatus}, #{auditComment}, #{publishStatus}, #{publishTime}, - #{viewCount}, #{likeCount}, #{commentCount}, #{sort}, #{isPaid}, #{price}, #{requiredPoints}, #{supportPointsPay}, #{isOfficial}, #{shareCount}, #{fileUrl}, #{icon}, #{background}, #{createTime}, #{updateTime}, #{createBy}, #{updateBy}, #{deleteFlag}) + #{viewCount}, #{likeCount}, #{commentCount}, #{sort}, #{isPaid}, #{price}, #{requiredPoints}, #{supportPointsPay}, #{isOfficial}, #{shareCount}, #{fileUrl}, #{icon}, #{background}, #{createTime}, #{updateTime}, #{createBy}, #{updateBy}, #{deleteFlag}, + #{userUpload},#{tagIds} ) + @@ -272,6 +280,12 @@ update_by = #{updateBy}, + + skill_path = #{skillPath}, + + + skill_content = #{skillContent}, + where content_id = #{contentId}