feat: 增加技能管理模块试验性功能,等待优化
This commit is contained in:
@@ -24,6 +24,10 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-chatlog</artifactId>
|
||||
|
||||
@@ -6,6 +6,7 @@ import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Plugin;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -76,6 +77,15 @@ public class AiResourceCreatorNameSupport {
|
||||
fillCreatorNames(agents, Agent::getCreatedBy, Agent::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量填充 Skill 创建人名称。
|
||||
*
|
||||
* @param skills Skill 集合
|
||||
*/
|
||||
public void fillSkillCreatorNames(Collection<Skill> skills) {
|
||||
fillCreatorNames(skills, Skill::getCreatedBy, Skill::setCreatedByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用的创建人名称填充逻辑。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.easyflow.admin.controller.skill;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
import tech.easyflow.skill.mapper.SkillMapper;
|
||||
import tech.easyflow.skill.service.SkillCategoryService;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 分类管理控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/skillCategory")
|
||||
@UsePermission(moduleName = "/api/v1/skill")
|
||||
public class SkillCategoryController extends BaseCurdController<SkillCategoryService, SkillCategory> {
|
||||
|
||||
@Resource
|
||||
private SkillMapper skillMapper;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 分类管理控制器。
|
||||
*
|
||||
* @param service Skill 分类服务
|
||||
*/
|
||||
public SkillCategoryController(SkillCategoryService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户可见的 Skill 分类。
|
||||
*
|
||||
* @param entity 查询条件
|
||||
* @param asTree 是否转树
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方式
|
||||
* @return 可见分类列表
|
||||
*/
|
||||
@GetMapping("visibleList")
|
||||
public Result<List<SkillCategory>> visibleList(SkillCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
if (access.isRestricted()) {
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
queryWrapper.in("id", access.getCategoryIds());
|
||||
}
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
return Result.ok(service.list(queryWrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分类前校验是否仍被 Skill 使用。
|
||||
*
|
||||
* @param ids 分类 ID 集合
|
||||
* @return 校验结果
|
||||
*/
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
for (Serializable id : ids) {
|
||||
List<Skill> skills = skillMapper.selectListByQuery(QueryWrapper.create().eq(Skill::getCategoryId, id));
|
||||
if (skills != null && !skills.isEmpty()) {
|
||||
throw new BusinessException("请先迁移或删除该分类下的 Skill");
|
||||
}
|
||||
List<SkillCategory> children = service.list(QueryWrapper.create().eq(SkillCategory::getParentId, id));
|
||||
if (children != null && !children.isEmpty()) {
|
||||
throw new BusinessException("请先删除子分类");
|
||||
}
|
||||
}
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
package tech.easyflow.admin.controller.skill;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.file.SkillFileContent;
|
||||
import tech.easyflow.skill.file.SkillFileNode;
|
||||
import tech.easyflow.skill.file.SkillFileSaveRequest;
|
||||
import tech.easyflow.skill.file.SkillFileService;
|
||||
import tech.easyflow.skill.imports.SkillExportService;
|
||||
import tech.easyflow.skill.imports.SkillImportPreview;
|
||||
import tech.easyflow.skill.imports.SkillImportService;
|
||||
import tech.easyflow.skill.publish.SkillPublishAppService;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.InputStream;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static tech.easyflow.skill.entity.table.SkillTableDef.SKILL;
|
||||
|
||||
/**
|
||||
* Skill 管理端控制器。
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/skill")
|
||||
public class SkillController extends BaseCurdController<SkillService, Skill> {
|
||||
|
||||
@Resource
|
||||
private SkillApprovalStateService skillApprovalStateService;
|
||||
@Resource
|
||||
private SkillPublishAppService skillPublishAppService;
|
||||
@Resource
|
||||
private SkillImportService skillImportService;
|
||||
@Resource
|
||||
private SkillExportService skillExportService;
|
||||
@Resource
|
||||
private SkillFileService skillFileService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||
|
||||
/**
|
||||
* 创建 Skill 控制器。
|
||||
*
|
||||
* @param service Skill 服务
|
||||
*/
|
||||
public SkillController(SkillService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 详情。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return Skill 详情
|
||||
*/
|
||||
@GetMapping("/getDetail")
|
||||
public Result<Skill> getDetail(BigInteger id) {
|
||||
Skill skill = service.getDetail(id);
|
||||
skillApprovalStateService.fillSkillApprovalState(skill);
|
||||
return Result.ok(skill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 保存后的 Skill
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("save")
|
||||
public Result<?> save(@JsonBody Skill skill) {
|
||||
return Result.ok(service.saveDraft(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 保存后的 Skill
|
||||
*/
|
||||
@Override
|
||||
@PostMapping("update")
|
||||
public Result<?> update(@JsonBody Skill skill) {
|
||||
return Result.ok(service.updateDraft(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览 zip 导入结果。
|
||||
*
|
||||
* @param file zip 文件
|
||||
* @return 导入预览
|
||||
*/
|
||||
@PostMapping(value = "/import/preview", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<SkillImportPreview> importPreview(MultipartFile file) throws Exception {
|
||||
return Result.ok(skillImportService.preview(file.getInputStream()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认导入 zip。
|
||||
*
|
||||
* @param file zip 文件
|
||||
* @param categoryId 分类 ID
|
||||
* @param overwriteDraft 是否覆盖草稿
|
||||
* @return 导入后的 Skill 列表
|
||||
*/
|
||||
@PostMapping(value = "/import/confirm", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<List<Skill>> importConfirm(MultipartFile file, BigInteger categoryId, Boolean overwriteDraft) throws Exception {
|
||||
return Result.ok(skillImportService.importZip(file.getInputStream(), categoryId, Boolean.TRUE.equals(overwriteDraft)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 Skill zip。
|
||||
*
|
||||
* @param ids Skill ID 集合
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@PostMapping("/export")
|
||||
public void export(@JsonBody(value = "ids", required = true) List<BigInteger> ids, HttpServletResponse response) throws Exception {
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode("skills.zip", StandardCharsets.UTF_8) + "\"");
|
||||
skillExportService.exportZip(ids, response.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 文件树。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @return 文件树
|
||||
*/
|
||||
@GetMapping("/file/tree")
|
||||
public Result<List<SkillFileNode>> fileTree(BigInteger skillId) {
|
||||
return Result.ok(skillFileService.tree(skillId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 文件内容。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
@GetMapping("/file/content")
|
||||
public Result<SkillFileContent> fileContent(BigInteger skillId, String path) {
|
||||
return Result.ok(skillFileService.getContent(skillId, path));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Skill 文本文件。
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 保存后的文件内容
|
||||
*/
|
||||
@PostMapping("/file/save")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<SkillFileContent> saveFile(@JsonBody SkillFileSaveRequest request) {
|
||||
return Result.ok(skillFileService.saveContent(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 Skill 逻辑文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
* @return 操作结果
|
||||
*/
|
||||
@PostMapping("/file/delete")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<Void> deleteFile(@JsonBody(value = "skillId", required = true) BigInteger skillId,
|
||||
@JsonBody(value = "path", required = true) String path) {
|
||||
skillFileService.deleteFile(skillId, path);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 Skill asset。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
* @param file 上传文件
|
||||
* @return asset 内容
|
||||
*/
|
||||
@PostMapping(value = "/file/asset/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<SkillFileContent> uploadAsset(BigInteger skillId, String path, MultipartFile file) {
|
||||
return Result.ok(skillFileService.uploadAsset(skillId, path, file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载或预览 Skill asset。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path asset 逻辑路径
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@GetMapping("/file/asset")
|
||||
public void asset(BigInteger skillId, String path, HttpServletResponse response) throws Exception {
|
||||
SkillFileContent content = skillFileService.getContent(skillId, path);
|
||||
response.setContentType(content.getMediaType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : content.getMediaType());
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" + URLEncoder.encode(fileName(path), StandardCharsets.UTF_8) + "\"");
|
||||
try (InputStream inputStream = skillFileService.openAsset(skillId, path)) {
|
||||
StreamUtils.copy(inputStream, response.getOutputStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交发布审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitPublishApproval")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(skillPublishAppService.submitPublishApproval(id), "已提交发布审批", "已直接发布");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交下线审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitOfflineApproval")
|
||||
@SaCheckPermission("/api/v1/skill/save")
|
||||
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(skillPublishAppService.submitOfflineApproval(id), "已提交下线审批", "已直接下线");
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交删除审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批实例 ID
|
||||
*/
|
||||
@PostMapping("/submitDeleteApproval")
|
||||
@SaCheckPermission("/api/v1/skill/remove")
|
||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||
return buildApprovalActionResult(skillPublishAppService.submitDeleteApproval(id), "已提交删除审批", "已直接删除");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
for (Serializable id : ids) {
|
||||
Skill skill = service.getById(String.valueOf(id));
|
||||
if (skill != null) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE, "无权限删除该 Skill");
|
||||
}
|
||||
}
|
||||
return super.onRemoveBefore(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询 Skill 分页。
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param queryWrapper 查询条件
|
||||
* @return Skill 分页
|
||||
*/
|
||||
@Override
|
||||
protected Page<Skill> queryPage(Page<Skill> page, QueryWrapper queryWrapper) {
|
||||
if (!applyCategoryPermission(queryWrapper)) {
|
||||
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), 0L);
|
||||
}
|
||||
applyPublishedOnlyFilter(queryWrapper);
|
||||
Page<Skill> result = super.queryPage(page, queryWrapper);
|
||||
if (isPublishedOnlyRequest()) {
|
||||
result.setRecords(result.getRecords().stream().map(skill -> service.fromSnapshot(skill.getPublishedSnapshotJson())).toList());
|
||||
}
|
||||
skillApprovalStateService.fillSkillApprovalState(result.getRecords());
|
||||
aiResourceCreatorNameSupport.fillSkillCreatorNames(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
if (!access.isRestricted()) {
|
||||
return true;
|
||||
}
|
||||
if (access.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.eq(Skill::getCreatedBy, access.getAccountId());
|
||||
return true;
|
||||
}
|
||||
queryWrapper.and(SKILL.CREATED_BY.eq(access.getAccountId()).or(SKILL.CATEGORY_ID.in(access.getCategoryIds())));
|
||||
return true;
|
||||
}
|
||||
|
||||
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||
if (isPublishedOnlyRequest()) {
|
||||
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPublishedOnlyRequest() {
|
||||
HttpServletRequest request = currentRequest();
|
||||
if (request == null) {
|
||||
return false;
|
||||
}
|
||||
return "true".equalsIgnoreCase(request.getParameter("publishedOnly"));
|
||||
}
|
||||
|
||||
private HttpServletRequest currentRequest() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return null;
|
||||
}
|
||||
return attributes.getRequest();
|
||||
}
|
||||
|
||||
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||
String approvalMessage,
|
||||
String directMessage) {
|
||||
return Result.ok(actionResult.isApprovalRequired() ? approvalMessage : directMessage, actionResult.getInstanceId());
|
||||
}
|
||||
|
||||
private String fileName(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "asset";
|
||||
}
|
||||
int index = path.lastIndexOf('/');
|
||||
return index < 0 ? path : path.substring(index + 1);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;
|
||||
@@ -14,8 +15,6 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
||||
@Component("local")
|
||||
@@ -116,6 +115,20 @@ public class LocalFileStorageServiceImpl implements FileStorageService {
|
||||
|
||||
@Override
|
||||
public String save(MultipartFile file, String prePath) {
|
||||
return save(file);
|
||||
try {
|
||||
String path = PathGeneratorUtil.generateUserPath(file.getOriginalFilename());
|
||||
if (StringUtils.hasText(prePath)) {
|
||||
String normalized = prePath.replaceAll("^/+", "").replaceAll("/+$", "");
|
||||
path = normalized + "/" + path.replaceAll("^/+", "");
|
||||
}
|
||||
File target = getLocalFile(path);
|
||||
if (!target.getParentFile().exists() && !target.getParentFile().mkdirs()) {
|
||||
LOG.error("创建文件失败: {} ", target.getParentFile());
|
||||
}
|
||||
file.transferTo(target);
|
||||
return prefix + path;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public enum ApprovalResourceType {
|
||||
|
||||
BOT("BOT"),
|
||||
AGENT("AGENT"),
|
||||
SKILL("SKILL"),
|
||||
WORKFLOW("WORKFLOW"),
|
||||
KNOWLEDGE("KNOWLEDGE");
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ tech.easyflow.chatlog.config.ChatlogModuleConfig
|
||||
tech.easyflow.datacenter.config.DatacenterModuleConfig
|
||||
tech.easyflow.job.config.JobModuleConfig
|
||||
tech.easyflow.log.config.LogModuleConfig
|
||||
tech.easyflow.skill.config.SkillModuleConfig
|
||||
tech.easyflow.system.config.SysModuleConfig
|
||||
|
||||
59
easyflow-modules/easyflow-module-skill/pom.xml
Normal file
59
easyflow-modules/easyflow-module-skill/pom.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-module-skill</name>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-approval</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-satoken</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-file-storage</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-skill</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/**
|
||||
* Skill 模块自动配置。
|
||||
*/
|
||||
@MapperScan("tech.easyflow.skill.mapper")
|
||||
@ComponentScan("tech.easyflow.skill")
|
||||
@AutoConfiguration
|
||||
public class SkillModuleConfig {
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 主实体。
|
||||
*/
|
||||
@Table("tb_skill")
|
||||
public class Skill extends DateEntity implements VisibilityResource, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger categoryId;
|
||||
private String name;
|
||||
private String displayName;
|
||||
private String description;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
private String skillContent;
|
||||
private Boolean enabled;
|
||||
private String visibilityScope;
|
||||
private String sourceType;
|
||||
private String packageHash;
|
||||
private Integer referenceCount;
|
||||
private Integer scriptCount;
|
||||
private Integer assetCount;
|
||||
private String publishStatus;
|
||||
private BigInteger currentApprovalInstanceId;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> publishedSnapshotJson = new LinkedHashMap<>();
|
||||
private Date publishedAt;
|
||||
private BigInteger publishedBy;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
@Column(ignore = true)
|
||||
private List<SkillReference> references;
|
||||
@Column(ignore = true)
|
||||
private List<SkillScript> scripts;
|
||||
@Column(ignore = true)
|
||||
private List<SkillAsset> assets;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getDeptId() { return deptId; }
|
||||
public void setDeptId(BigInteger deptId) { this.deptId = deptId; }
|
||||
public BigInteger getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(BigInteger categoryId) { this.categoryId = categoryId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
public String getSkillContent() { return skillContent; }
|
||||
public void setSkillContent(String skillContent) { this.skillContent = skillContent; }
|
||||
public Boolean getEnabled() { return enabled; }
|
||||
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||
public String getVisibilityScope() { return visibilityScope; }
|
||||
public void setVisibilityScope(String visibilityScope) { this.visibilityScope = visibilityScope; }
|
||||
public String getSourceType() { return sourceType; }
|
||||
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
|
||||
public String getPackageHash() { return packageHash; }
|
||||
public void setPackageHash(String packageHash) { this.packageHash = packageHash; }
|
||||
public Integer getReferenceCount() { return referenceCount; }
|
||||
public void setReferenceCount(Integer referenceCount) { this.referenceCount = referenceCount; }
|
||||
public Integer getScriptCount() { return scriptCount; }
|
||||
public void setScriptCount(Integer scriptCount) { this.scriptCount = scriptCount; }
|
||||
public Integer getAssetCount() { return assetCount; }
|
||||
public void setAssetCount(Integer assetCount) { this.assetCount = assetCount; }
|
||||
public String getPublishStatus() { return publishStatus; }
|
||||
public void setPublishStatus(String publishStatus) { this.publishStatus = publishStatus; }
|
||||
public BigInteger getCurrentApprovalInstanceId() { return currentApprovalInstanceId; }
|
||||
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { this.currentApprovalInstanceId = currentApprovalInstanceId; }
|
||||
public Map<String, Object> getPublishedSnapshotJson() { return publishedSnapshotJson; }
|
||||
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) { this.publishedSnapshotJson = publishedSnapshotJson == null ? new LinkedHashMap<>() : publishedSnapshotJson; }
|
||||
public Date getPublishedAt() { return publishedAt; }
|
||||
public void setPublishedAt(Date publishedAt) { this.publishedAt = publishedAt; }
|
||||
public BigInteger getPublishedBy() { return publishedBy; }
|
||||
public void setPublishedBy(BigInteger publishedBy) { this.publishedBy = publishedBy; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
public Boolean getApprovalPending() { return approvalPending; }
|
||||
public void setApprovalPending(Boolean approvalPending) { this.approvalPending = approvalPending; }
|
||||
public String getCurrentApprovalActionType() { return currentApprovalActionType; }
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) { this.currentApprovalActionType = currentApprovalActionType; }
|
||||
public String getDisplayPublishStatus() { return displayPublishStatus; }
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; }
|
||||
public String getCreatedByName() { return createdByName; }
|
||||
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
|
||||
public List<SkillReference> getReferences() { return references; }
|
||||
public void setReferences(List<SkillReference> references) { this.references = references; }
|
||||
public List<SkillScript> getScripts() { return scripts; }
|
||||
public void setScripts(List<SkillScript> scripts) { this.scripts = scripts; }
|
||||
public List<SkillAsset> getAssets() { return assets; }
|
||||
public void setAssets(List<SkillAsset> assets) { this.assets = assets; }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill asset 实体。
|
||||
*/
|
||||
@Table("tb_skill_asset")
|
||||
public class SkillAsset implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String name;
|
||||
private String mediaType;
|
||||
private String contentRef;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public String getContentRef() { return contentRef; }
|
||||
public void setContentRef(String contentRef) { this.contentRef = contentRef; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引实体。
|
||||
*/
|
||||
@Table("tb_skill_asset_content")
|
||||
public class SkillAssetContent implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id
|
||||
private String contentRef;
|
||||
private String contentHash;
|
||||
private String filePath;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private Integer refCount;
|
||||
private Date created;
|
||||
private Date modified;
|
||||
|
||||
public String getContentRef() { return contentRef; }
|
||||
public void setContentRef(String contentRef) { this.contentRef = contentRef; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public String getFilePath() { return filePath; }
|
||||
public void setFilePath(String filePath) { this.filePath = filePath; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Integer getRefCount() { return refCount; }
|
||||
public void setRefCount(Integer refCount) { this.refCount = refCount; }
|
||||
public Date getCreated() { return created; }
|
||||
public void setCreated(Date created) { this.created = created; }
|
||||
public Date getModified() { return modified; }
|
||||
public void setModified(Date modified) { this.modified = modified; }
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill 分类实体。
|
||||
*/
|
||||
@Table("tb_skill_category")
|
||||
public class SkillCategory extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger parentId;
|
||||
private String categoryName;
|
||||
private Integer levelNo;
|
||||
private String ancestors;
|
||||
private Integer sortNo;
|
||||
private Integer status;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getParentId() { return parentId; }
|
||||
public void setParentId(BigInteger parentId) { this.parentId = parentId; }
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
|
||||
public Integer getLevelNo() { return levelNo; }
|
||||
public void setLevelNo(Integer levelNo) { this.levelNo = levelNo; }
|
||||
public String getAncestors() { return ancestors; }
|
||||
public void setAncestors(String ancestors) { this.ancestors = ancestors; }
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill reference 文档实体。
|
||||
*/
|
||||
@Table("tb_skill_reference")
|
||||
public class SkillReference implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String name;
|
||||
private String content;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.skill.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill script 实体。
|
||||
*/
|
||||
@Table("tb_skill_script")
|
||||
public class SkillScript implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String language;
|
||||
private String content;
|
||||
private String contentHash;
|
||||
private Long size;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getContentHash() { return contentHash; }
|
||||
public void setContentHash(String contentHash) { this.contentHash = contentHash; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件内容。
|
||||
*/
|
||||
public class SkillFileContent {
|
||||
|
||||
private String path;
|
||||
private String type;
|
||||
private String content;
|
||||
private String language;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private String downloadUrl;
|
||||
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public String getDownloadUrl() { return downloadUrl; }
|
||||
public void setDownloadUrl(String downloadUrl) { this.downloadUrl = downloadUrl; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件树节点。
|
||||
*/
|
||||
public class SkillFileNode {
|
||||
|
||||
private String key;
|
||||
private String path;
|
||||
private String name;
|
||||
private String type;
|
||||
private String language;
|
||||
private String mediaType;
|
||||
private Long size;
|
||||
private List<SkillFileNode> children = new ArrayList<>();
|
||||
|
||||
public String getKey() { return key; }
|
||||
public void setKey(String key) { this.key = key; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getType() { return type; }
|
||||
public void setType(String type) { this.type = type; }
|
||||
public String getLanguage() { return language; }
|
||||
public void setLanguage(String language) { this.language = language; }
|
||||
public String getMediaType() { return mediaType; }
|
||||
public void setMediaType(String mediaType) { this.mediaType = mediaType; }
|
||||
public Long getSize() { return size; }
|
||||
public void setSize(Long size) { this.size = size; }
|
||||
public List<SkillFileNode> getChildren() { return children; }
|
||||
public void setChildren(List<SkillFileNode> children) { this.children = children == null ? new ArrayList<>() : children; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 文本文件保存请求。
|
||||
*/
|
||||
public class SkillFileSaveRequest {
|
||||
|
||||
private BigInteger skillId;
|
||||
private String path;
|
||||
private String content;
|
||||
|
||||
public BigInteger getSkillId() { return skillId; }
|
||||
public void setSkillId(BigInteger skillId) { this.skillId = skillId; }
|
||||
public String getPath() { return path; }
|
||||
public void setPath(String path) { this.path = path; }
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件服务。
|
||||
*/
|
||||
public interface SkillFileService {
|
||||
|
||||
/**
|
||||
* 获取 Skill 文件树。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @return 文件树
|
||||
*/
|
||||
List<SkillFileNode> tree(BigInteger skillId);
|
||||
|
||||
/**
|
||||
* 获取逻辑文件内容。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
SkillFileContent getContent(BigInteger skillId, String path);
|
||||
|
||||
/**
|
||||
* 保存文本文件内容。
|
||||
*
|
||||
* @param request 保存请求
|
||||
* @return 保存后的文件内容
|
||||
*/
|
||||
SkillFileContent saveContent(SkillFileSaveRequest request);
|
||||
|
||||
/**
|
||||
* 删除逻辑文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path 逻辑路径
|
||||
*/
|
||||
void deleteFile(BigInteger skillId, String path);
|
||||
|
||||
/**
|
||||
* 上传 asset 文件。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path asset 逻辑路径
|
||||
* @param file 上传文件
|
||||
* @return asset 文件内容
|
||||
*/
|
||||
SkillFileContent uploadAsset(BigInteger skillId, String path, MultipartFile file);
|
||||
|
||||
/**
|
||||
* 打开 asset 输入流。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param path asset 逻辑路径
|
||||
* @return asset 输入流
|
||||
*/
|
||||
InputStream openAsset(BigInteger skillId, String path);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
import com.easyagents.skill.model.SkillScriptLanguage;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.easyagents.skill.util.SkillPaths;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.service.SkillAssetService;
|
||||
import tech.easyflow.skill.service.SkillReferenceService;
|
||||
import tech.easyflow.skill.service.SkillScriptService;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillFileServiceImpl implements SkillFileService {
|
||||
|
||||
private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
|
||||
|
||||
private final SkillService skillService;
|
||||
private final SkillReferenceService skillReferenceService;
|
||||
private final SkillScriptService skillScriptService;
|
||||
private final SkillAssetService skillAssetService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 逻辑文件服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param skillReferenceService reference 服务
|
||||
* @param skillScriptService script 服务
|
||||
* @param skillAssetService asset 服务
|
||||
* @param contentStore asset 内容存储
|
||||
* @param resourceAccessService 资源访问服务
|
||||
*/
|
||||
public SkillFileServiceImpl(SkillService skillService,
|
||||
SkillReferenceService skillReferenceService,
|
||||
SkillScriptService skillScriptService,
|
||||
SkillAssetService skillAssetService,
|
||||
DBSkillContentStore contentStore,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
this.skillService = skillService;
|
||||
this.skillReferenceService = skillReferenceService;
|
||||
this.skillScriptService = skillScriptService;
|
||||
this.skillAssetService = skillAssetService;
|
||||
this.contentStore = contentStore;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<SkillFileNode> tree(BigInteger skillId) {
|
||||
Skill skill = requireReadableSkill(skillId);
|
||||
List<SkillFileNode> roots = new ArrayList<>();
|
||||
roots.add(fileNode(SkillPaths.SKILL_FILE, "SKILL.md", SkillFileType.SKILL.name()));
|
||||
roots.add(directoryNode(SkillPaths.REFERENCES_DIR, skill.getReferences().stream()
|
||||
.map(item -> fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.REFERENCE.name()))
|
||||
.toList()));
|
||||
roots.add(directoryNode(SkillPaths.SCRIPTS_DIR, skill.getScripts().stream()
|
||||
.map(item -> {
|
||||
SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.SCRIPT.name());
|
||||
node.setLanguage(item.getLanguage());
|
||||
return node;
|
||||
})
|
||||
.toList()));
|
||||
roots.add(directoryNode(SkillPaths.ASSETS_DIR, skill.getAssets().stream()
|
||||
.map(item -> {
|
||||
SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.ASSET.name());
|
||||
node.setMediaType(item.getMediaType());
|
||||
node.setSize(item.getSize());
|
||||
return node;
|
||||
})
|
||||
.toList()));
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SkillFileContent getContent(BigInteger skillId, String path) {
|
||||
Skill skill = requireReadableSkill(skillId);
|
||||
String normalizedPath = SkillPaths.normalize(path);
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(SkillPaths.SKILL_FILE);
|
||||
content.setType(SkillFileType.SKILL.name());
|
||||
content.setContent(skill.getSkillContent());
|
||||
content.setSize((long) bytes(skill.getSkillContent()).length);
|
||||
return content;
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
SkillReference reference = requireReference(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(reference.getPath());
|
||||
content.setType(SkillFileType.REFERENCE.name());
|
||||
content.setContent(reference.getContent());
|
||||
content.setSize(reference.getSize());
|
||||
return content;
|
||||
}
|
||||
if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
SkillScript script = requireScript(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(script.getPath());
|
||||
content.setType(SkillFileType.SCRIPT.name());
|
||||
content.setContent(script.getContent());
|
||||
content.setLanguage(script.getLanguage());
|
||||
content.setSize(script.getSize());
|
||||
return content;
|
||||
}
|
||||
if (SkillPaths.ASSETS_DIR.equals(topDir)) {
|
||||
SkillAsset asset = requireAsset(skillId, normalizedPath);
|
||||
SkillFileContent content = new SkillFileContent();
|
||||
content.setPath(asset.getPath());
|
||||
content.setType(SkillFileType.ASSET.name());
|
||||
content.setMediaType(asset.getMediaType());
|
||||
content.setSize(asset.getSize());
|
||||
content.setDownloadUrl("/api/v1/skill/file/asset");
|
||||
return content;
|
||||
}
|
||||
throw new BusinessException("不支持的 Skill 文件路径");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SkillFileContent saveContent(SkillFileSaveRequest request) {
|
||||
if (request == null || request.getSkillId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill skill = requireManageSkill(request.getSkillId());
|
||||
String normalizedPath = SkillPaths.normalize(request.getPath());
|
||||
String content = request.getContent() == null ? "" : request.getContent();
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
skill.setSkillContent(content);
|
||||
skillService.updateDraft(skill);
|
||||
return getContent(skill.getId(), SkillPaths.SKILL_FILE);
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
saveReference(skill, normalizedPath, content);
|
||||
refreshSkillCounts(skill.getId());
|
||||
return getContent(skill.getId(), normalizedPath);
|
||||
}
|
||||
if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
saveScript(skill, normalizedPath, content);
|
||||
refreshSkillCounts(skill.getId());
|
||||
return getContent(skill.getId(), normalizedPath);
|
||||
}
|
||||
throw new BusinessException("仅支持保存 SKILL.md、references/*.md 和 scripts/*.py|*.js|*.sh");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteFile(BigInteger skillId, String path) {
|
||||
requireManageSkill(skillId);
|
||||
String normalizedPath = SkillPaths.normalize(path);
|
||||
if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
|
||||
throw new BusinessException("SKILL.md 不允许删除");
|
||||
}
|
||||
String topDir = SkillPaths.firstSegment(normalizedPath);
|
||||
if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
|
||||
skillReferenceService.remove(QueryWrapper.create().eq(SkillReference::getSkillId, skillId).eq(SkillReference::getPath, normalizedPath));
|
||||
} else if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
|
||||
skillScriptService.remove(QueryWrapper.create().eq(SkillScript::getSkillId, skillId).eq(SkillScript::getPath, normalizedPath));
|
||||
} else if (SkillPaths.ASSETS_DIR.equals(topDir)) {
|
||||
skillAssetService.remove(QueryWrapper.create().eq(SkillAsset::getSkillId, skillId).eq(SkillAsset::getPath, normalizedPath));
|
||||
} else {
|
||||
throw new BusinessException("不支持的 Skill 文件路径");
|
||||
}
|
||||
refreshSkillCounts(skillId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public SkillFileContent uploadAsset(BigInteger skillId, String path, MultipartFile file) {
|
||||
Skill skill = requireManageSkill(skillId);
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("asset 文件不能为空");
|
||||
}
|
||||
String normalizedPath = normalizeAssetPath(path, file.getOriginalFilename());
|
||||
try {
|
||||
String contentRef = contentStore.put(file.getBytes());
|
||||
String hash = contentRef.substring("sha256:".length());
|
||||
SkillAsset asset = findAsset(skillId, normalizedPath);
|
||||
if (asset == null) {
|
||||
asset = new SkillAsset();
|
||||
asset.setTenantId(skill.getTenantId());
|
||||
asset.setSkillId(skillId);
|
||||
asset.setPath(normalizedPath);
|
||||
}
|
||||
asset.setName(SkillPaths.fileName(normalizedPath));
|
||||
asset.setMediaType(detectMediaType(normalizedPath));
|
||||
asset.setContentRef(contentRef);
|
||||
asset.setContentHash(hash);
|
||||
asset.setSize(file.getSize());
|
||||
if (asset.getId() == null) {
|
||||
skillAssetService.save(asset);
|
||||
} else {
|
||||
skillAssetService.updateById(asset);
|
||||
}
|
||||
refreshSkillCounts(skillId);
|
||||
return getContent(skillId, normalizedPath);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 asset 文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public InputStream openAsset(BigInteger skillId, String path) {
|
||||
requireReadableSkill(skillId);
|
||||
SkillAsset asset = requireAsset(skillId, SkillPaths.normalize(path));
|
||||
return contentStore.open(asset.getContentRef());
|
||||
}
|
||||
|
||||
private Skill requireReadableSkill(BigInteger skillId) {
|
||||
Skill skill = requireSkill(skillId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
|
||||
return skillService.getDetail(skillId);
|
||||
}
|
||||
|
||||
private Skill requireManageSkill(BigInteger skillId) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
return skill;
|
||||
}
|
||||
|
||||
private Skill requireSkill(BigInteger skillId) {
|
||||
if (skillId == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill skill = skillService.getById(skillId);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
private void saveReference(Skill skill, String path, String content) {
|
||||
if (!SkillPaths.hasExtension(path, ".md")) {
|
||||
throw new BusinessException("reference 仅支持 .md 文件");
|
||||
}
|
||||
SkillReference reference = findReference(skill.getId(), path);
|
||||
if (reference == null) {
|
||||
reference = new SkillReference();
|
||||
reference.setTenantId(skill.getTenantId());
|
||||
reference.setSkillId(skill.getId());
|
||||
reference.setPath(path);
|
||||
}
|
||||
byte[] bytes = bytes(content);
|
||||
reference.setName(SkillPaths.fileName(path));
|
||||
reference.setContent(content);
|
||||
reference.setContentHash(SkillHashes.sha256Hex(bytes));
|
||||
reference.setSize((long) bytes.length);
|
||||
if (reference.getId() == null) {
|
||||
skillReferenceService.save(reference);
|
||||
} else {
|
||||
skillReferenceService.updateById(reference);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveScript(Skill skill, String path, String content) {
|
||||
SkillScriptLanguage language = SkillScriptLanguage.fromPath(path);
|
||||
if (language == SkillScriptLanguage.UNKNOWN) {
|
||||
throw new BusinessException("script 仅支持 .py、.js、.sh 文件");
|
||||
}
|
||||
SkillScript script = findScript(skill.getId(), path);
|
||||
if (script == null) {
|
||||
script = new SkillScript();
|
||||
script.setTenantId(skill.getTenantId());
|
||||
script.setSkillId(skill.getId());
|
||||
script.setPath(path);
|
||||
}
|
||||
byte[] bytes = bytes(content);
|
||||
script.setLanguage(language.name());
|
||||
script.setContent(content);
|
||||
script.setContentHash(SkillHashes.sha256Hex(bytes));
|
||||
script.setSize((long) bytes.length);
|
||||
if (script.getId() == null) {
|
||||
skillScriptService.save(script);
|
||||
} else {
|
||||
skillScriptService.updateById(script);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshSkillCounts(BigInteger skillId) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
Skill update = new Skill();
|
||||
update.setId(skillId);
|
||||
update.setReferenceCount(skill.getReferences() == null ? 0 : skill.getReferences().size());
|
||||
update.setScriptCount(skill.getScripts() == null ? 0 : skill.getScripts().size());
|
||||
update.setAssetCount(skill.getAssets() == null ? 0 : skill.getAssets().size());
|
||||
skillService.updateById(update);
|
||||
}
|
||||
|
||||
private SkillReference findReference(BigInteger skillId, String path) {
|
||||
List<SkillReference> records = skillReferenceService.list(QueryWrapper.create()
|
||||
.eq(SkillReference::getSkillId, skillId)
|
||||
.eq(SkillReference::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillReference requireReference(BigInteger skillId, String path) {
|
||||
SkillReference reference = findReference(skillId, path);
|
||||
if (reference == null) {
|
||||
throw new BusinessException("reference 文件不存在");
|
||||
}
|
||||
return reference;
|
||||
}
|
||||
|
||||
private SkillScript findScript(BigInteger skillId, String path) {
|
||||
List<SkillScript> records = skillScriptService.list(QueryWrapper.create()
|
||||
.eq(SkillScript::getSkillId, skillId)
|
||||
.eq(SkillScript::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillScript requireScript(BigInteger skillId, String path) {
|
||||
SkillScript script = findScript(skillId, path);
|
||||
if (script == null) {
|
||||
throw new BusinessException("script 文件不存在");
|
||||
}
|
||||
return script;
|
||||
}
|
||||
|
||||
private SkillAsset findAsset(BigInteger skillId, String path) {
|
||||
List<SkillAsset> records = skillAssetService.list(QueryWrapper.create()
|
||||
.eq(SkillAsset::getSkillId, skillId)
|
||||
.eq(SkillAsset::getPath, path));
|
||||
return records.isEmpty() ? null : records.get(0);
|
||||
}
|
||||
|
||||
private SkillAsset requireAsset(BigInteger skillId, String path) {
|
||||
SkillAsset asset = findAsset(skillId, path);
|
||||
if (asset == null) {
|
||||
throw new BusinessException("asset 文件不存在");
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
private String normalizeAssetPath(String path, String originalFilename) {
|
||||
String effectivePath = path;
|
||||
if (effectivePath == null || effectivePath.isBlank()) {
|
||||
effectivePath = SkillPaths.ASSETS_DIR + "/" + (originalFilename == null ? "asset.bin" : originalFilename);
|
||||
}
|
||||
String normalized = SkillPaths.normalize(effectivePath);
|
||||
if (!SkillPaths.ASSETS_DIR.equals(SkillPaths.firstSegment(normalized))) {
|
||||
throw new BusinessException("asset 必须位于 assets/ 目录下");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private SkillFileNode directoryNode(String name, List<SkillFileNode> flatChildren) {
|
||||
SkillFileNode node = new SkillFileNode();
|
||||
node.setKey(name);
|
||||
node.setPath(name);
|
||||
node.setName(name + "/");
|
||||
node.setType("DIRECTORY");
|
||||
node.setChildren(toNestedChildren(name, flatChildren));
|
||||
return node;
|
||||
}
|
||||
|
||||
private List<SkillFileNode> toNestedChildren(String root, List<SkillFileNode> flatChildren) {
|
||||
Map<String, SkillFileNode> nodes = new LinkedHashMap<>();
|
||||
for (SkillFileNode child : flatChildren) {
|
||||
String[] segments = child.getPath().substring(root.length() + 1).split("/");
|
||||
String currentPath = root;
|
||||
for (int i = 0; i < segments.length; i++) {
|
||||
currentPath = currentPath + "/" + segments[i];
|
||||
boolean leaf = i == segments.length - 1;
|
||||
if (leaf) {
|
||||
nodes.put(currentPath, child);
|
||||
} else {
|
||||
String segmentName = segments[i];
|
||||
nodes.computeIfAbsent(currentPath, key -> {
|
||||
SkillFileNode directory = new SkillFileNode();
|
||||
directory.setKey(key);
|
||||
directory.setPath(key);
|
||||
directory.setName(segmentName + "/");
|
||||
directory.setType("DIRECTORY");
|
||||
return directory;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
List<SkillFileNode> roots = new ArrayList<>();
|
||||
for (SkillFileNode node : nodes.values()) {
|
||||
String parentPath = parentPath(node.getPath());
|
||||
if (root.equals(parentPath)) {
|
||||
roots.add(node);
|
||||
} else {
|
||||
SkillFileNode parent = nodes.get(parentPath);
|
||||
if (parent != null) {
|
||||
parent.getChildren().add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private SkillFileNode fileNode(String path, String name, String type) {
|
||||
SkillFileNode node = new SkillFileNode();
|
||||
node.setKey(path);
|
||||
node.setPath(path);
|
||||
node.setName(name);
|
||||
node.setType(type);
|
||||
return node;
|
||||
}
|
||||
|
||||
private String parentPath(String path) {
|
||||
int index = path.lastIndexOf('/');
|
||||
return index < 0 ? "" : path.substring(0, index);
|
||||
}
|
||||
|
||||
private byte[] bytes(String content) {
|
||||
return (content == null ? "" : content).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String detectMediaType(String path) {
|
||||
String mediaType = URLConnection.guessContentTypeFromName(path);
|
||||
return mediaType == null ? DEFAULT_MEDIA_TYPE : mediaType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.skill.file;
|
||||
|
||||
/**
|
||||
* Skill 逻辑文件类型。
|
||||
*/
|
||||
public enum SkillFileType {
|
||||
|
||||
/**
|
||||
* SKILL.md 主文件。
|
||||
*/
|
||||
SKILL,
|
||||
|
||||
/**
|
||||
* references/ 下的 Markdown 文档。
|
||||
*/
|
||||
REFERENCE,
|
||||
|
||||
/**
|
||||
* scripts/ 下的脚本文件。
|
||||
*/
|
||||
SCRIPT,
|
||||
|
||||
/**
|
||||
* assets/ 下的静态资产。
|
||||
*/
|
||||
ASSET
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Skill zip 导出服务。
|
||||
*/
|
||||
public interface SkillExportService {
|
||||
|
||||
/**
|
||||
* 导出一个或多个 Skill 为标准 zip 包。
|
||||
*
|
||||
* @param skillIds Skill ID 集合
|
||||
* @param outputStream zip 输出流
|
||||
*/
|
||||
void exportZip(Collection<BigInteger> skillIds, OutputStream outputStream);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import com.easyagents.skill.util.SkillPaths;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Skill zip 导出服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillExportServiceImpl implements SkillExportService {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
|
||||
/**
|
||||
* 创建 Skill 导出服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param contentStore Skill asset 内容存储
|
||||
*/
|
||||
public SkillExportServiceImpl(SkillService skillService, DBSkillContentStore contentStore) {
|
||||
this.skillService = skillService;
|
||||
this.contentStore = contentStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void exportZip(Collection<BigInteger> skillIds, OutputStream outputStream) {
|
||||
if (skillIds == null || skillIds.isEmpty()) {
|
||||
throw new BusinessException("请选择要导出的 Skill");
|
||||
}
|
||||
try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream, StandardCharsets.UTF_8)) {
|
||||
Set<String> folderNames = new LinkedHashSet<>();
|
||||
for (BigInteger skillId : skillIds) {
|
||||
Skill skill = skillService.getDetail(skillId);
|
||||
writeSkill(zipOutputStream, folderNames, skill);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("导出 Skill 失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSkill(ZipOutputStream zipOutputStream, Set<String> folderNames, Skill skill) throws IOException {
|
||||
String folder = uniqueFolderName(folderNames, skill.getName());
|
||||
writeText(zipOutputStream, folder + "/" + SkillPaths.SKILL_FILE, skill.getSkillContent());
|
||||
if (skill.getReferences() != null) {
|
||||
for (SkillReference reference : skill.getReferences()) {
|
||||
writeText(zipOutputStream, folder + "/" + reference.getPath(), reference.getContent());
|
||||
}
|
||||
}
|
||||
if (skill.getScripts() != null) {
|
||||
for (SkillScript script : skill.getScripts()) {
|
||||
writeText(zipOutputStream, folder + "/" + script.getPath(), script.getContent());
|
||||
}
|
||||
}
|
||||
if (skill.getAssets() != null) {
|
||||
for (SkillAsset asset : skill.getAssets()) {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(folder + "/" + asset.getPath()));
|
||||
zipOutputStream.write(contentStore.readAllBytes(asset.getContentRef()));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeText(ZipOutputStream zipOutputStream, String path, String content) throws IOException {
|
||||
zipOutputStream.putNextEntry(new ZipEntry(path));
|
||||
zipOutputStream.write((content == null ? "" : content).getBytes(StandardCharsets.UTF_8));
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
private String uniqueFolderName(Set<String> folderNames, String name) {
|
||||
String base = sanitizeFolderName(name);
|
||||
String candidate = base;
|
||||
int index = 2;
|
||||
while (!folderNames.add(candidate)) {
|
||||
candidate = base + "-" + index++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String sanitizeFolderName(String value) {
|
||||
String sanitized = value == null ? "skill" : value.trim().replaceAll("[\\\\/:*?\"<>|\\s]+", "-");
|
||||
sanitized = sanitized.replaceAll("^-+", "").replaceAll("-+$", "");
|
||||
return sanitized.isBlank() ? "skill" : sanitized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill 导入预览结果。
|
||||
*/
|
||||
public class SkillImportPreview {
|
||||
|
||||
private List<SkillImportPreviewItem> skills = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 获取导入 Skill 预览项。
|
||||
*
|
||||
* @return 预览项列表
|
||||
*/
|
||||
public List<SkillImportPreviewItem> getSkills() {
|
||||
return skills;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置导入 Skill 预览项。
|
||||
*
|
||||
* @param skills 预览项列表
|
||||
*/
|
||||
public void setSkills(List<SkillImportPreviewItem> skills) {
|
||||
this.skills = skills == null ? new ArrayList<>() : skills;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
/**
|
||||
* Skill 导入预览项。
|
||||
*/
|
||||
public class SkillImportPreviewItem {
|
||||
|
||||
private String packageId;
|
||||
private String name;
|
||||
private String description;
|
||||
private int referenceCount;
|
||||
private int scriptCount;
|
||||
private int assetCount;
|
||||
private boolean conflict;
|
||||
|
||||
/**
|
||||
* 获取包内 Skill ID。
|
||||
*
|
||||
* @return 包内 Skill ID
|
||||
*/
|
||||
public String getPackageId() {
|
||||
return packageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置包内 Skill ID。
|
||||
*
|
||||
* @param packageId 包内 Skill ID
|
||||
*/
|
||||
public void setPackageId(String packageId) {
|
||||
this.packageId = packageId;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public int getReferenceCount() { return referenceCount; }
|
||||
public void setReferenceCount(int referenceCount) { this.referenceCount = referenceCount; }
|
||||
public int getScriptCount() { return scriptCount; }
|
||||
public void setScriptCount(int scriptCount) { this.scriptCount = scriptCount; }
|
||||
public int getAssetCount() { return assetCount; }
|
||||
public void setAssetCount(int assetCount) { this.assetCount = assetCount; }
|
||||
public boolean isConflict() { return conflict; }
|
||||
public void setConflict(boolean conflict) { this.conflict = conflict; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill zip 导入服务。
|
||||
*/
|
||||
public interface SkillImportService {
|
||||
|
||||
/**
|
||||
* 预览 zip 中的 Skill 包。
|
||||
*
|
||||
* @param inputStream zip 输入流
|
||||
* @return 导入预览
|
||||
*/
|
||||
SkillImportPreview preview(InputStream inputStream);
|
||||
|
||||
/**
|
||||
* 确认导入 zip 中的 Skill 包。
|
||||
*
|
||||
* @param inputStream zip 输入流
|
||||
* @param categoryId 目标分类 ID,可为空
|
||||
* @param overwriteDraft 是否覆盖同名草稿
|
||||
* @return 已保存 Skill 列表
|
||||
*/
|
||||
List<Skill> importZip(InputStream inputStream, BigInteger categoryId, boolean overwriteDraft);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package tech.easyflow.skill.imports;
|
||||
|
||||
import com.easyagents.skill.codec.ZipSkillPackageCodec;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.store.DBSkillContentStore;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Skill zip 导入服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillImportServiceImpl implements SkillImportService {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final DBSkillContentStore contentStore;
|
||||
|
||||
/**
|
||||
* 创建 Skill 导入服务。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
* @param contentStore Skill asset 内容存储
|
||||
*/
|
||||
public SkillImportServiceImpl(SkillService skillService, DBSkillContentStore contentStore) {
|
||||
this.skillService = skillService;
|
||||
this.contentStore = contentStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public SkillImportPreview preview(InputStream inputStream) {
|
||||
List<com.easyagents.skill.model.Skill> importedSkills = new ZipSkillPackageCodec().importZip(inputStream);
|
||||
SkillImportPreview preview = new SkillImportPreview();
|
||||
preview.setSkills(importedSkills.stream().map(this::toPreviewItem).toList());
|
||||
return preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Skill> importZip(InputStream inputStream, BigInteger categoryId, boolean overwriteDraft) {
|
||||
List<com.easyagents.skill.model.Skill> importedSkills = new ZipSkillPackageCodec(contentStore).importZip(inputStream);
|
||||
List<Skill> savedSkills = new ArrayList<>();
|
||||
for (com.easyagents.skill.model.Skill imported : importedSkills) {
|
||||
Skill existing = findByName(imported.getName());
|
||||
if (existing != null) {
|
||||
if (!overwriteDraft || !PublishStatus.DRAFT.getCode().equals(existing.getPublishStatus())) {
|
||||
throw new BusinessException("Skill 已存在,且不允许覆盖:" + imported.getName());
|
||||
}
|
||||
Skill replacement = SkillModelConverter.fromAgentSkill(imported);
|
||||
replacement.setId(existing.getId());
|
||||
replacement.setCategoryId(categoryId);
|
||||
replacement.setSourceType("ZIP");
|
||||
replacement.setPackageHash(SkillHashes.sha256Hex(imported.getSkillContent().getBytes(StandardCharsets.UTF_8)));
|
||||
savedSkills.add(skillService.updateDraft(replacement));
|
||||
} else {
|
||||
Skill skill = SkillModelConverter.fromAgentSkill(imported);
|
||||
skill.setCategoryId(categoryId);
|
||||
skill.setSourceType("ZIP");
|
||||
skill.setPackageHash(SkillHashes.sha256Hex(imported.getSkillContent().getBytes(StandardCharsets.UTF_8)));
|
||||
savedSkills.add(skillService.saveDraft(skill));
|
||||
}
|
||||
}
|
||||
return savedSkills;
|
||||
}
|
||||
|
||||
private SkillImportPreviewItem toPreviewItem(com.easyagents.skill.model.Skill skill) {
|
||||
SkillImportPreviewItem item = new SkillImportPreviewItem();
|
||||
item.setPackageId(skill.getId());
|
||||
item.setName(skill.getName());
|
||||
item.setDescription(skill.getDescription());
|
||||
item.setReferenceCount(skill.getReferences().size());
|
||||
item.setScriptCount(skill.getScripts().size());
|
||||
item.setAssetCount(skill.getAssets().size());
|
||||
item.setConflict(findByName(skill.getName()) != null);
|
||||
return item;
|
||||
}
|
||||
|
||||
private Skill findByName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
List<Skill> skills = skillService.list(QueryWrapper.create().eq(Skill::getName, name));
|
||||
return skills.isEmpty() ? null : skills.get(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引 Mapper。
|
||||
*/
|
||||
public interface SkillAssetContentMapper extends BaseMapper<SkillAssetContent> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
|
||||
/**
|
||||
* Skill asset Mapper。
|
||||
*/
|
||||
public interface SkillAssetMapper extends BaseMapper<SkillAsset> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
|
||||
/**
|
||||
* Skill 分类 Mapper。
|
||||
*/
|
||||
public interface SkillCategoryMapper extends BaseMapper<SkillCategory> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
/**
|
||||
* Skill Mapper。
|
||||
*/
|
||||
public interface SkillMapper extends BaseMapper<Skill> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
|
||||
/**
|
||||
* Skill reference Mapper。
|
||||
*/
|
||||
public interface SkillReferenceMapper extends BaseMapper<SkillReference> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
/**
|
||||
* Skill script Mapper。
|
||||
*/
|
||||
public interface SkillScriptMapper extends BaseMapper<SkillScript> {
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package tech.easyflow.skill.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.AbstractAiResourceLifecycleHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 审批资源处理器。
|
||||
*/
|
||||
@Component
|
||||
public class SkillApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Skill> {
|
||||
|
||||
private final SkillService skillService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 审批资源处理器。
|
||||
*
|
||||
* @param approvalInstanceService 审批实例服务
|
||||
* @param objectMapper JSON 映射器
|
||||
* @param skillService Skill 服务
|
||||
* @param resourceAccessService 资源访问服务
|
||||
*/
|
||||
public SkillApprovalSubjectHandler(ApprovalInstanceService approvalInstanceService,
|
||||
ObjectMapper objectMapper,
|
||||
SkillService skillService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.skillService = skillService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String resourceType() {
|
||||
return ApprovalResourceType.SKILL.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Skill skill = skillService.getById(String.valueOf(identifier));
|
||||
if (skill == null || !PublishStatus.from(skill.getPublishStatus()).isExternallyVisible()
|
||||
|| skill.getPublishedSnapshotJson() == null || skill.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException(denyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skill requireResource(BigInteger resourceId) {
|
||||
Skill skill = skillService.getById(resourceId);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertManagePermission(Skill resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, resource, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Skill resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Skill resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Skill resource) {
|
||||
return resource.getDisplayName() == null || resource.getDisplayName().isBlank()
|
||||
? resource.getName()
|
||||
: resource.getDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Skill resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Skill resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Skill resource) {
|
||||
return skillService.buildPublishSnapshot(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(publishStatus.getCode());
|
||||
skill.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
skill.setPublishedSnapshotJson(resourceSnapshot);
|
||||
skill.setPublishedAt(new Date());
|
||||
skill.setPublishedBy(operatorId);
|
||||
skill.setCurrentApprovalInstanceId(null);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Skill skill = new Skill();
|
||||
skill.setId(resourceId);
|
||||
skill.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
skill.setCurrentApprovalInstanceId(null);
|
||||
skillService.updateById(skill);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
skillService.removeAggregate(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "Skill";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.skill.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.publish.AiResourceLifecycleService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 发布生命周期应用服务。
|
||||
*/
|
||||
@Service
|
||||
public class SkillPublishAppService {
|
||||
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
/**
|
||||
* 创建 Skill 发布应用服务。
|
||||
*
|
||||
* @param aiResourceLifecycleService AI 资源生命周期服务
|
||||
*/
|
||||
public SkillPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 发布审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.PUBLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 下线审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.OFFLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Skill 删除审批。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.DELETE);
|
||||
}
|
||||
|
||||
private ApprovalActionResult submit(BigInteger id, ApprovalActionType actionType) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("Skill 审批时资源ID不能为空");
|
||||
}
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.SKILL.getCode(),
|
||||
id,
|
||||
actionType.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package tech.easyflow.skill.repository;
|
||||
|
||||
import com.easyagents.skill.model.SkillDescriptor;
|
||||
import com.easyagents.skill.repository.SkillRepository;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillService;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 基于数据库的 easy-agents-skill 仓储适配器。
|
||||
*/
|
||||
@Repository
|
||||
public class DBSkillRepository implements SkillRepository {
|
||||
|
||||
private final SkillService skillService;
|
||||
|
||||
/**
|
||||
* 创建数据库 Skill 仓储。
|
||||
*
|
||||
* @param skillService Skill 服务
|
||||
*/
|
||||
public DBSkillRepository(SkillService skillService) {
|
||||
this.skillService = skillService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void save(com.easyagents.skill.model.Skill skill) {
|
||||
Skill entity = SkillModelConverter.fromAgentSkill(skill);
|
||||
BigInteger parsedId = tryParseId(skill.getId());
|
||||
if (parsedId != null && skillService.getById(parsedId) != null) {
|
||||
entity.setId(parsedId);
|
||||
skillService.updateDraft(entity);
|
||||
return;
|
||||
}
|
||||
skillService.saveDraft(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Optional<com.easyagents.skill.model.Skill> get(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
Skill skill = skillService.getDetail(id);
|
||||
return Optional.of(SkillModelConverter.toAgentSkill(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Optional<SkillDescriptor> getDescriptor(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
Skill skill = skillService.getById(id);
|
||||
if (skill == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new SkillDescriptor(String.valueOf(skill.getId()), skill.getName(), skill.getDescription(),
|
||||
new com.easyagents.skill.model.SkillMetadata(skill.getMetadataJson())));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<SkillDescriptor> listDescriptors() {
|
||||
return skillService.list().stream()
|
||||
.map(skill -> new SkillDescriptor(String.valueOf(skill.getId()), skill.getName(), skill.getDescription(),
|
||||
new com.easyagents.skill.model.SkillMetadata(skill.getMetadataJson())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void delete(String skillId) {
|
||||
skillService.removeAggregate(parseId(skillId));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean exists(String skillId) {
|
||||
BigInteger id = parseId(skillId);
|
||||
return skillService.count(QueryWrapper.create().eq(Skill::getId, id)) > 0;
|
||||
}
|
||||
|
||||
private BigInteger parseId(String skillId) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
return new BigInteger(skillId);
|
||||
}
|
||||
|
||||
private BigInteger tryParseId(String skillId) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigInteger(skillId);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Skill 审批展示态派生服务。
|
||||
*/
|
||||
public interface SkillApprovalStateService {
|
||||
|
||||
/**
|
||||
* 填充单个 Skill 审批展示态。
|
||||
*
|
||||
* @param skill Skill
|
||||
*/
|
||||
void fillSkillApprovalState(Skill skill);
|
||||
|
||||
/**
|
||||
* 批量填充 Skill 审批展示态。
|
||||
*
|
||||
* @param skills Skill 集合
|
||||
*/
|
||||
void fillSkillApprovalState(Collection<Skill> skills);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引服务。
|
||||
*/
|
||||
public interface SkillAssetContentService extends IService<SkillAssetContent> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
|
||||
/**
|
||||
* Skill asset 服务。
|
||||
*/
|
||||
public interface SkillAssetService extends IService<SkillAsset> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Skill 分类服务。
|
||||
*/
|
||||
public interface SkillCategoryService extends IService<SkillCategory> {
|
||||
|
||||
/**
|
||||
* 校验目标分类可作为 Skill 分类。
|
||||
*
|
||||
* @param categoryId 分类 ID,可为空
|
||||
*/
|
||||
void validateUsableCategory(BigInteger categoryId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
|
||||
/**
|
||||
* Skill reference 服务。
|
||||
*/
|
||||
public interface SkillReferenceService extends IService<SkillReference> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
/**
|
||||
* Skill script 服务。
|
||||
*/
|
||||
public interface SkillScriptService extends IService<SkillScript> {
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.skill.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 业务服务。
|
||||
*/
|
||||
public interface SkillService extends IService<Skill> {
|
||||
|
||||
/**
|
||||
* 获取 Skill 详情。
|
||||
*
|
||||
* @param id Skill ID
|
||||
* @return Skill 详情
|
||||
*/
|
||||
Skill getDetail(BigInteger id);
|
||||
|
||||
/**
|
||||
* 保存 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 保存后的 Skill
|
||||
*/
|
||||
Skill saveDraft(Skill skill);
|
||||
|
||||
/**
|
||||
* 更新 Skill 草稿。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 更新后的 Skill
|
||||
*/
|
||||
Skill updateDraft(Skill skill);
|
||||
|
||||
/**
|
||||
* 构建发布快照。
|
||||
*
|
||||
* @param skill Skill 草稿
|
||||
* @return 发布快照
|
||||
*/
|
||||
Map<String, Object> buildPublishSnapshot(Skill skill);
|
||||
|
||||
/**
|
||||
* 从发布快照还原 Skill。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @return Skill
|
||||
*/
|
||||
Skill fromSnapshot(Map<String, Object> snapshot);
|
||||
|
||||
/**
|
||||
* 删除 Skill 聚合。
|
||||
*
|
||||
* @param id Skill ID
|
||||
*/
|
||||
void removeAggregate(BigInteger id);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.service.SkillApprovalStateService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Skill 审批展示态派生服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillApprovalStateServiceImpl implements SkillApprovalStateService {
|
||||
|
||||
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
/**
|
||||
* 创建 Skill 审批展示态派生服务。
|
||||
*
|
||||
* @param approvalInstanceMapper 审批实例 Mapper
|
||||
*/
|
||||
public SkillApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillSkillApprovalState(Skill skill) {
|
||||
fillSkillApprovalState(skill == null ? List.of() : List.of(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillSkillApprovalState(Collection<Skill> skills) {
|
||||
if (CollectionUtils.isEmpty(skills)) {
|
||||
return;
|
||||
}
|
||||
List<Skill> validSkills = skills.stream().filter(Objects::nonNull).toList();
|
||||
if (validSkills.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validSkills);
|
||||
for (Skill skill : validSkills) {
|
||||
fillOne(skill, instanceMap.get(skill.getCurrentApprovalInstanceId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillOne(Skill skill, ApprovalInstance instance) {
|
||||
PublishStatus currentStatus = PublishStatus.from(skill.getPublishStatus());
|
||||
if (!isValidCurrentInstance(instance)) {
|
||||
skill.setApprovalPending(false);
|
||||
skill.setCurrentApprovalActionType(null);
|
||||
skill.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||
if (instanceStatus.isFinished()) {
|
||||
skill.setApprovalPending(false);
|
||||
skill.setCurrentApprovalActionType(null);
|
||||
skill.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||
skill.setApprovalPending(true);
|
||||
skill.setCurrentApprovalActionType(actionType.getCode());
|
||||
skill.setDisplayPublishStatus(resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||
}
|
||||
|
||||
private Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<Skill> skills) {
|
||||
Set<BigInteger> instanceIds = skills.stream()
|
||||
.map(Skill::getCurrentApprovalInstanceId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (instanceIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||
);
|
||||
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private boolean isValidCurrentInstance(ApprovalInstance instance) {
|
||||
return instance != null && ApprovalResourceType.SKILL.getCode().equals(instance.getResourceType());
|
||||
}
|
||||
|
||||
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||
ApprovalActionType actionType) {
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
return currentStatus;
|
||||
}
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
import tech.easyflow.skill.mapper.SkillAssetContentMapper;
|
||||
import tech.easyflow.skill.service.SkillAssetContentService;
|
||||
|
||||
/**
|
||||
* Skill asset 内容索引服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillAssetContentServiceImpl extends ServiceImpl<SkillAssetContentMapper, SkillAssetContent> implements SkillAssetContentService {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.mapper.SkillAssetMapper;
|
||||
import tech.easyflow.skill.service.SkillAssetService;
|
||||
|
||||
/**
|
||||
* Skill asset 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillAssetServiceImpl extends ServiceImpl<SkillAssetMapper, SkillAsset> implements SkillAssetService {
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.SkillCategory;
|
||||
import tech.easyflow.skill.mapper.SkillCategoryMapper;
|
||||
import tech.easyflow.skill.service.SkillCategoryService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Skill 分类服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillCategoryServiceImpl extends ServiceImpl<SkillCategoryMapper, SkillCategory> implements SkillCategoryService {
|
||||
|
||||
private static final int MAX_LEVEL = 3;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void validateUsableCategory(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return;
|
||||
}
|
||||
SkillCategory category = getById(categoryId);
|
||||
if (category == null) {
|
||||
throw new BusinessException("Skill 分类不存在");
|
||||
}
|
||||
if (category.getLevelNo() != null && category.getLevelNo() > MAX_LEVEL) {
|
||||
throw new BusinessException("Skill 分类最多支持三级");
|
||||
}
|
||||
if (category.getStatus() != null && category.getStatus() != 1) {
|
||||
throw new BusinessException("Skill 分类不可用");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean save(SkillCategory entity) {
|
||||
applyCategoryFields(entity);
|
||||
return super.save(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean updateById(SkillCategory entity) {
|
||||
applyCategoryFields(entity);
|
||||
return super.updateById(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断分类下是否存在子分类。
|
||||
*
|
||||
* @param categoryId 分类 ID
|
||||
* @return 存在子分类时返回 true
|
||||
*/
|
||||
public boolean hasChildren(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return false;
|
||||
}
|
||||
return count(QueryWrapper.create().eq(SkillCategory::getParentId, categoryId)) > 0;
|
||||
}
|
||||
|
||||
private void applyCategoryFields(SkillCategory category) {
|
||||
if (category == null) {
|
||||
throw new BusinessException("Skill 分类不能为空");
|
||||
}
|
||||
if (category.getCategoryName() == null || category.getCategoryName().isBlank()) {
|
||||
throw new BusinessException("Skill 分类名称不能为空");
|
||||
}
|
||||
SkillCategory parent = null;
|
||||
if (category.getParentId() != null) {
|
||||
parent = getById(category.getParentId());
|
||||
if (parent == null) {
|
||||
throw new BusinessException("父级 Skill 分类不存在");
|
||||
}
|
||||
if (category.getId() != null && category.getId().equals(category.getParentId())) {
|
||||
throw new BusinessException("父级分类不能是自身");
|
||||
}
|
||||
}
|
||||
int level = parent == null ? 1 : (parent.getLevelNo() == null ? 1 : parent.getLevelNo()) + 1;
|
||||
if (level > MAX_LEVEL) {
|
||||
throw new BusinessException("Skill 分类最多支持三级");
|
||||
}
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
Date now = new Date();
|
||||
category.setLevelNo(level);
|
||||
category.setAncestors(parent == null ? "" : appendAncestor(parent));
|
||||
category.setStatus(category.getStatus() == null ? 1 : category.getStatus());
|
||||
category.setSortNo(category.getSortNo() == null ? 0 : category.getSortNo());
|
||||
if (category.getId() == null) {
|
||||
category.setTenantId(account.getTenantId());
|
||||
category.setCreated(now);
|
||||
category.setCreatedBy(account.getId());
|
||||
}
|
||||
category.setModified(now);
|
||||
category.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private String appendAncestor(SkillCategory parent) {
|
||||
String ancestors = parent.getAncestors() == null || parent.getAncestors().isBlank()
|
||||
? String.valueOf(parent.getId())
|
||||
: parent.getAncestors() + "," + parent.getId();
|
||||
if (ancestors.length() > 512) {
|
||||
throw new BusinessException("Skill 分类层级路径过长");
|
||||
}
|
||||
return ancestors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.mapper.SkillReferenceMapper;
|
||||
import tech.easyflow.skill.service.SkillReferenceService;
|
||||
|
||||
/**
|
||||
* Skill reference 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillReferenceServiceImpl extends ServiceImpl<SkillReferenceMapper, SkillReference> implements SkillReferenceService {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.mapper.SkillScriptMapper;
|
||||
import tech.easyflow.skill.service.SkillScriptService;
|
||||
|
||||
/**
|
||||
* Skill script 服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillScriptServiceImpl extends ServiceImpl<SkillScriptMapper, SkillScript> implements SkillScriptService {
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package tech.easyflow.skill.service.impl;
|
||||
|
||||
import com.easyagents.skill.exception.SkillException;
|
||||
import com.easyagents.skill.factory.SkillFactory;
|
||||
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
import tech.easyflow.skill.mapper.SkillMapper;
|
||||
import tech.easyflow.skill.service.*;
|
||||
import tech.easyflow.skill.support.SkillModelConverter;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Skill 业务服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SkillServiceImpl extends ServiceImpl<SkillMapper, Skill> implements SkillService {
|
||||
|
||||
private final DefaultSkillValidator skillValidator = new DefaultSkillValidator();
|
||||
|
||||
@Resource
|
||||
private SkillCategoryService skillCategoryService;
|
||||
@Resource
|
||||
private SkillReferenceService skillReferenceService;
|
||||
@Resource
|
||||
private SkillScriptService skillScriptService;
|
||||
@Resource
|
||||
private SkillAssetService skillAssetService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Skill getDetail(BigInteger id) {
|
||||
Skill skill = requireSkill(id);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
|
||||
fillResources(skill);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Skill saveDraft(Skill skill) {
|
||||
validateDraft(skill);
|
||||
applyDraftDefaults(skill);
|
||||
save(skill);
|
||||
replaceResources(skill);
|
||||
return getDetail(skill.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Skill updateDraft(Skill skill) {
|
||||
if (skill == null || skill.getId() == null) {
|
||||
throw new BusinessException("Skill ID 不能为空");
|
||||
}
|
||||
Skill existing = requireSkill(skill.getId());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.SKILL, existing, ResourceAction.MANAGE, "无权限管理该 Skill");
|
||||
validateDraft(skill);
|
||||
applyDraftUpdate(existing, skill);
|
||||
updateById(existing);
|
||||
replaceResources(existing);
|
||||
return getDetail(existing.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> buildPublishSnapshot(Skill skill) {
|
||||
Skill detail = getDetail(skill.getId());
|
||||
com.easyagents.skill.model.Skill agentSkill = SkillModelConverter.toAgentSkill(detail);
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", detail.getId());
|
||||
snapshot.put("tenantId", detail.getTenantId());
|
||||
snapshot.put("deptId", detail.getDeptId());
|
||||
snapshot.put("createdBy", detail.getCreatedBy());
|
||||
snapshot.put("categoryId", detail.getCategoryId());
|
||||
snapshot.put("name", detail.getName());
|
||||
snapshot.put("displayName", detail.getDisplayName());
|
||||
snapshot.put("description", detail.getDescription());
|
||||
snapshot.put("metadataJson", detail.getMetadataJson());
|
||||
snapshot.put("skillContent", detail.getSkillContent());
|
||||
snapshot.put("enabled", detail.getEnabled());
|
||||
snapshot.put("visibilityScope", detail.getVisibilityScope());
|
||||
snapshot.put("sourceType", detail.getSourceType());
|
||||
snapshot.put("packageHash", detail.getPackageHash());
|
||||
snapshot.put("references", agentSkill.getReferences());
|
||||
snapshot.put("scripts", agentSkill.getScripts());
|
||||
snapshot.put("assets", agentSkill.getAssets());
|
||||
snapshot.put("snapshotAt", new Date());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Skill fromSnapshot(Map<String, Object> snapshot) {
|
||||
if (snapshot == null || snapshot.isEmpty()) {
|
||||
throw new BusinessException("Skill 发布快照为空");
|
||||
}
|
||||
Skill skill = objectMapper.convertValue(snapshot, Skill.class);
|
||||
skill.setId(toBigInteger(snapshot.get("id")));
|
||||
skill.setTenantId(toBigInteger(snapshot.get("tenantId")));
|
||||
skill.setDeptId(toBigInteger(snapshot.get("deptId")));
|
||||
skill.setCreatedBy(toBigInteger(snapshot.get("createdBy")));
|
||||
skill.setCategoryId(toBigInteger(snapshot.get("categoryId")));
|
||||
skill.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
skill.setPublishedSnapshotJson(snapshot);
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void removeAggregate(BigInteger id) {
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
removeResources(id);
|
||||
removeById(id);
|
||||
}
|
||||
|
||||
private Skill requireSkill(BigInteger id) {
|
||||
Skill skill = getById(id);
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不存在");
|
||||
}
|
||||
return skill;
|
||||
}
|
||||
|
||||
private void validateDraft(Skill skill) {
|
||||
if (skill == null) {
|
||||
throw new BusinessException("Skill 不能为空");
|
||||
}
|
||||
normalizeFromSkillContent(skill);
|
||||
if (skill.getName() == null || skill.getName().isBlank()) {
|
||||
throw new BusinessException("Skill 名称不能为空");
|
||||
}
|
||||
if (skill.getDescription() == null || skill.getDescription().isBlank()) {
|
||||
throw new BusinessException("Skill 描述不能为空");
|
||||
}
|
||||
if (skill.getSkillContent() == null || skill.getSkillContent().isBlank()) {
|
||||
throw new BusinessException("SKILL.md 内容不能为空");
|
||||
}
|
||||
skillCategoryService.validateUsableCategory(skill.getCategoryId());
|
||||
validateTargetCategoryVisible(skill.getCategoryId());
|
||||
skill.setVisibilityScope(VisibilityScope.fromOrDefault(skill.getVisibilityScope(), VisibilityScope.PRIVATE).name());
|
||||
validateSkillPackage(skill);
|
||||
}
|
||||
|
||||
private void validateTargetCategoryVisible(BigInteger categoryId) {
|
||||
if (categoryId == null) {
|
||||
return;
|
||||
}
|
||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.SKILL.getCode());
|
||||
if (access.isRestricted() && !access.getCategoryIds().contains(categoryId)) {
|
||||
throw new BusinessException("无权限使用该 Skill 分类");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDraftDefaults(Skill skill) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
skill.setTenantId(account.getTenantId());
|
||||
skill.setDeptId(account.getDeptId());
|
||||
skill.setCreated(now);
|
||||
skill.setCreatedBy(account.getId());
|
||||
skill.setModified(now);
|
||||
skill.setModifiedBy(account.getId());
|
||||
skill.setEnabled(skill.getEnabled() == null || skill.getEnabled());
|
||||
skill.setSourceType(skill.getSourceType() == null ? "MANUAL" : skill.getSourceType());
|
||||
skill.setPublishStatus(PublishStatus.DRAFT.getCode());
|
||||
syncCounts(skill);
|
||||
}
|
||||
|
||||
private void applyDraftUpdate(Skill existing, Skill incoming) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
existing.setCategoryId(incoming.getCategoryId());
|
||||
existing.setName(incoming.getName());
|
||||
existing.setDisplayName(incoming.getDisplayName());
|
||||
existing.setDescription(incoming.getDescription());
|
||||
existing.setMetadataJson(incoming.getMetadataJson());
|
||||
existing.setSkillContent(incoming.getSkillContent());
|
||||
existing.setEnabled(incoming.getEnabled() == null || incoming.getEnabled());
|
||||
existing.setVisibilityScope(incoming.getVisibilityScope());
|
||||
existing.setSourceType(incoming.getSourceType());
|
||||
existing.setPackageHash(incoming.getPackageHash());
|
||||
existing.setReferences(incoming.getReferences());
|
||||
existing.setScripts(incoming.getScripts());
|
||||
existing.setAssets(incoming.getAssets());
|
||||
syncCounts(existing);
|
||||
existing.setModified(new Date());
|
||||
existing.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private void syncCounts(Skill skill) {
|
||||
skill.setReferenceCount(skill.getReferences() == null ? 0 : skill.getReferences().size());
|
||||
skill.setScriptCount(skill.getScripts() == null ? 0 : skill.getScripts().size());
|
||||
skill.setAssetCount(skill.getAssets() == null ? 0 : skill.getAssets().size());
|
||||
}
|
||||
|
||||
private void fillResources(Skill skill) {
|
||||
skill.setReferences(skillReferenceService.list(QueryWrapper.create().eq(SkillReference::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
skill.setScripts(skillScriptService.list(QueryWrapper.create().eq(SkillScript::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
skill.setAssets(skillAssetService.list(QueryWrapper.create().eq(SkillAsset::getSkillId, skill.getId()).orderBy("path asc")));
|
||||
}
|
||||
|
||||
private void replaceResources(Skill skill) {
|
||||
removeResources(skill.getId());
|
||||
BigInteger tenantId = skill.getTenantId();
|
||||
BigInteger skillId = skill.getId();
|
||||
if (skill.getReferences() != null) {
|
||||
for (SkillReference reference : skill.getReferences()) {
|
||||
reference.setTenantId(tenantId);
|
||||
reference.setSkillId(skillId);
|
||||
skillReferenceService.save(reference);
|
||||
}
|
||||
}
|
||||
if (skill.getScripts() != null) {
|
||||
for (SkillScript script : skill.getScripts()) {
|
||||
script.setTenantId(tenantId);
|
||||
script.setSkillId(skillId);
|
||||
skillScriptService.save(script);
|
||||
}
|
||||
}
|
||||
if (skill.getAssets() != null) {
|
||||
for (SkillAsset asset : skill.getAssets()) {
|
||||
asset.setTenantId(tenantId);
|
||||
asset.setSkillId(skillId);
|
||||
skillAssetService.save(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeResources(BigInteger skillId) {
|
||||
skillReferenceService.remove(QueryWrapper.create().eq(SkillReference::getSkillId, skillId));
|
||||
skillScriptService.remove(QueryWrapper.create().eq(SkillScript::getSkillId, skillId));
|
||||
skillAssetService.remove(QueryWrapper.create().eq(SkillAsset::getSkillId, skillId));
|
||||
}
|
||||
|
||||
private void normalizeFromSkillContent(Skill skill) {
|
||||
try {
|
||||
com.easyagents.skill.model.Skill parsed = SkillFactory.create(
|
||||
skill.getId() == null ? "draft" : String.valueOf(skill.getId()),
|
||||
skill.getSkillContent(),
|
||||
SkillModelConverter.toAgentReferences(skill.getReferences()),
|
||||
SkillModelConverter.toAgentScripts(skill.getScripts()),
|
||||
SkillModelConverter.toAgentAssets(skill.getAssets())
|
||||
);
|
||||
skill.setName(parsed.getName());
|
||||
if (skill.getDisplayName() == null || skill.getDisplayName().isBlank()) {
|
||||
skill.setDisplayName(parsed.getName());
|
||||
}
|
||||
skill.setDescription(parsed.getDescription());
|
||||
skill.setMetadataJson(parsed.getMetadata().getValues());
|
||||
} catch (SkillException e) {
|
||||
throw new BusinessException("SKILL.md frontmatter 不合法:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSkillPackage(Skill skill) {
|
||||
try {
|
||||
skillValidator.validate(SkillModelConverter.toAgentSkill(skill));
|
||||
} catch (SkillException e) {
|
||||
throw new BusinessException("Skill 包校验失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
if (account == null || account.getId() == null) {
|
||||
throw new BusinessException("未登录或登录态无效");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
private BigInteger toBigInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigInteger bigInteger) {
|
||||
return bigInteger;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
return new BigInteger(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package tech.easyflow.skill.store;
|
||||
|
||||
import com.easyagents.skill.store.SkillContentStore;
|
||||
import com.easyagents.skill.util.SkillHashes;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.filestorage.FileStorageService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.skill.entity.SkillAssetContent;
|
||||
import tech.easyflow.skill.service.SkillAssetContentService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 基于数据库索引和现有文件存储的 Skill 内容存储。
|
||||
*/
|
||||
@Component
|
||||
public class DBSkillContentStore implements SkillContentStore {
|
||||
|
||||
private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
|
||||
|
||||
private final SkillAssetContentService skillAssetContentService;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
/**
|
||||
* 创建数据库 Skill 内容存储。
|
||||
*
|
||||
* @param skillAssetContentService asset 内容索引服务
|
||||
* @param fileStorageService 文件存储服务
|
||||
*/
|
||||
public DBSkillContentStore(SkillAssetContentService skillAssetContentService,
|
||||
@Qualifier("default") FileStorageService fileStorageService) {
|
||||
this.skillAssetContentService = skillAssetContentService;
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String put(byte[] bytes) {
|
||||
String contentRef = SkillHashes.sha256Ref(bytes);
|
||||
SkillAssetContent existing = skillAssetContentService.getById(contentRef);
|
||||
if (existing != null) {
|
||||
existing.setRefCount((existing.getRefCount() == null ? 0 : existing.getRefCount()) + 1);
|
||||
existing.setModified(new Date());
|
||||
skillAssetContentService.updateById(existing);
|
||||
return contentRef;
|
||||
}
|
||||
String hex = contentRef.substring("sha256:".length());
|
||||
String prePath = "skill-assets/" + hex.substring(0, Math.min(2, hex.length()));
|
||||
MultipartFile file = new ByteArrayMultipartFile(bytes, contentRef + ".bin", DEFAULT_MEDIA_TYPE);
|
||||
String filePath = fileStorageService.save(file, prePath);
|
||||
SkillAssetContent content = new SkillAssetContent();
|
||||
content.setContentRef(contentRef);
|
||||
content.setContentHash(hex);
|
||||
content.setFilePath(filePath);
|
||||
content.setMediaType(DEFAULT_MEDIA_TYPE);
|
||||
content.setSize((long) (bytes == null ? 0 : bytes.length));
|
||||
content.setRefCount(1);
|
||||
content.setCreated(new Date());
|
||||
content.setModified(new Date());
|
||||
skillAssetContentService.save(content);
|
||||
return contentRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public InputStream open(String contentRef) {
|
||||
SkillAssetContent content = requireContent(contentRef);
|
||||
try {
|
||||
return fileStorageService.readStream(content.getFilePath());
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 Skill asset 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public byte[] readAllBytes(String contentRef) {
|
||||
try (InputStream inputStream = open(contentRef)) {
|
||||
return inputStream.readAllBytes();
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("读取 Skill asset 失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean exists(String contentRef) {
|
||||
if (contentRef == null || contentRef.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return skillAssetContentService.count(QueryWrapper.create().eq(SkillAssetContent::getContentRef, contentRef)) > 0;
|
||||
}
|
||||
|
||||
private SkillAssetContent requireContent(String contentRef) {
|
||||
SkillAssetContent content = skillAssetContentService.getById(contentRef);
|
||||
if (content == null) {
|
||||
throw new BusinessException("Skill asset 内容不存在");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private static class ByteArrayMultipartFile implements MultipartFile {
|
||||
|
||||
private final byte[] bytes;
|
||||
private final String filename;
|
||||
private final String contentType;
|
||||
|
||||
private ByteArrayMultipartFile(byte[] bytes, String filename, String contentType) {
|
||||
this.bytes = bytes == null ? new byte[0] : bytes;
|
||||
this.filename = filename;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
@Override public String getName() { return "file"; }
|
||||
@Override public String getOriginalFilename() { return filename; }
|
||||
@Override public String getContentType() { return contentType; }
|
||||
@Override public boolean isEmpty() { return bytes.length == 0; }
|
||||
@Override public long getSize() { return bytes.length; }
|
||||
@Override public byte[] getBytes() { return bytes; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(bytes); }
|
||||
@Override public void transferTo(File dest) throws IOException { org.springframework.util.FileCopyUtils.copy(bytes, dest); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package tech.easyflow.skill.support;
|
||||
|
||||
import com.easyagents.skill.factory.SkillFactory;
|
||||
import com.easyagents.skill.model.SkillMetadata;
|
||||
import com.easyagents.skill.model.SkillScriptLanguage;
|
||||
import tech.easyflow.skill.entity.Skill;
|
||||
import tech.easyflow.skill.entity.SkillAsset;
|
||||
import tech.easyflow.skill.entity.SkillReference;
|
||||
import tech.easyflow.skill.entity.SkillScript;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* EasyFlow Skill 实体与 easy-agents-skill 模型转换器。
|
||||
*/
|
||||
public final class SkillModelConverter {
|
||||
|
||||
private SkillModelConverter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 easy-agents-skill 聚合。
|
||||
*
|
||||
* @param skill Skill 主实体
|
||||
* @return easy-agents-skill 聚合
|
||||
*/
|
||||
public static com.easyagents.skill.model.Skill toAgentSkill(Skill skill) {
|
||||
return SkillFactory.create(
|
||||
String.valueOf(skill.getId()),
|
||||
skill.getSkillContent(),
|
||||
toAgentReferences(skill.getReferences()),
|
||||
toAgentScripts(skill.getScripts()),
|
||||
toAgentAssets(skill.getAssets())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入模型到主实体。
|
||||
*
|
||||
* @param imported 导入 Skill
|
||||
* @return Skill 主实体
|
||||
*/
|
||||
public static Skill fromAgentSkill(com.easyagents.skill.model.Skill imported) {
|
||||
Skill skill = new Skill();
|
||||
skill.setName(imported.getName());
|
||||
skill.setDisplayName(imported.getName());
|
||||
skill.setDescription(imported.getDescription());
|
||||
skill.setMetadataJson(imported.getMetadata().getValues());
|
||||
skill.setSkillContent(imported.getSkillContent());
|
||||
skill.setReferences(imported.getReferences().stream()
|
||||
.map(item -> fromAgentReference(null, null, item))
|
||||
.toList());
|
||||
skill.setScripts(imported.getScripts().stream()
|
||||
.map(item -> fromAgentScript(null, null, item))
|
||||
.toList());
|
||||
skill.setAssets(imported.getAssets().stream()
|
||||
.map(item -> fromAgentAsset(null, null, item))
|
||||
.toList());
|
||||
skill.setReferenceCount(imported.getReferences().size());
|
||||
skill.setScriptCount(imported.getScripts().size());
|
||||
skill.setAssetCount(imported.getAssets().size());
|
||||
return skill;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 reference 列表。
|
||||
*
|
||||
* @param references reference 实体
|
||||
* @return easy-agents-skill reference
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillReference> toAgentReferences(List<SkillReference> references) {
|
||||
return references == null ? List.of() : references.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillReference target = new com.easyagents.skill.model.SkillReference();
|
||||
target.setPath(item.getPath());
|
||||
target.setName(item.getName());
|
||||
target.setContent(item.getContent());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 script 列表。
|
||||
*
|
||||
* @param scripts script 实体
|
||||
* @return easy-agents-skill script
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillScript> toAgentScripts(List<SkillScript> scripts) {
|
||||
return scripts == null ? List.of() : scripts.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillScript target = new com.easyagents.skill.model.SkillScript();
|
||||
target.setPath(item.getPath());
|
||||
target.setLanguage(parseLanguage(item.getLanguage()));
|
||||
target.setContent(item.getContent());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 asset 列表。
|
||||
*
|
||||
* @param assets asset 实体
|
||||
* @return easy-agents-skill asset
|
||||
*/
|
||||
public static List<com.easyagents.skill.model.SkillAsset> toAgentAssets(List<SkillAsset> assets) {
|
||||
return assets == null ? List.of() : assets.stream().map(item -> {
|
||||
com.easyagents.skill.model.SkillAsset target = new com.easyagents.skill.model.SkillAsset();
|
||||
target.setPath(item.getPath());
|
||||
target.setName(item.getName());
|
||||
target.setMediaType(item.getMediaType());
|
||||
target.setContentRef(item.getContentRef());
|
||||
target.setContentHash(item.getContentHash());
|
||||
target.setSize(item.getSize() == null ? 0L : item.getSize());
|
||||
target.setMetadata(new SkillMetadata(item.getMetadataJson()));
|
||||
return target;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 reference。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return reference 实体
|
||||
*/
|
||||
public static SkillReference fromAgentReference(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillReference source) {
|
||||
SkillReference target = new SkillReference();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setName(source.getName());
|
||||
target.setContent(source.getContent());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 script。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return script 实体
|
||||
*/
|
||||
public static SkillScript fromAgentScript(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillScript source) {
|
||||
SkillScript target = new SkillScript();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setLanguage(source.getLanguage().name());
|
||||
target.setContent(source.getContent());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换导入 asset。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param source 导入模型
|
||||
* @return asset 实体
|
||||
*/
|
||||
public static SkillAsset fromAgentAsset(BigInteger skillId, BigInteger tenantId,
|
||||
com.easyagents.skill.model.SkillAsset source) {
|
||||
SkillAsset target = new SkillAsset();
|
||||
target.setTenantId(tenantId);
|
||||
target.setSkillId(skillId);
|
||||
target.setPath(source.getPath());
|
||||
target.setName(source.getName());
|
||||
target.setMediaType(source.getMediaType());
|
||||
target.setContentRef(source.getContentRef());
|
||||
target.setContentHash(source.getContentHash());
|
||||
target.setSize(source.getSize());
|
||||
target.setMetadataJson(source.getMetadata().getValues());
|
||||
return target;
|
||||
}
|
||||
|
||||
private static SkillScriptLanguage parseLanguage(String language) {
|
||||
if (language == null || language.isBlank()) {
|
||||
return SkillScriptLanguage.UNKNOWN;
|
||||
}
|
||||
try {
|
||||
return SkillScriptLanguage.valueOf(language);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return SkillScriptLanguage.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public enum CategoryResourceType {
|
||||
|
||||
BOT("BOT"),
|
||||
AGENT("AGENT"),
|
||||
SKILL("SKILL"),
|
||||
PLUGIN("PLUGIN"),
|
||||
WORKFLOW("WORKFLOW"),
|
||||
KNOWLEDGE("KNOWLEDGE"),
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<module>easyflow-module-chatlog</module>
|
||||
<module>easyflow-module-ai</module>
|
||||
<module>easyflow-module-agent</module>
|
||||
<module>easyflow-module-skill</module>
|
||||
<module>easyflow-module-job</module>
|
||||
<module>easyflow-module-datacenter</module>
|
||||
</modules>
|
||||
|
||||
@@ -56,6 +56,10 @@
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-auth</artifactId>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill_category` (
|
||||
`id` BIGINT NOT NULL COMMENT 'ID',
|
||||
`tenant_id` BIGINT NULL COMMENT '租户ID',
|
||||
`parent_id` BIGINT NULL COMMENT '父分类ID',
|
||||
`category_name` VARCHAR(128) NOT NULL COMMENT '分类名称',
|
||||
`level_no` INT DEFAULT 1 COMMENT '层级',
|
||||
`ancestors` VARCHAR(512) NULL COMMENT '祖级路径',
|
||||
`sort_no` INT DEFAULT 0 COMMENT '排序',
|
||||
`status` INT DEFAULT 1 COMMENT '状态',
|
||||
`created` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`created_by` BIGINT NULL COMMENT '创建人',
|
||||
`modified` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`modified_by` BIGINT NULL COMMENT '修改人',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_skill_category_tenant_parent` (`tenant_id`, `parent_id`, `status`, `sort_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill 分类';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill` (
|
||||
`id` BIGINT NOT NULL COMMENT 'ID',
|
||||
`tenant_id` BIGINT NULL COMMENT '租户ID',
|
||||
`dept_id` BIGINT NULL COMMENT '部门ID',
|
||||
`category_id` BIGINT NULL COMMENT '分类ID',
|
||||
`name` VARCHAR(128) NOT NULL COMMENT 'Skill 名称',
|
||||
`display_name` VARCHAR(128) NULL COMMENT '展示名称',
|
||||
`description` VARCHAR(1024) NULL COMMENT '描述',
|
||||
`metadata_json` JSON NULL COMMENT '元数据',
|
||||
`skill_content` MEDIUMTEXT NULL COMMENT 'SKILL.md 内容',
|
||||
`enabled` TINYINT(1) DEFAULT 1 COMMENT '是否启用',
|
||||
`visibility_scope` VARCHAR(32) NULL COMMENT '可见范围',
|
||||
`source_type` VARCHAR(32) NULL COMMENT '来源类型',
|
||||
`package_hash` VARCHAR(128) NULL COMMENT '包 hash',
|
||||
`reference_count` INT DEFAULT 0 COMMENT '引用文档数量',
|
||||
`script_count` INT DEFAULT 0 COMMENT '脚本数量',
|
||||
`asset_count` INT DEFAULT 0 COMMENT '资产数量',
|
||||
`publish_status` VARCHAR(32) DEFAULT 'DRAFT' COMMENT '发布状态',
|
||||
`current_approval_instance_id` BIGINT NULL COMMENT '当前审批实例ID',
|
||||
`published_snapshot_json` JSON NULL COMMENT '已发布快照',
|
||||
`published_at` DATETIME NULL COMMENT '发布时间',
|
||||
`published_by` BIGINT NULL COMMENT '发布人',
|
||||
`created` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`created_by` BIGINT NULL COMMENT '创建人',
|
||||
`modified` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
`modified_by` BIGINT NULL COMMENT '修改人',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_skill_tenant_name` (`tenant_id`, `name`),
|
||||
KEY `idx_skill_tenant_category` (`tenant_id`, `category_id`, `enabled`),
|
||||
KEY `idx_skill_publish_status` (`publish_status`),
|
||||
KEY `idx_skill_created_by` (`created_by`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill_reference` (
|
||||
`id` BIGINT NOT NULL COMMENT 'ID',
|
||||
`tenant_id` BIGINT NULL COMMENT '租户ID',
|
||||
`skill_id` BIGINT NOT NULL COMMENT 'Skill ID',
|
||||
`path` VARCHAR(512) NOT NULL COMMENT '逻辑路径',
|
||||
`name` VARCHAR(255) NULL COMMENT '文件名',
|
||||
`content` MEDIUMTEXT NULL COMMENT '内容',
|
||||
`content_hash` VARCHAR(128) NULL COMMENT '内容 hash',
|
||||
`size` BIGINT DEFAULT 0 COMMENT '大小',
|
||||
`metadata_json` JSON NULL COMMENT '元数据',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_skill_reference_path` (`skill_id`, `path`),
|
||||
KEY `idx_skill_reference_skill` (`skill_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill reference';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill_script` (
|
||||
`id` BIGINT NOT NULL COMMENT 'ID',
|
||||
`tenant_id` BIGINT NULL COMMENT '租户ID',
|
||||
`skill_id` BIGINT NOT NULL COMMENT 'Skill ID',
|
||||
`path` VARCHAR(512) NOT NULL COMMENT '逻辑路径',
|
||||
`language` VARCHAR(32) NULL COMMENT '脚本语言',
|
||||
`content` MEDIUMTEXT NULL COMMENT '内容',
|
||||
`content_hash` VARCHAR(128) NULL COMMENT '内容 hash',
|
||||
`size` BIGINT DEFAULT 0 COMMENT '大小',
|
||||
`metadata_json` JSON NULL COMMENT '元数据',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_skill_script_path` (`skill_id`, `path`),
|
||||
KEY `idx_skill_script_skill` (`skill_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill script';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill_asset` (
|
||||
`id` BIGINT NOT NULL COMMENT 'ID',
|
||||
`tenant_id` BIGINT NULL COMMENT '租户ID',
|
||||
`skill_id` BIGINT NOT NULL COMMENT 'Skill ID',
|
||||
`path` VARCHAR(512) NOT NULL COMMENT '逻辑路径',
|
||||
`name` VARCHAR(255) NULL COMMENT '文件名',
|
||||
`media_type` VARCHAR(128) NULL COMMENT '媒体类型',
|
||||
`content_ref` VARCHAR(128) NOT NULL COMMENT '内容引用',
|
||||
`content_hash` VARCHAR(128) NULL COMMENT '内容 hash',
|
||||
`size` BIGINT DEFAULT 0 COMMENT '大小',
|
||||
`metadata_json` JSON NULL COMMENT '元数据',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_skill_asset_path` (`skill_id`, `path`),
|
||||
KEY `idx_skill_asset_skill` (`skill_id`),
|
||||
KEY `idx_skill_asset_content_ref` (`content_ref`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill asset';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tb_skill_asset_content` (
|
||||
`content_ref` VARCHAR(128) NOT NULL COMMENT '内容引用',
|
||||
`content_hash` VARCHAR(128) NOT NULL COMMENT '内容 hash',
|
||||
`file_path` VARCHAR(1024) NOT NULL COMMENT '文件路径',
|
||||
`media_type` VARCHAR(128) NULL COMMENT '媒体类型',
|
||||
`size` BIGINT DEFAULT 0 COMMENT '大小',
|
||||
`ref_count` INT DEFAULT 0 COMMENT '引用数',
|
||||
`created` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`modified` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
PRIMARY KEY (`content_ref`),
|
||||
KEY `idx_skill_asset_content_hash` (`content_hash`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Skill asset 内容索引';
|
||||
@@ -0,0 +1,172 @@
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000001, 0, 0, '技能管理', '/ai/skill', '/ai/skill/SkillList', 'lucide:badge-check',
|
||||
1, '', 3, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, '管理端 Skill 管理菜单'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000001
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000011, 367400000000000001, 1, '查询', '', '', '',
|
||||
0, '/api/v1/skill/query', 1, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-查询'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000011
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000012, 367400000000000001, 1, '详情', '', '', '',
|
||||
0, '/api/v1/skill/getDetail', 2, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-详情'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000012
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000013, 367400000000000001, 1, '保存', '', '', '',
|
||||
0, '/api/v1/skill/save', 3, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-保存'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000013
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000014, 367400000000000001, 1, '更新', '', '', '',
|
||||
0, '/api/v1/skill/update', 4, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-更新'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000014
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000015, 367400000000000001, 1, '删除', '', '', '',
|
||||
0, '/api/v1/skill/remove', 5, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-删除'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000015
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000016, 367400000000000001, 1, '发布', '', '', '',
|
||||
0, '/api/v1/skill/submitPublishApproval', 6, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-发布审批'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000016
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000017, 367400000000000001, 1, '下线', '', '', '',
|
||||
0, '/api/v1/skill/submitOfflineApproval', 7, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-下线审批'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000017
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_menu` (
|
||||
`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`,
|
||||
`is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`
|
||||
)
|
||||
SELECT
|
||||
367400000000000018, 367400000000000001, 1, '删除审批', '', '', '',
|
||||
0, '/api/v1/skill/submitDeleteApproval', 8, 0, '2026-06-05 20:00:00', 1, '2026-06-05 20:00:00', 1, 'Skill-删除审批'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_menu` WHERE `id` = 367400000000000018
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000101, 1, 367400000000000001
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000001
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000111, 1, 367400000000000011
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000011
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000112, 1, 367400000000000012
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000012
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000113, 1, 367400000000000013
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000013
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000114, 1, 367400000000000014
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000014
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000115, 1, 367400000000000015
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000015
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000116, 1, 367400000000000016
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000016
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000117, 1, 367400000000000017
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000017
|
||||
);
|
||||
|
||||
INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`)
|
||||
SELECT 367400000000000118, 1, 367400000000000018
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `tb_sys_role_menu` WHERE `role_id` = 1 AND `menu_id` = 367400000000000018
|
||||
);
|
||||
@@ -28,6 +28,13 @@
|
||||
"@easyflow/styles": "workspace:*",
|
||||
"@easyflow/types": "workspace:*",
|
||||
"@easyflow/utils": "workspace:*",
|
||||
"@codemirror/commands": "^6.10.2",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language": "^6.12.2",
|
||||
"@codemirror/legacy-modes": "^6.5.1",
|
||||
"@codemirror/state": "^6.5.4",
|
||||
"@codemirror/view": "^6.39.15",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@tinyflow-ai/vue": "workspace:*",
|
||||
"@vueuse/core": "catalog:",
|
||||
|
||||
164
easyflow-ui-admin/app/src/components/editor/CodeMirrorEditor.vue
Normal file
164
easyflow-ui-admin/app/src/components/editor/CodeMirrorEditor.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import {onBeforeUnmount, onMounted, ref, shallowRef, watch} from 'vue';
|
||||
|
||||
import {defaultKeymap, history, historyKeymap, indentWithTab} from '@codemirror/commands';
|
||||
import {javascript} from '@codemirror/lang-javascript';
|
||||
import {python} from '@codemirror/lang-python';
|
||||
import {
|
||||
bracketMatching,
|
||||
defaultHighlightStyle,
|
||||
indentOnInput,
|
||||
StreamLanguage,
|
||||
syntaxHighlighting
|
||||
} from '@codemirror/language';
|
||||
import {shell} from '@codemirror/legacy-modes/mode/shell';
|
||||
import {EditorState, type Extension} from '@codemirror/state';
|
||||
import {
|
||||
drawSelection,
|
||||
EditorView,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
lineNumbers
|
||||
} from '@codemirror/view';
|
||||
|
||||
type EditorLanguage = 'javascript' | 'markdown' | 'python' | 'shell' | 'text';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string;
|
||||
language?: EditorLanguage;
|
||||
readonly?: boolean;
|
||||
}>(),
|
||||
{
|
||||
language: 'text',
|
||||
readonly: false,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const hostRef = ref<HTMLElement>();
|
||||
const viewRef = shallowRef<EditorView>();
|
||||
let syncingFromOutside = false;
|
||||
|
||||
function languageExtension(): Extension[] {
|
||||
if (props.language === 'python') {
|
||||
return [python()];
|
||||
}
|
||||
if (props.language === 'javascript') {
|
||||
return [javascript({ jsx: true, typescript: true })];
|
||||
}
|
||||
if (props.language === 'shell') {
|
||||
return [StreamLanguage.define(shell)];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createState(doc: string) {
|
||||
return EditorState.create({
|
||||
doc,
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
history(),
|
||||
drawSelection(),
|
||||
highlightActiveLine(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
EditorView.editable.of(!props.readonly),
|
||||
EditorState.readOnly.of(props.readonly),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (!update.docChanged || syncingFromOutside) return;
|
||||
emit('update:modelValue', update.state.doc.toString());
|
||||
}),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
height: '100%',
|
||||
fontSize: '13px',
|
||||
backgroundColor: 'var(--el-bg-color)',
|
||||
color: 'var(--el-text-color-primary)',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--el-fill-color-lighter)',
|
||||
borderRight: '1px solid var(--el-border-color-lighter)',
|
||||
color: 'var(--el-text-color-secondary)',
|
||||
},
|
||||
'.cm-activeLine': {
|
||||
backgroundColor: 'var(--el-fill-color-light)',
|
||||
},
|
||||
'.cm-activeLineGutter': {
|
||||
backgroundColor: 'var(--el-fill-color-light)',
|
||||
},
|
||||
}),
|
||||
...languageExtension(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function mountEditor() {
|
||||
if (!hostRef.value) return;
|
||||
viewRef.value = new EditorView({
|
||||
state: createState(props.modelValue || ''),
|
||||
parent: hostRef.value,
|
||||
});
|
||||
}
|
||||
|
||||
function recreateEditor() {
|
||||
const currentDoc = viewRef.value?.state.doc.toString() ?? props.modelValue ?? '';
|
||||
viewRef.value?.destroy();
|
||||
viewRef.value = undefined;
|
||||
if (!hostRef.value) return;
|
||||
viewRef.value = new EditorView({
|
||||
state: createState(currentDoc),
|
||||
parent: hostRef.value,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const view = viewRef.value;
|
||||
if (!view) return;
|
||||
const nextValue = value || '';
|
||||
const currentValue = view.state.doc.toString();
|
||||
if (nextValue === currentValue) return;
|
||||
syncingFromOutside = true;
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: currentValue.length, insert: nextValue },
|
||||
});
|
||||
syncingFromOutside = false;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => [props.language, props.readonly],
|
||||
() => recreateEditor(),
|
||||
);
|
||||
|
||||
onMounted(mountEditor);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
viewRef.value?.destroy();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="hostRef" class="code-mirror-editor"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.code-mirror-editor {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
18
easyflow-ui-admin/app/src/router/routes/modules/skill.ts
Normal file
18
easyflow-ui-admin/app/src/router/routes/modules/skill.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type {RouteRecordRaw} from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'SkillDetail',
|
||||
path: '/ai/skill/detail/:id',
|
||||
component: () => import('#/views/ai/skill/SkillDetail.vue'),
|
||||
meta: {
|
||||
title: '技能详情',
|
||||
openInNewWindow: true,
|
||||
hideInMenu: true,
|
||||
activePath: '/ai/skill',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
||||
5
easyflow-ui-admin/app/src/types/codemirror-legacy-modes.d.ts
vendored
Normal file
5
easyflow-ui-admin/app/src/types/codemirror-legacy-modes.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module '@codemirror/legacy-modes/mode/shell.js' {
|
||||
import type {StreamParser} from '@codemirror/language';
|
||||
|
||||
export const shell: StreamParser<unknown>;
|
||||
}
|
||||
2
easyflow-ui-admin/app/src/types/markdown-it.d.ts
vendored
Normal file
2
easyflow-ui-admin/app/src/types/markdown-it.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare module 'markdown-it';
|
||||
|
||||
398
easyflow-ui-admin/app/src/views/ai/skill/SkillDetail.vue
Normal file
398
easyflow-ui-admin/app/src/views/ai/skill/SkillDetail.vue
Normal file
@@ -0,0 +1,398 @@
|
||||
<script setup lang="ts">
|
||||
import type {SkillFileContent, SkillFileNode, SkillInfo} from './types';
|
||||
|
||||
import {computed, onMounted, ref, watch} from 'vue';
|
||||
import {useRoute, useRouter} from 'vue-router';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import {ArrowLeft, Check, Download, Promotion} from '@element-plus/icons-vue';
|
||||
import {ElButton, ElEmpty, ElImage, ElMessage, ElTag, ElTree,} from 'element-plus';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
import CodeMirrorEditor from '#/components/editor/CodeMirrorEditor.vue';
|
||||
|
||||
import {
|
||||
getSkillDetail,
|
||||
getSkillFileContent,
|
||||
getSkillFileTree,
|
||||
resolveSkillAssetUrl,
|
||||
saveSkill,
|
||||
saveSkillFile,
|
||||
submitSkillPublishApproval,
|
||||
} from './api';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const skillId = computed(() => String(route.params.id || 'new'));
|
||||
const isNew = computed(() => skillId.value === 'new');
|
||||
const skill = ref<SkillInfo>({
|
||||
displayName: '新建技能',
|
||||
skillContent: '---\nname: new-skill\ndescription: 新技能\n---\n\n# 新技能\n',
|
||||
visibilityScope: 'PRIVATE',
|
||||
enabled: true,
|
||||
});
|
||||
const fileTree = ref<SkillFileNode[]>([]);
|
||||
const selectedFile = ref<SkillFileContent>();
|
||||
const editorContent = ref('');
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
|
||||
const markdown = new MarkdownIt({
|
||||
breaks: true,
|
||||
html: false,
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
const renderedMarkdown = computed(() => {
|
||||
if (!isMarkdownFile.value) return '';
|
||||
return DOMPurify.sanitize(markdown.render(editorContent.value || ''));
|
||||
});
|
||||
|
||||
const isMarkdownFile = computed(() =>
|
||||
selectedFile.value?.type === 'SKILL' || selectedFile.value?.type === 'REFERENCE',
|
||||
);
|
||||
const isScriptFile = computed(() => selectedFile.value?.type === 'SCRIPT');
|
||||
const isAssetFile = computed(() => selectedFile.value?.type === 'ASSET');
|
||||
const assetUrl = computed(() =>
|
||||
selectedFile.value?.path && !isNew.value
|
||||
? resolveSkillAssetUrl(skillId.value, selectedFile.value.path)
|
||||
: '',
|
||||
);
|
||||
const isImageAsset = computed(() =>
|
||||
String(selectedFile.value?.mediaType || '').startsWith('image/') &&
|
||||
selectedFile.value?.mediaType !== 'image/svg+xml',
|
||||
);
|
||||
const isPdfAsset = computed(() => selectedFile.value?.mediaType === 'application/pdf');
|
||||
|
||||
onMounted(initPage);
|
||||
|
||||
watch(skillId, initPage);
|
||||
|
||||
async function initPage() {
|
||||
if (isNew.value) {
|
||||
const defaultContent = '---\nname: new-skill\ndescription: 新技能\n---\n\n# 新技能\n';
|
||||
skill.value = {
|
||||
displayName: '新建技能',
|
||||
skillContent: defaultContent,
|
||||
visibilityScope: 'PRIVATE',
|
||||
enabled: true,
|
||||
};
|
||||
selectedFile.value = { path: 'SKILL.md', type: 'SKILL', content: defaultContent };
|
||||
editorContent.value = defaultContent;
|
||||
fileTree.value = [
|
||||
{ key: 'SKILL.md', path: 'SKILL.md', name: 'SKILL.md', type: 'SKILL' },
|
||||
{ key: 'references', path: 'references', name: 'references/', type: 'DIRECTORY', children: [] },
|
||||
{ key: 'scripts', path: 'scripts', name: 'scripts/', type: 'DIRECTORY', children: [] },
|
||||
{ key: 'assets', path: 'assets', name: 'assets/', type: 'DIRECTORY', children: [] },
|
||||
];
|
||||
return;
|
||||
}
|
||||
await loadDetail();
|
||||
}
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const detailRes = await getSkillDetail(skillId.value);
|
||||
if (detailRes.errorCode === 0) {
|
||||
skill.value = detailRes.data;
|
||||
}
|
||||
await loadTree();
|
||||
await selectFile('SKILL.md');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
const res = await getSkillFileTree(skillId.value);
|
||||
if (res.errorCode === 0) {
|
||||
fileTree.value = res.data || [];
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFile(path: string) {
|
||||
if (isNew.value) return;
|
||||
const res = await getSkillFileContent(skillId.value, path);
|
||||
if (res.errorCode === 0) {
|
||||
selectedFile.value = res.data;
|
||||
editorContent.value = res.data.content || '';
|
||||
}
|
||||
}
|
||||
|
||||
function handleTreeNodeClick(node: SkillFileNode) {
|
||||
if (node.type === 'DIRECTORY') return;
|
||||
selectFile(node.path);
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true;
|
||||
try {
|
||||
if (isNew.value) {
|
||||
skill.value.skillContent = editorContent.value;
|
||||
const res = await saveSkill(skill.value);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success('保存成功');
|
||||
await router.replace(`/ai/skill/detail/${res.data.id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!selectedFile.value) return;
|
||||
if (isMarkdownFile.value || isScriptFile.value) {
|
||||
const res = await saveSkillFile(skillId.value, selectedFile.value.path, editorContent.value);
|
||||
if (res.errorCode === 0) {
|
||||
selectedFile.value = res.data;
|
||||
ElMessage.success('保存成功');
|
||||
await loadTree();
|
||||
if (selectedFile.value.path === 'SKILL.md') {
|
||||
await loadDetail();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish() {
|
||||
if (isNew.value) {
|
||||
ElMessage.warning('请先保存技能');
|
||||
return;
|
||||
}
|
||||
const res = await submitSkillPublishApproval(skillId.value);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || '操作成功');
|
||||
await loadDetail();
|
||||
}
|
||||
}
|
||||
|
||||
function editorLanguage() {
|
||||
const language = selectedFile.value?.language;
|
||||
if (language === 'PYTHON') return 'python';
|
||||
if (language === 'JAVASCRIPT') return 'javascript';
|
||||
if (language === 'SHELL') return 'shell';
|
||||
if (isMarkdownFile.value) return 'markdown';
|
||||
return 'text';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="skill-detail-page" v-loading="loading">
|
||||
<header class="skill-detail-page__header">
|
||||
<div class="skill-detail-page__title">
|
||||
<ElButton :icon="ArrowLeft" text @click="router.push('/ai/skill')" />
|
||||
<div>
|
||||
<h1>{{ skill.displayName || skill.name || '技能详情' }}</h1>
|
||||
<p>{{ skill.description || '未填写描述' }}</p>
|
||||
</div>
|
||||
<ElTag v-if="skill.displayPublishStatus || skill.publishStatus" size="small" effect="plain">
|
||||
{{ skill.displayPublishStatus || skill.publishStatus }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="skill-detail-page__actions">
|
||||
<ElButton :icon="Check" type="primary" :loading="saving" @click="handleSave">
|
||||
保存
|
||||
</ElButton>
|
||||
<ElButton :icon="Promotion" @click="handlePublish">发布</ElButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="skill-detail-page__body">
|
||||
<aside class="skill-detail-page__tree">
|
||||
<ElTree
|
||||
node-key="path"
|
||||
:data="fileTree"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleTreeNodeClick"
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<section class="skill-detail-page__workspace">
|
||||
<template v-if="selectedFile">
|
||||
<div class="skill-detail-page__filebar">
|
||||
<strong>{{ selectedFile.path }}</strong>
|
||||
<span v-if="selectedFile.size">{{ selectedFile.size }} bytes</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isMarkdownFile" class="skill-detail-page__split">
|
||||
<CodeMirrorEditor
|
||||
v-model="editorContent"
|
||||
class="skill-detail-page__editor"
|
||||
:language="editorLanguage()"
|
||||
/>
|
||||
<div class="skill-detail-page__preview markdown-body" v-html="renderedMarkdown"></div>
|
||||
</div>
|
||||
|
||||
<CodeMirrorEditor
|
||||
v-else-if="isScriptFile"
|
||||
v-model="editorContent"
|
||||
class="skill-detail-page__editor"
|
||||
:language="editorLanguage()"
|
||||
/>
|
||||
|
||||
<div v-else-if="isAssetFile" class="skill-detail-page__asset">
|
||||
<ElImage v-if="isImageAsset" :src="assetUrl" fit="contain" />
|
||||
<iframe v-else-if="isPdfAsset" :src="assetUrl"></iframe>
|
||||
<div v-else class="skill-detail-page__asset-info">
|
||||
<p>{{ selectedFile.mediaType || 'application/octet-stream' }}</p>
|
||||
<p>{{ selectedFile.size || 0 }} bytes</p>
|
||||
</div>
|
||||
<ElButton :icon="Download" tag="a" :href="assetUrl" target="_blank">
|
||||
下载
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<ElEmpty v-else />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.skill-detail-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.skill-detail-page__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.skill-detail-page__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.skill-detail-page__title h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 650;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.skill-detail-page__title p {
|
||||
margin: 2px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.skill-detail-page__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.skill-detail-page__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.skill-detail-page__tree {
|
||||
width: 280px;
|
||||
min-width: 240px;
|
||||
padding: 12px;
|
||||
overflow: auto;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.skill-detail-page__workspace {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.skill-detail-page__filebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.skill-detail-page__filebar span {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.skill-detail-page__split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skill-detail-page__editor {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.skill-detail-page__split .skill-detail-page__editor {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.skill-detail-page__preview {
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.skill-detail-page__asset {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.skill-detail-page__asset :deep(.el-image) {
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.skill-detail-page__asset iframe {
|
||||
width: 100%;
|
||||
height: 60vh;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.skill-detail-page__asset-info {
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3) {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(p) {
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
335
easyflow-ui-admin/app/src/views/ai/skill/SkillList.vue
Normal file
335
easyflow-ui-admin/app/src/views/ai/skill/SkillList.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<script setup lang="ts">
|
||||
import type {SkillInfo} from './types';
|
||||
import type {ActionButton, CardPrimaryAction} from '#/components/page/CardList.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
|
||||
import {markRaw, onMounted, ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
|
||||
import {Delete, Edit, Plus, Promotion, Upload} from '@element-plus/icons-vue';
|
||||
import {ElMessage, ElMessageBox, ElTag} from 'element-plus';
|
||||
import {tryit} from 'radash';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import {
|
||||
getSkillCategories,
|
||||
importSkillConfirm,
|
||||
importSkillPreview,
|
||||
submitSkillDeleteApproval,
|
||||
submitSkillOfflineApproval,
|
||||
submitSkillPublishApproval,
|
||||
} from './api';
|
||||
|
||||
const router = useRouter();
|
||||
const pageDataRef = ref();
|
||||
const sideList = ref<any[]>([]);
|
||||
const importInputRef = ref<HTMLInputElement>();
|
||||
const selectedCategoryId = ref<string>('');
|
||||
const SKILL_TAB_PAGE_KEY = '/ai/skill';
|
||||
|
||||
const headerButtons = [
|
||||
{
|
||||
key: 'create',
|
||||
text: '新建技能',
|
||||
icon: markRaw(Plus),
|
||||
type: 'primary',
|
||||
data: { action: 'create' },
|
||||
permission: '/api/v1/skill/save',
|
||||
},
|
||||
{
|
||||
key: 'import',
|
||||
text: '导入',
|
||||
icon: markRaw(Upload),
|
||||
type: 'default',
|
||||
data: { action: 'import' },
|
||||
permission: '/api/v1/skill/save',
|
||||
},
|
||||
];
|
||||
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: Edit,
|
||||
text: '编辑',
|
||||
permission: '/api/v1/skill/update',
|
||||
onClick(row: SkillInfo) {
|
||||
router.push({
|
||||
path: `/ai/skill/detail/${row.id}`,
|
||||
query: {
|
||||
pageKey: SKILL_TAB_PAGE_KEY,
|
||||
navTitle: row.displayName || row.name || '技能详情',
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row: SkillInfo) =>
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? '重新发布'
|
||||
: '发布',
|
||||
permission: '/api/v1/skill/save',
|
||||
placement: 'inline',
|
||||
visible: (row: SkillInfo) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: handlePublishAction,
|
||||
},
|
||||
{
|
||||
icon: Promotion,
|
||||
text: '下线',
|
||||
permission: '/api/v1/skill/save',
|
||||
placement: 'menu',
|
||||
visible: (row: SkillInfo) =>
|
||||
canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: handleOfflineAction,
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: '删除',
|
||||
permission: '/api/v1/skill/remove',
|
||||
placement: 'menu',
|
||||
tone: 'danger',
|
||||
visible: (row: SkillInfo) =>
|
||||
canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: handleDeleteAction,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(loadCategories);
|
||||
|
||||
function handleSearch(keyword: string) {
|
||||
pageDataRef.value?.setQuery({
|
||||
isQueryOr: true,
|
||||
name: keyword,
|
||||
displayName: keyword,
|
||||
description: keyword,
|
||||
});
|
||||
}
|
||||
|
||||
function handleButtonClick(payload: any) {
|
||||
if (payload?.key === 'create' || payload?.data?.action === 'create') {
|
||||
router.push({
|
||||
path: '/ai/skill/detail/new',
|
||||
query: { pageKey: SKILL_TAB_PAGE_KEY, navTitle: '新建技能' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (payload?.key === 'import' || payload?.data?.action === 'import') {
|
||||
importInputRef.value?.click();
|
||||
}
|
||||
}
|
||||
|
||||
function changeCategory(category: any) {
|
||||
selectedCategoryId.value = category.id || '';
|
||||
pageDataRef.value?.setQuery({ categoryId: category.id });
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
const [, res] = await tryit(getSkillCategories)();
|
||||
if (res?.errorCode === 0) {
|
||||
sideList.value = [
|
||||
{ id: '', categoryName: '全部分类' },
|
||||
...(res.data || []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImportFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
const preview = await importSkillPreview(file);
|
||||
if (preview.errorCode !== 0) return;
|
||||
const conflictCount = preview.data?.skills?.filter((item) => item.conflict).length || 0;
|
||||
const ok = await confirmAction(
|
||||
conflictCount > 0
|
||||
? `检测到 ${conflictCount} 个同名草稿,确认覆盖草稿并导入?`
|
||||
: '确认导入该 Skill zip?',
|
||||
conflictCount > 0 ? 'warning' : 'info',
|
||||
);
|
||||
if (!ok) return;
|
||||
const res = await importSkillConfirm(
|
||||
file,
|
||||
selectedCategoryId.value || undefined,
|
||||
conflictCount > 0,
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success('导入完成');
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePublishStatusMeta(displayPublishStatus?: string, publishStatus?: string) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'DELETE_PENDING':
|
||||
return { label: '删除中', type: 'danger' as const };
|
||||
case 'OFFLINE':
|
||||
return { label: '已下线', type: 'info' as const };
|
||||
case 'OFFLINE_PENDING':
|
||||
return { label: '下线中', type: 'warning' as const };
|
||||
case 'PUBLISH_PENDING':
|
||||
return { label: '发布中', type: 'warning' as const };
|
||||
case 'PUBLISHED':
|
||||
return { label: '已发布', type: 'success' as const };
|
||||
default:
|
||||
return { label: '草稿', type: 'info' as const };
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmAction(message: string, type: 'info' | 'warning' = 'info') {
|
||||
try {
|
||||
await ElMessageBox.confirm(message, '提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublishAction(row: SkillInfo) {
|
||||
if (isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)) {
|
||||
ElMessage.warning('当前技能正在审批中');
|
||||
return;
|
||||
}
|
||||
const ok = await confirmAction(
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? '确认提交重新发布审批?'
|
||||
: '确认提交发布审批?',
|
||||
);
|
||||
if (!ok) return;
|
||||
const res = await submitSkillPublishApproval(String(row.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || '操作成功');
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOfflineAction(row: SkillInfo) {
|
||||
const ok = await confirmAction('确认提交下线审批?', 'warning');
|
||||
if (!ok) return;
|
||||
const res = await submitSkillOfflineApproval(String(row.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || '操作成功');
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteAction(row: SkillInfo) {
|
||||
const ok = await confirmAction('确认提交删除审批?', 'warning');
|
||||
if (!ok) return;
|
||||
const res = await submitSkillDeleteApproval(String(row.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || '操作成功');
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="skill-list-page">
|
||||
<HeaderSearch
|
||||
:buttons="headerButtons"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
<input
|
||||
ref="importInputRef"
|
||||
class="skill-list-page__file"
|
||||
type="file"
|
||||
accept=".zip,application/zip"
|
||||
@change="handleImportFile"
|
||||
/>
|
||||
<div class="skill-list-page__body">
|
||||
<PageSide
|
||||
label-key="categoryName"
|
||||
value-key="id"
|
||||
:menus="sideList"
|
||||
@change="changeCategory"
|
||||
/>
|
||||
<div class="skill-list-page__content">
|
||||
<PageData
|
||||
ref="pageDataRef"
|
||||
page-url="/api/v1/skill/page"
|
||||
:page-sizes="[12, 18, 24]"
|
||||
:page-size="12"
|
||||
>
|
||||
<template #default="{ pageList }">
|
||||
<CardList
|
||||
title-field="displayName"
|
||||
desc-field="description"
|
||||
:data="pageList"
|
||||
:default-icon="''"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElTag
|
||||
size="small"
|
||||
effect="plain"
|
||||
round
|
||||
:type="
|
||||
resolvePublishStatusMeta(
|
||||
item.displayPublishStatus,
|
||||
item.publishStatus,
|
||||
).type
|
||||
"
|
||||
>
|
||||
{{
|
||||
resolvePublishStatusMeta(
|
||||
item.displayPublishStatus,
|
||||
item.publishStatus,
|
||||
).label
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.skill-list-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.skill-list-page__file {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skill-list-page__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 24px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.skill-list-page__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: calc(100vh - 192px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
106
easyflow-ui-admin/app/src/views/ai/skill/api.ts
Normal file
106
easyflow-ui-admin/app/src/views/ai/skill/api.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
RequestResult,
|
||||
SkillFileContent,
|
||||
SkillFileNode,
|
||||
SkillImportPreview,
|
||||
SkillInfo,
|
||||
} from './types';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
|
||||
export function getSkillDetail(id: number | string) {
|
||||
return api.get<RequestResult<SkillInfo>>('/api/v1/skill/getDetail', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export function saveSkill(skill: SkillInfo) {
|
||||
return api.post<RequestResult<SkillInfo>>('/api/v1/skill/save', skill);
|
||||
}
|
||||
|
||||
export function updateSkill(skill: SkillInfo) {
|
||||
return api.post<RequestResult<SkillInfo>>('/api/v1/skill/update', skill);
|
||||
}
|
||||
|
||||
export function getSkillCategories() {
|
||||
return api.get<RequestResult<any[]>>('/api/v1/skillCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export function submitSkillPublishApproval(id: number | string) {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/skill/submitPublishApproval',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function submitSkillOfflineApproval(id: number | string) {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/skill/submitOfflineApproval',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function submitSkillDeleteApproval(id: number | string) {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/skill/submitDeleteApproval',
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
export function getSkillFileTree(skillId: number | string) {
|
||||
return api.get<RequestResult<SkillFileNode[]>>('/api/v1/skill/file/tree', {
|
||||
params: { skillId },
|
||||
});
|
||||
}
|
||||
|
||||
export function getSkillFileContent(skillId: number | string, path: string) {
|
||||
return api.get<RequestResult<SkillFileContent>>('/api/v1/skill/file/content', {
|
||||
params: { skillId, path },
|
||||
});
|
||||
}
|
||||
|
||||
export function saveSkillFile(
|
||||
skillId: number | string,
|
||||
path: string,
|
||||
content: string,
|
||||
) {
|
||||
return api.post<RequestResult<SkillFileContent>>('/api/v1/skill/file/save', {
|
||||
skillId,
|
||||
path,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
export function importSkillPreview(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return api.postFile<RequestResult<SkillImportPreview>>(
|
||||
'/api/v1/skill/import/preview',
|
||||
formData,
|
||||
);
|
||||
}
|
||||
|
||||
export function importSkillConfirm(
|
||||
file: File,
|
||||
categoryId?: number | string,
|
||||
overwriteDraft = false,
|
||||
) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (categoryId) {
|
||||
formData.append('categoryId', String(categoryId));
|
||||
}
|
||||
formData.append('overwriteDraft', String(overwriteDraft));
|
||||
return api.postFile<RequestResult<SkillInfo[]>>(
|
||||
'/api/v1/skill/import/confirm',
|
||||
formData,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveSkillAssetUrl(skillId: number | string, path: string) {
|
||||
return `/api/v1/skill/file/asset?skillId=${encodeURIComponent(
|
||||
String(skillId),
|
||||
)}&path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
99
easyflow-ui-admin/app/src/views/ai/skill/types.ts
Normal file
99
easyflow-ui-admin/app/src/views/ai/skill/types.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
export interface RequestResult<T = any> {
|
||||
data: T;
|
||||
errorCode: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface SkillInfo {
|
||||
id?: number | string;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
categoryId?: number | string;
|
||||
metadataJson?: Record<string, any>;
|
||||
skillContent?: string;
|
||||
enabled?: boolean;
|
||||
visibilityScope?: string;
|
||||
sourceType?: string;
|
||||
referenceCount?: number;
|
||||
scriptCount?: number;
|
||||
assetCount?: number;
|
||||
publishStatus?: string;
|
||||
displayPublishStatus?: string;
|
||||
approvalPending?: boolean;
|
||||
currentApprovalActionType?: string;
|
||||
currentApprovalInstanceId?: number | string;
|
||||
created?: string;
|
||||
createdByName?: string;
|
||||
references?: SkillReference[];
|
||||
scripts?: SkillScript[];
|
||||
assets?: SkillAsset[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SkillReference {
|
||||
id?: number | string;
|
||||
path: string;
|
||||
name?: string;
|
||||
content?: string;
|
||||
contentHash?: string;
|
||||
size?: number;
|
||||
metadataJson?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SkillScript {
|
||||
id?: number | string;
|
||||
path: string;
|
||||
language?: string;
|
||||
content?: string;
|
||||
contentHash?: string;
|
||||
size?: number;
|
||||
metadataJson?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SkillAsset {
|
||||
id?: number | string;
|
||||
path: string;
|
||||
name?: string;
|
||||
mediaType?: string;
|
||||
contentRef?: string;
|
||||
contentHash?: string;
|
||||
size?: number;
|
||||
metadataJson?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SkillFileNode {
|
||||
key: string;
|
||||
path: string;
|
||||
name: string;
|
||||
type: 'ASSET' | 'DIRECTORY' | 'REFERENCE' | 'SCRIPT' | 'SKILL' | string;
|
||||
language?: string;
|
||||
mediaType?: string;
|
||||
size?: number;
|
||||
children?: SkillFileNode[];
|
||||
}
|
||||
|
||||
export interface SkillFileContent {
|
||||
path: string;
|
||||
type: 'ASSET' | 'REFERENCE' | 'SCRIPT' | 'SKILL' | string;
|
||||
content?: string;
|
||||
language?: string;
|
||||
mediaType?: string;
|
||||
size?: number;
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export interface SkillImportPreviewItem {
|
||||
packageId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
referenceCount: number;
|
||||
scriptCount: number;
|
||||
assetCount: number;
|
||||
conflict: boolean;
|
||||
}
|
||||
|
||||
export interface SkillImportPreview {
|
||||
skills: SkillImportPreviewItem[];
|
||||
}
|
||||
|
||||
164
easyflow-ui-admin/pnpm-lock.yaml
generated
164
easyflow-ui-admin/pnpm-lock.yaml
generated
@@ -531,10 +531,10 @@ importers:
|
||||
version: 24.10.1
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
|
||||
version: 6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx':
|
||||
specifier: 'catalog:'
|
||||
version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
|
||||
version: 5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))
|
||||
'@vue/test-utils':
|
||||
specifier: 'catalog:'
|
||||
version: 2.4.6
|
||||
@@ -576,10 +576,10 @@ importers:
|
||||
version: 3.6.1(sass@1.94.0)(typescript@5.9.3)(vue-tsc@2.2.10(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
vitest:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
vue:
|
||||
specifier: ^3.5.17
|
||||
version: 3.5.24(typescript@5.9.3)
|
||||
@@ -589,6 +589,27 @@ importers:
|
||||
|
||||
app:
|
||||
dependencies:
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.10.2
|
||||
version: 6.10.2
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.4
|
||||
version: 6.2.4
|
||||
'@codemirror/lang-python':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
'@codemirror/language':
|
||||
specifier: ^6.12.2
|
||||
version: 6.12.2
|
||||
'@codemirror/legacy-modes':
|
||||
specifier: ^6.5.1
|
||||
version: 6.5.3
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.4
|
||||
version: 6.5.4
|
||||
'@codemirror/view':
|
||||
specifier: ^6.39.15
|
||||
version: 6.39.15
|
||||
'@easyflow-core/shadcn-ui':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/@core/ui-kit/shadcn-ui
|
||||
@@ -2484,6 +2505,9 @@ packages:
|
||||
'@codemirror/language@6.12.2':
|
||||
resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
|
||||
|
||||
'@codemirror/legacy-modes@6.5.3':
|
||||
resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==}
|
||||
|
||||
'@codemirror/lint@6.9.4':
|
||||
resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
|
||||
|
||||
@@ -12143,6 +12167,10 @@ snapshots:
|
||||
'@lezer/lr': 1.4.8
|
||||
style-mod: 4.1.3
|
||||
|
||||
'@codemirror/legacy-modes@6.5.3':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.12.2
|
||||
|
||||
'@codemirror/lint@6.9.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.4
|
||||
@@ -14415,18 +14443,6 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.50
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
|
||||
vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
vue: 3.5.24(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
@@ -14439,12 +14455,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.29
|
||||
vite: 7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
vue: 3.5.24(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.29
|
||||
@@ -14465,14 +14475,6 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
@@ -14480,7 +14482,6 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -21323,27 +21324,6 @@ snapshots:
|
||||
dependencies:
|
||||
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
|
||||
vite-node@3.2.4(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -21364,7 +21344,6 @@ snapshots:
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
vite-plugin-compression@0.5.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
@@ -21494,23 +21473,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite@7.2.2(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.53.2
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
less: 4.4.2
|
||||
sass: 1.94.0
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.1
|
||||
|
||||
vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.3
|
||||
@@ -21545,23 +21507,6 @@ snapshots:
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.1
|
||||
|
||||
vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.53.2
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
fsevents: 2.3.3
|
||||
jiti: 1.21.7
|
||||
less: 4.4.2
|
||||
sass: 1.94.0
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.1
|
||||
|
||||
vite@7.3.1(@types/node@24.10.1)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.3
|
||||
@@ -21578,55 +21523,11 @@ snapshots:
|
||||
sass: 1.94.0
|
||||
terser: 5.44.1
|
||||
yaml: 2.8.1
|
||||
optional: true
|
||||
|
||||
vitefu@1.1.2(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)):
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@vitest/spy': 3.2.4
|
||||
'@vitest/utils': 3.2.4
|
||||
chai: 5.3.3
|
||||
debug: 4.4.3
|
||||
expect-type: 1.2.2
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.1(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
vite-node: 3.2.4(@types/node@24.10.1)(jiti@1.21.7)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.10.1
|
||||
happy-dom: 17.6.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@17.6.3)(jiti@2.6.1)(less@4.4.2)(sass@1.94.0)(terser@5.44.1)(yaml@2.8.1):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
@@ -21669,7 +21570,6 @@ snapshots:
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
optional: true
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12: {}
|
||||
|
||||
|
||||
10
pom.xml
10
pom.xml
@@ -206,6 +206,11 @@
|
||||
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||
<version>${easy-agents.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-skill</artifactId>
|
||||
<version>${easy-agents.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
@@ -451,6 +456,11 @@
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-skill</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-auth</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user