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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user