677 lines
34 KiB
Java
677 lines
34 KiB
Java
package com.kexue.skills.service.impl;
|
||
|
||
import cn.dev33.satoken.stp.StpUtil;
|
||
import cn.hutool.core.collection.CollectionUtil;
|
||
import com.alibaba.fastjson.JSON;
|
||
import com.alibaba.fastjson.JSONObject;
|
||
import com.kexue.skills.common.Assert;
|
||
import com.kexue.skills.common.util.HttpUtil;
|
||
import com.kexue.skills.config.DeepSeekConfig;
|
||
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.request.SkillAnalyzeRequest;
|
||
import com.kexue.skills.entity.request.SkillGenRequest;
|
||
import com.kexue.skills.entity.request.SkillPreGenRequest;
|
||
import com.kexue.skills.entity.request.SkillRequest;
|
||
import com.kexue.skills.entity.response.SkillResponse;
|
||
import com.kexue.skills.exception.BizException;
|
||
import com.kexue.skills.mapper.CmsContentMapper;
|
||
import com.kexue.skills.service.CmsTagService;
|
||
import com.kexue.skills.service.SkillGenService;
|
||
import com.kexue.skills.utils.EscapeCharacterUtils;
|
||
import jodd.util.StringUtil;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.stereotype.Service;
|
||
|
||
import javax.annotation.Resource;
|
||
import java.io.File;
|
||
import java.io.FileOutputStream;
|
||
import java.math.BigDecimal;
|
||
import java.time.LocalDateTime;
|
||
import java.time.format.DateTimeFormatter;
|
||
import java.util.*;
|
||
import java.util.stream.Collectors;
|
||
|
||
/**
|
||
* 技能生成服务实现
|
||
*
|
||
* @author 维哥
|
||
* @since 2026-01-28
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
public class SkillGenServiceImpl implements SkillGenService {
|
||
|
||
@Autowired
|
||
private DeepSeekConfig deepSeekConfig;
|
||
|
||
@Autowired
|
||
private GlmConfig glmConfig;
|
||
|
||
@Autowired
|
||
private CmsTagService cmsTagService;
|
||
|
||
@Resource
|
||
private CmsContentMapper cmsContentMapper;
|
||
|
||
/**
|
||
* 生成技能
|
||
*
|
||
* @param request 生成请求
|
||
* @return 生成结果
|
||
*/
|
||
@Override
|
||
public SkillResponse preGenerate(SkillPreGenRequest request) {
|
||
log.info("生成技能请求: {}", request);
|
||
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
|
||
|
||
// 从数据库中读取cms_tag表的标签信息
|
||
CmsTagDto tagDto = new CmsTagDto();
|
||
tagDto.setDeleteFlag(0);
|
||
tagDto.setStatus(1);
|
||
List<CmsTag> tags = cmsTagService.getList(tagDto);
|
||
|
||
// 将标签名称拼接成逗号分隔的字符串
|
||
StringBuilder tagsList = new StringBuilder();
|
||
for (int i = 0; i < tags.size(); i++) {
|
||
CmsTag tag = tags.get(i);
|
||
tagsList.append(tag.getTagId()+"."+tag.getTagName());
|
||
if (i < tags.size() - 1) {
|
||
tagsList.append(",");
|
||
}
|
||
}
|
||
|
||
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),
|
||
deepSeekConfig.getChat().getTemperature(), deepSeekConfig.getChat().getMaxTokens(),
|
||
request.getPrompt(), tagsList.toString());
|
||
|
||
String deepseekResponse = "";
|
||
try {
|
||
// 发送HTTP请求到deepseek API
|
||
deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null);
|
||
log.info("Deepseek API响应: {}", deepseekResponse);
|
||
|
||
// 解析deepseek返回结果
|
||
JSONObject responseJson = JSON.parseObject(deepseekResponse);
|
||
List<JSONObject> 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");
|
||
|
||
// 解析content中的JSON
|
||
JSONObject skillJson = JSON.parseObject(content);
|
||
SkillResponse skillResponse = new SkillResponse();
|
||
skillResponse.setName(skillJson.getString("name"));
|
||
skillResponse.setDescription(skillJson.getString("description"));
|
||
skillResponse.setTags(skillJson.getJSONArray("tags").toJavaList(String.class));
|
||
|
||
log.info("解析技能响应: {}", skillResponse);
|
||
return skillResponse;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
|
||
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
public SkillResponse preGenerateV2(SkillPreGenRequest request) {
|
||
log.info("生成技能请求V2: {}", request);
|
||
String url = glmConfig.getBaseUrl() + "/chat/completions";
|
||
String apiKey = glmConfig.getApiKey();
|
||
String model = glmConfig.getChat().getModel();
|
||
double temperature = glmConfig.getChat().getTemperature();
|
||
int maxTokens = glmConfig.getChat().getMaxTokens();
|
||
|
||
// 从数据库中读取cms_tag表的标签信息
|
||
CmsTagDto tagDto = new CmsTagDto();
|
||
tagDto.setDeleteFlag(0);
|
||
tagDto.setStatus(1);
|
||
List<CmsTag> tags = cmsTagService.getList(tagDto);
|
||
|
||
// 将标签名称拼接成逗号分隔的字符串
|
||
StringBuilder tagsList = new StringBuilder();
|
||
for (int i = 0; i < tags.size(); i++) {
|
||
CmsTag tag = tags.get(i);
|
||
tagsList.append(tag.getTagId()+"."+tag.getTagName());
|
||
if (i < tags.size() - 1) {
|
||
tagsList.append(",");
|
||
}
|
||
}
|
||
|
||
// 构建系统消息内容
|
||
String systemContent = "你是一个专业的AI技能设计助手。请根据agent skills撰写规范,按照用户提出的主题描述及参考文件,生成这个skill的名称、描述,并从以下标签列表中选择至少3个标签:\"" + tagsList.toString() + "\",tags只需要返回序号数组,并简述这个skill的具体价值点。输出json格式,仅输出以上所提到的名称、描述、标签、价值点,节点名称分别为name、description、tags、value_point,节点内容以中文形式返回。请严格按照指定的JSON格式输出,仅包含要求的字段,以中文形式返回。";
|
||
|
||
// 准备文件URL列表
|
||
List<String> fileUrls = new ArrayList<>();
|
||
if (request.getFileUrl() != null && !request.getFileUrl().isEmpty()) {
|
||
fileUrls.add(request.getFileUrl());
|
||
}
|
||
if (request.getFileUrls() != null && !request.getFileUrls().isEmpty()) {
|
||
fileUrls.addAll(request.getFileUrls());
|
||
}
|
||
|
||
// 创建技能请求
|
||
SkillRequest skillRequest = new SkillRequest(model, systemContent, request.getPrompt(), fileUrls, temperature, maxTokens);
|
||
|
||
String response = "";
|
||
try {
|
||
// 发送HTTP请求到API
|
||
response = HttpUtil.sendPostRequest(url, skillRequest, apiKey, null);
|
||
log.info("API响应: {}", response);
|
||
|
||
// 解析返回结果
|
||
JSONObject responseJson = JSON.parseObject(response);
|
||
List<JSONObject> 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");
|
||
|
||
// 解析content中的JSON
|
||
JSONObject skillJson = JSON.parseObject(content);
|
||
SkillResponse skillResponse = new SkillResponse();
|
||
skillResponse.setName(skillJson.getString("name"));
|
||
skillResponse.setDescription(skillJson.getString("description"));
|
||
skillResponse.setTags(skillJson.getJSONArray("tags").toJavaList(String.class));
|
||
skillResponse.setSummary(skillJson.getString("value_point"));
|
||
|
||
log.info("解析技能响应: {}", skillResponse);
|
||
return skillResponse;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("调用API失败: {}", e.getMessage(), e);
|
||
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ response);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
public CmsContent generate(SkillGenRequest request) {
|
||
log.info("生成技能请求: {}", request);
|
||
// 参数验证,确保每个参数都必须传递
|
||
Assert.notEmpty(request.getName(), "技能名称不能为空");
|
||
Assert.notEmpty(request.getDescription(), "技能描述不能为空");
|
||
Assert.notEmpty(request.getTags(), "技能标签不能为空");
|
||
Assert.notEmpty(request.getRequirement(), "技能摘要不能为空");
|
||
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
|
||
|
||
// 从数据库中读取cms_tag表的标签信息
|
||
CmsTagDto tagDto = new CmsTagDto();
|
||
tagDto.setDeleteFlag(0);
|
||
tagDto.setStatus(1);
|
||
List<CmsTag> tags = cmsTagService.getList(tagDto);
|
||
|
||
List<String> tags1 = request.getTags();
|
||
|
||
// 将标签名称拼接成逗号分隔的字符串
|
||
StringBuilder tagsList = new StringBuilder();
|
||
String defaultIcon = "";
|
||
for (int i = 0; i < tags.size(); i++) {
|
||
CmsTag tag = tags.get(i);
|
||
if (tags1.contains(tag.getTagId()+"")) {
|
||
if (StringUtil.isEmpty(defaultIcon)){
|
||
defaultIcon = tag.getIcon();
|
||
}
|
||
tagsList.append(tag.getTagName());
|
||
if (i < tags.size() - 1) {
|
||
tagsList.append(",");
|
||
}
|
||
}
|
||
}
|
||
String systemContent = """
|
||
你是AI技能包设计专家,仅输出【完整的纯YAML文本】,输出内容是完整可解析的技能YAML描述文件,绝非片段,不包含任何多余文字(无解释、无注释、无引言、无结尾)。
|
||
|
||
### 一、YAML顶层强制规则(仅一个节点:package)
|
||
1. 顶层只能有 package 一个节点,所有信息(名称、版本、目录结构等)均嵌套在 package 下
|
||
2. package 节点必含子字段:name、version、description、author、created、tags、structure(缺一不可)
|
||
3. 最终必须输出完整闭合的YAML结构,禁止输出残缺片段、部分节点
|
||
|
||
### 二、package子字段规范(固定格式)
|
||
1. name:技能名称(与用户提供的Skill名称完全一致,不修改)
|
||
2. version:固定为 "1.0.0"
|
||
3. description:用户提供的Skill描述(完整复制,不增删任何内容)
|
||
4. author:固定为 "AI技能生成助手"
|
||
5. created:格式为 "YYYY-MM-DD"(使用当前日期,如2026-04-01)
|
||
6. tags:数组格式,值为用户提供的Skill标签(中文,自动去重,逗号后加空格)
|
||
7. structure:技能包目录树,根目录固定为 /,structure下直接编写根目录的一级子文件/子文件夹,禁止重复描述根目录本身
|
||
|
||
### 三、structure目录树规则(仅Python脚本,无其他语言)
|
||
#### 1. 基础必含文件(所有技能通用)
|
||
- 根目录 / 下必生成:skills.md 文件,归属父路径 /,文件必须保留后缀名 .md
|
||
|
||
#### 2. structure核心编写规则
|
||
1. structure节点下不写根目录/的描述,直接从一级子文件/子文件夹开始编写
|
||
2. 【path路径强制规则】path只写父级目录路径,不拼接文件名称/目录名称
|
||
- 目录自身名称使用 name 字段体现,目录一律无后缀名
|
||
- 文件自身名称使用 name 字段体现,文件必须保留标准后缀名
|
||
- 示例:scripts目录在根目录下 → path: /,name: scripts
|
||
- 示例:skills.md在根目录下 → path: /,name: skills.md
|
||
- 示例:main.py在scripts目录下 → path: /scripts,name: main.py
|
||
3. 所有directory/file节点均为根目录/的直接/间接子节点
|
||
|
||
#### 3. Python脚本目录/文件判断逻辑
|
||
- 若用户提供的“Skill描述”“Skill摘要”中包含“脚本”“代码”“执行”“运行”“处理”“计算”等需执行逻辑的关键词:
|
||
1. 必须新增 scripts 目录,该目录为文件夹,无任何后缀名,path: /,name: scripts
|
||
2. 必须在该目录下固定生成 main.py 文件,文件必须带 .py 后缀,不允许省略或修改文件名
|
||
- 若用户需求中无任何执行逻辑相关描述(如纯文档、纯说明类技能):不生成 scripts 目录,避免冗余
|
||
|
||
#### 4. 节点必含字段
|
||
- directory类型:name、type、path(仅父级路径)、format(固定"dir")、description、children(空目录写 children: [])
|
||
- file类型:name、type、path(仅父级路径)、format(仅markdown/python两种)、description、content(非空,有实际可用内容)
|
||
|
||
### 四、文件content内容规范(Python脚本必实用)
|
||
#### 1. 根目录下 skills.md
|
||
- # 技能名称(不加多余符号,居中可加空格但不强制)
|
||
- ## 技能描述(整合用户“描述+摘要”,补充逻辑连贯性)
|
||
- ## 标签(格式:- 标签1\n- 标签2)
|
||
- ## 使用说明(分点写:适用场景、操作步骤,需脚本则写“运行scripts/main.py脚本”,无需则写“直接参考文档使用”)
|
||
- ## 目录结构(用代码块 ``` 列出所有文件/目录路径)
|
||
|
||
#### 2. scripts目录下 main.py
|
||
- 必含依赖导入(如 import pandas as pd,无依赖则不写)
|
||
- 必含入口函数 def execute(params: dict) -> dict:(参数为dict,返回dict结果)
|
||
- 函数内必含:
|
||
1. 参数校验(判断必填键是否存在,缺失返回错误)
|
||
2. 核心逻辑(匹配技能需求,如数据处理、文本分析)
|
||
3. 结果返回(成功:{"status": "success", "data": 结果};失败:{"status": "fail", "error": 信息})
|
||
- 必含注释:函数说明、参数/返回值说明、示例调用(if __name__ == "__main__": 块)
|
||
- 禁止空函数、语法错误,确保复制后可直接运行
|
||
|
||
### 五、YAML语法死规定(100%无解析错误)
|
||
1. 缩进:统一2个空格(禁止Tab,禁止1/3/4空格,嵌套层级严格对齐)
|
||
- package 下子字段缩进2空格
|
||
- structure 下目录/文件节点缩进4空格(package→structure→children,每层+2空格)
|
||
2. 路径:全部遵循path只写父目录规则,Unix风格 /,禁止 \\ 或 ./
|
||
3. 字符串:特殊字符(:、#、空格)无需转义,直接书写
|
||
4. 数组:tags格式严格为 tags: [标签1, 标签2](逗号后加空格,无多余逗号)
|
||
5. content:多行内容用 | 开头,内容行首顶格,内部遵循对应格式缩进(Python用4空格)
|
||
|
||
### 六、错误规避红线(绝对不能触碰)
|
||
1. 禁止顶层出现除 package 外的任何节点
|
||
2. 禁止在YAML前后加任何多余文字
|
||
3. 禁止生成非Python脚本,禁止生成无后缀的脚本文件
|
||
4. 禁止字段缺失
|
||
5. 禁止输出YAML片段,必须输出完整可解析文件
|
||
6. structure禁止描述根目录/,直接从一级子节点开始
|
||
7. 严禁path中携带文件/目录名称,必须只写父级路径
|
||
8. 严禁将scripts目录错误添加后缀名,严禁修改Python脚本名为非main.py
|
||
|
||
最终输出仅完整纯YAML,直接可复制存储、解析使用,无任何冗余或格式问题!
|
||
""";
|
||
|
||
String userContent = """
|
||
基于以下信息生成技能包YAML,严格遵守system指令(仅Python脚本,无其他语言):
|
||
1. Skill名称:%s
|
||
2. Skill描述:%s
|
||
3. Skill标签:%s(中文,直接使用,不修改)
|
||
4. Skill摘要/需求:%s(用于完善skills.md的“使用说明”章节)
|
||
""".formatted(
|
||
request.getName(),
|
||
request.getDescription(),
|
||
tagsList.toString(),
|
||
request.getRequirement()
|
||
);
|
||
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),systemContent,userContent,deepSeekConfig.getChat().getTemperature(), 8192,"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);
|
||
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);
|
||
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList();
|
||
if (CollectionUtil.isNotEmpty( list)){
|
||
cmsContent.setIcon(list.get(0).getIcon());
|
||
}
|
||
// 保存到数据库
|
||
cmsContentMapper.insert(cmsContent);
|
||
return cmsContent;
|
||
} catch (Exception e) {
|
||
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
|
||
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
|
||
}
|
||
}
|
||
|
||
private CmsContent getCmsContent(SkillGenRequest request, String CmsContent,Long userId,String defaultIcon){
|
||
CmsContent cmsContent = new CmsContent();
|
||
cmsContent.setTitle(request.getName());
|
||
cmsContent.setDescription(request.getDescription());
|
||
cmsContent.setContent(CmsContent);
|
||
cmsContent.setTags(request.getTags().stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||
cmsContent.setIsPaid(0);//免费
|
||
cmsContent.setIsOfficial(false);
|
||
cmsContent.setPublishStatus(1);
|
||
cmsContent.setAuthorId(userId);
|
||
cmsContent.setAuthorName(null);
|
||
cmsContent.setAuditStatus(1);
|
||
cmsContent.setViewCount(0);
|
||
cmsContent.setCommentCount(0);
|
||
cmsContent.setRequiredPoints(0);
|
||
cmsContent.setSupportPointsPay(0);
|
||
cmsContent.setPrice(new BigDecimal(0));
|
||
cmsContent.setLikeCount(0);
|
||
cmsContent.setShareCount(0);
|
||
cmsContent.setCreateTime(new Date());
|
||
cmsContent.setUpdateTime(new Date());
|
||
cmsContent.setContentType(1);
|
||
cmsContent.setCreateBy(userId+"");
|
||
cmsContent.setUpdateBy(userId+"");
|
||
cmsContent.setDeleteFlag(0);
|
||
cmsContent.setIcon(defaultIcon);
|
||
cmsContent.setRequirement(request.getRequirement());
|
||
cmsContent.setIntroduce(request.getIntroduce());
|
||
return cmsContent;
|
||
}
|
||
|
||
|
||
/**
|
||
* 分析技能
|
||
*
|
||
* @param request 分析请求
|
||
* @return 分析结果
|
||
*/
|
||
@Override
|
||
public String analyzeSkill(SkillAnalyzeRequest request) {
|
||
log.info("分析技能请求: {}", request);
|
||
// 这里可以实现技能分析逻辑
|
||
// 不需要数据库操作,直接返回结果
|
||
return "技能分析成功: " + request.getSkillId();
|
||
}
|
||
|
||
@Override
|
||
public String genIntroduce(String content) {
|
||
log.info("生成技能介绍请求: {}", content);
|
||
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
|
||
|
||
String systemContent = "你是一个专业的AI技能设计助手。我会给你提供一个完整的skill的内容,请你帮我总结出skill的作用,能够解决的问题,输出一段描述文本";
|
||
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, content, 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
|
||
public CmsContent uploadSkill(String skillUrl) {
|
||
log.info("上传技能压缩包请求: {}", skillUrl);
|
||
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
|
||
|
||
// 从数据库中读取cms_tag表的标签信息
|
||
CmsTagDto tagDto = new CmsTagDto();
|
||
tagDto.setDeleteFlag(0);
|
||
tagDto.setStatus(1);
|
||
List<CmsTag> tags = cmsTagService.getList(tagDto);
|
||
|
||
// 将标签名称拼接成逗号分隔的字符串
|
||
StringBuilder tagsList = new StringBuilder();
|
||
for (int i = 0; i < tags.size(); i++) {
|
||
CmsTag tag = tags.get(i);
|
||
tagsList.append(tag.getTagId()+"."+tag.getTagName());
|
||
if (i < tags.size() - 1) {
|
||
tagsList.append(",");
|
||
}
|
||
}
|
||
|
||
// 构建系统消息内容
|
||
String systemContent = """
|
||
你是一位专业的AI技能包解析助手,需完成以下任务:
|
||
1. 我会提供一个技能包压缩包(格式包含zip/rar)的URL,你需解析该压缩包根目录下的SKILL.md文件; 或者提供一个在线的SKILL.md文件的url;
|
||
2. 基于SKILL.md内容,提炼出:
|
||
- skill的核心标题(name);
|
||
- skill的核心描述(description);
|
||
- 详细功能介绍(introduce);
|
||
- 从指定标签列表中选取至少3个适配标签(tagList),标签范围:TAG_LIST;
|
||
- 选择标签的时候只需要输出对应的标签编号
|
||
3. 生成content字段:基于技能包的名称、描述、标签,按照skills目录结构输出完整的技能包YAML内容(包含skills.md本体、scripts目录脚本等),需遵循以下严格规范:
|
||
- 包含技能包必需的文件和目录;
|
||
- 多行内容使用「|」字面块表示;
|
||
- 所有内容从行首开始,无前置空格;
|
||
- 空目录标注为「children: []」;
|
||
- 文件内容需具备实际使用价值;
|
||
- YAML文档需完整,核心概要包含name、version、description、author、created、tags等属性;
|
||
- 核心节点为structure,用于描述技能包文件目录结构;structure下每个节点需包含基础属性:name、type、path、format、description;content和children为互选属性(type为file时,content字段填写文件内容;type为directory时,children数组填写子目录/文件节点)。
|
||
请将最终结果以JSON格式输出,JSON结构必须包含:name(技能名称),description(技能描述)、introduce(功能介绍)、tagList(标签列表)、content(完整YAML格式技能包内容),无需额外说明,仅输出符合要求的JSON内容。
|
||
""";
|
||
systemContent = systemContent.replace("TAG_LIST", tagsList.toString());
|
||
|
||
// 构建用户消息内容
|
||
String userContent = skillUrl;
|
||
|
||
// 创建技能请求
|
||
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, userContent, 0.5, 8192, "json_object");
|
||
|
||
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()) {
|
||
// 获取最新的choice
|
||
JSONObject latestChoice = choices.get(0);
|
||
JSONObject message = latestChoice.getJSONObject("message");
|
||
String content = message.getString("content");
|
||
|
||
// 解析content中的JSON
|
||
JSONObject skillJson = JSON.parseObject(content);
|
||
|
||
// 构建技能生成请求
|
||
SkillGenRequest request = new SkillGenRequest();
|
||
request.setName(skillJson.getString("name"));
|
||
request.setDescription(skillJson.getString("description"));
|
||
request.setIntroduce(skillJson.getString("introduce"));
|
||
request.setTags(skillJson.getJSONArray("tagList").toJavaList(String.class));
|
||
|
||
// 生成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)){
|
||
cmsContent.setIcon(list.get(0).getIcon());
|
||
}
|
||
// 保存到数据库
|
||
// cmsContentMapper.insert(cmsContent);
|
||
return cmsContent;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
|
||
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
public CmsContent uploadSkillV2(byte[] fileBytes, String fileName) {
|
||
log.info("上传本地技能压缩包请求: {}", fileName);
|
||
|
||
try {
|
||
// 创建临时文件
|
||
String fileExtension = "";
|
||
if (fileName.contains(".")) {
|
||
int dotIndex = fileName.lastIndexOf(".");
|
||
if (dotIndex != -1) {
|
||
fileExtension = fileName.substring(dotIndex);
|
||
}
|
||
}
|
||
File tempFile = File.createTempFile("skill", fileExtension);
|
||
try (FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||
fos.write(fileBytes);
|
||
}
|
||
|
||
// 生成默认参数
|
||
String author = StpUtil.getLoginIdAsString();
|
||
// 从文件名中提取技能名称
|
||
String defaultSkillName = fileName;
|
||
if (fileName.contains(".")) {
|
||
int dotIndex = fileName.lastIndexOf(".");
|
||
if (dotIndex != -1) {
|
||
defaultSkillName = fileName.substring(0, dotIndex);
|
||
}
|
||
}
|
||
|
||
// 1. 提取压缩包中的skillMdText
|
||
Map<String, Object> skillInfo = com.kexue.skills.common.util.SkillZipParser.extractSkillMdText(tempFile.getAbsolutePath(), defaultSkillName);
|
||
String skillMdText = (String) skillInfo.get("skillMdText");
|
||
|
||
// 从数据库中读取cms_tag表的标签信息
|
||
CmsTagDto tagDto = new CmsTagDto();
|
||
tagDto.setDeleteFlag(0);
|
||
tagDto.setStatus(1);
|
||
List<CmsTag> tags = cmsTagService.getList(tagDto);
|
||
|
||
// 2. 解析skillMdText获取SkillGenRequest
|
||
SkillGenRequest request = null;
|
||
if (skillMdText != null && !skillMdText.isEmpty()) {
|
||
request = parseSkillMdText(skillMdText, tags);
|
||
} else {
|
||
// 如果没有找到md文件,使用默认值
|
||
request = new SkillGenRequest();
|
||
request.setName(defaultSkillName);
|
||
request.setDescription("未知");
|
||
request.setIntroduce("未知");
|
||
request.setTags(Arrays.asList("1001", "1000"));
|
||
}
|
||
|
||
// 3. 使用SkillGenRequest中的信息生成yaml
|
||
String yamlContent = com.kexue.skills.common.util.SkillZipParser.generateYamlFromSkillInfo(
|
||
tempFile.getAbsolutePath(),
|
||
author,
|
||
request.getName(),
|
||
request.getDescription(),
|
||
request.getTags()
|
||
);
|
||
log.info("解析出skill 压缩包内容:{}", yamlContent);
|
||
|
||
// 4. 生成CmsContent对象
|
||
CmsContent cmsContent = getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), "");
|
||
if (Objects.nonNull(cmsContent.getTags())) {
|
||
String s = cmsContent.getTags().split(",")[0];
|
||
long l = 1000;// 默认值
|
||
try {
|
||
l = Long.parseLong(s);
|
||
} catch (NumberFormatException e) {
|
||
// 异常是因为大模型返回的tag带中文了,忽略
|
||
}
|
||
long finalL = l;
|
||
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == finalL).toList();
|
||
if (CollectionUtil.isNotEmpty(list)){
|
||
cmsContent.setIcon(list.get(0).getIcon());
|
||
}
|
||
}
|
||
// 保存到数据库
|
||
cmsContentMapper.insert(cmsContent);
|
||
// 删除临时文件
|
||
tempFile.delete();
|
||
|
||
return cmsContent;
|
||
} catch (Exception e) {
|
||
log.error("上传本地技能压缩包失败: {}", e.getMessage(), e);
|
||
throw new BizException("上传本地技能压缩包失败:" + e.getMessage());
|
||
}
|
||
}
|
||
|
||
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());
|
||
if (i < tags.size() - 1) {
|
||
tagsList.append(",");
|
||
}
|
||
}
|
||
|
||
// 构建系统消息内容
|
||
String systemContent = """
|
||
你是一位专业的AI技能包解析助手,需完成以下任务:
|
||
1. 我会提供一个技能包压缩包SKILL.md文件的具体内容;
|
||
2. 基于SKILL.md内容,提炼出:
|
||
- skill的核心标题(name);
|
||
- skill的核心描述(description);
|
||
- 详细功能介绍(introduce);
|
||
- 从指定标签列表中选取至少3个适配标签(tagList),标签范围:TAG_LIST;
|
||
- 选择标签的时候只需要输出对应的标签编号,特别注意:只需要标签编号
|
||
请将最终结果以JSON格式输出,JSON结构必须包含:name(技能名称),description(技能描述)、introduce(功能介绍)、tagList(标签列表),无需额外说明,仅输出符合要求的JSON内容。
|
||
""";
|
||
systemContent = systemContent.replace("TAG_LIST", tagsList.toString());
|
||
|
||
// 创建技能请求
|
||
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, skillMdText, 0.5, 8192, "json_object");
|
||
|
||
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()) {
|
||
// 获取最新的choice
|
||
JSONObject latestChoice = choices.get(0);
|
||
JSONObject message = latestChoice.getJSONObject("message");
|
||
String content = message.getString("content");
|
||
|
||
// 解析content中的JSON
|
||
JSONObject skillJson = JSON.parseObject(content);
|
||
|
||
// 构建技能生成请求
|
||
SkillGenRequest request = new SkillGenRequest();
|
||
request.setName(skillJson.getString("name"));
|
||
request.setDescription(skillJson.getString("description"));
|
||
request.setIntroduce(skillJson.getString("introduce"));
|
||
// 处理tagList可能不存在的情况
|
||
if (skillJson.containsKey("tagList")) {
|
||
request.setTags(skillJson.getJSONArray("tagList").toJavaList(String.class));
|
||
} else {
|
||
request.setTags(Arrays.asList("1001", "1002"));
|
||
}
|
||
return request;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("调用Deepseek API失败: {}", e.getMessage(), e);
|
||
throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse);
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|