feat: 重构标准 Skill 管理与发布链路

- 统一标准 ZIP 导入导出与通用资源模型

- 收口分类范围权限和创建人查询

- 完善发布快照、审批幂等与数据库清理迁移
This commit is contained in:
2026-08-14 18:53:26 +08:00
parent 9be9bd7665
commit 77d66e1b42
91 changed files with 1660 additions and 10957 deletions

View File

@@ -1,12 +1,14 @@
package tech.easyflow.admin.controller.ai.support;
import org.springframework.stereotype.Component;
import com.mybatisflex.core.query.QueryWrapper;
import tech.easyflow.agent.entity.Agent;
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 tech.easyflow.system.entity.SysAccount;
import javax.annotation.Resource;
import java.math.BigInteger;
@@ -73,7 +75,38 @@ public class AiResourceCreatorNameSupport {
* @param skills Skill 集合
*/
public void fillSkillCreatorNames(Collection<Skill> skills) {
fillCreatorNames(skills, Skill::getCreatedBy, Skill::setCreatedByName);
if (skills == null || skills.isEmpty()) {
return;
}
LinkedHashSet<BigInteger> creatorIds = skills.stream().map(Skill::getCreatedBy)
.filter(Objects::nonNull)
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
if (creatorIds.isEmpty()) {
return;
}
Map<BigInteger, String> labels = sysAccountService.list(QueryWrapper.create()
.select(SysAccount::getId, SysAccount::getNickname, SysAccount::getLoginName)
.in(SysAccount::getId, creatorIds)).stream()
.collect(java.util.stream.Collectors.toMap(SysAccount::getId, this::skillCreatorLabel,
(left, right) -> left, java.util.LinkedHashMap::new));
skills.forEach(skill -> skill.setCreatedByName(
labels.getOrDefault(skill.getCreatedBy(), String.valueOf(skill.getCreatedBy()))));
}
/**
* 将 Skill 创建人格式化为“昵称(账号)”。
*
* @param account 创建人账号
* @return 创建人展示标签
*/
private String skillCreatorLabel(SysAccount account) {
String nickname = account.getNickname() == null ? "" : account.getNickname().trim();
String loginName = account.getLoginName() == null ? "" : account.getLoginName().trim();
if (nickname.isBlank()) {
return loginName.isBlank() ? String.valueOf(account.getId()) : loginName;
}
return loginName.isBlank() || nickname.equals(loginName)
? nickname : nickname + "" + loginName + "";
}
/**

View File

@@ -21,7 +21,6 @@ import tech.easyflow.system.service.CategoryPermissionService;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Locale;
@@ -77,14 +76,17 @@ public class SkillCategoryController {
}
}
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(resolveOrderBy(sortKey, sortType));
List<SkillCategory> categories = service.list(queryWrapper);
if (access.isRestricted()) {
Set<BigInteger> visibleIds = new java.util.LinkedHashSet<>(access.getCategoryIds());
categories.stream().filter(category -> access.getCategoryIds().contains(category.getId()))
.map(SkillCategory::getAncestors).filter(value -> value != null && !value.isBlank())
.flatMap(value -> java.util.Arrays.stream(value.split(",")))
.map(String::trim).filter(value -> !value.isBlank() && !"0".equals(value))
.map(BigInteger::new).forEach(visibleIds::add);
categories = categories.stream().filter(category -> visibleIds.contains(category.getId())).toList();
}
return Result.ok(Boolean.FALSE.equals(asTree) ? categories : toTree(categories));
}

View File

@@ -1,8 +1,6 @@
package tech.easyflow.admin.controller.skill;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.annotation.SaMode;
import cn.dev33.satoken.stp.StpUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse;
@@ -12,12 +10,12 @@ 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.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.admin.controller.skill.vo.SkillCapabilityBindingRequest;
import tech.easyflow.admin.controller.skill.vo.SkillCapabilityReplaceView;
import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
import tech.easyflow.admin.controller.skill.vo.SkillView;
import tech.easyflow.admin.controller.skill.vo.SkillPublishStatusView;
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
@@ -27,11 +25,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.util.SearchKeywordUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.skill.capability.SkillCapabilityBindingService;
import tech.easyflow.skill.capability.SkillCapabilityCandidate;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillCapabilityBinding;
import tech.easyflow.skill.enums.SkillCapabilityType;
import tech.easyflow.skill.file.SkillFileContent;
import tech.easyflow.skill.file.SkillFileNode;
import tech.easyflow.skill.file.SkillFileRenameRequest;
@@ -41,7 +35,6 @@ import tech.easyflow.skill.imports.SkillExportRequest;
import tech.easyflow.skill.imports.SkillExportArtifact;
import tech.easyflow.skill.imports.SkillExportService;
import tech.easyflow.skill.imports.SkillImportConfirmRequest;
import tech.easyflow.skill.imports.SkillImportFormat;
import tech.easyflow.skill.imports.SkillImportPreview;
import tech.easyflow.skill.imports.SkillImportService;
import tech.easyflow.skill.publish.SkillPublishAppService;
@@ -65,14 +58,14 @@ import java.util.Objects;
import java.util.Set;
/**
* Skill 管理端 API统一负责轻量查询、白名单写入、文件工作台、能力绑定和导入导出。
* Skill 管理端 API统一负责轻量查询、白名单写入、文件工作台和标准包导入导出。
*/
@RestController
@RequestMapping("/api/v1/skill")
public class SkillController {
private static final Set<String> PAGE_SORT_COLUMNS = Set.of(
"id", "name", "display_name", "created", "modified", "publish_status", "resource_count", "capability_count");
"id", "name", "display_name", "created", "modified", "publish_status");
private final SkillService skillService;
private final SkillApprovalStateService skillApprovalStateService;
@@ -80,7 +73,6 @@ public class SkillController {
private final SkillImportService skillImportService;
private final SkillExportService skillExportService;
private final SkillFileService skillFileService;
private final SkillCapabilityBindingService capabilityBindingService;
private final ResourceAccessService resourceAccessService;
private final CategoryPermissionService categoryPermissionService;
private final SkillVisibilityQueryHelper visibilityQueryHelper;
@@ -95,7 +87,6 @@ public class SkillController {
* @param skillImportService 导入服务
* @param skillExportService 导出服务
* @param skillFileService 文件服务
* @param capabilityBindingService 能力绑定服务
* @param resourceAccessService 资源权限服务
* @param categoryPermissionService 分类权限服务
* @param visibilityQueryHelper 可见性查询助手
@@ -107,7 +98,6 @@ public class SkillController {
SkillImportService skillImportService,
SkillExportService skillExportService,
SkillFileService skillFileService,
SkillCapabilityBindingService capabilityBindingService,
ResourceAccessService resourceAccessService,
CategoryPermissionService categoryPermissionService,
SkillVisibilityQueryHelper visibilityQueryHelper,
@@ -118,7 +108,6 @@ public class SkillController {
this.skillImportService = skillImportService;
this.skillExportService = skillExportService;
this.skillFileService = skillFileService;
this.capabilityBindingService = capabilityBindingService;
this.resourceAccessService = resourceAccessService;
this.categoryPermissionService = categoryPermissionService;
this.visibilityQueryHelper = visibilityQueryHelper;
@@ -134,9 +123,9 @@ public class SkillController {
* @param categoryScope 分类范围UNCATEGORIZED 表示未分类
* @param name 名称关键词
* @param displayName 展示名称关键词
* @param keyword 名称、用途或创建人模糊关键词
* @param publishStatus 发布状态
* @param sourceType 来源类型
* @param capabilityType 能力类型
* @param visibilityScope 使用范围
* @param sortKey 排序字段
* @param sortType 排序方向
* @return 轻量分页结果
@@ -144,8 +133,8 @@ public class SkillController {
@GetMapping("/page")
@SaCheckPermission("/api/v1/skill/query")
public Result<Page<SkillView>> page(Long pageNumber, Long pageSize, BigInteger categoryId, String categoryScope,
String name, String displayName, String publishStatus, String sourceType,
String capabilityType, String sortKey, String sortType) {
String name, String displayName, String keyword, String publishStatus,
String visibilityScope, String sortKey, String sortType) {
long normalizedPage = pageNumber == null || pageNumber < 1 ? 1 : pageNumber;
long normalizedSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
QueryWrapper query = descriptorQuery();
@@ -155,19 +144,16 @@ public class SkillController {
} else {
query.eq("category_id", categoryId, categoryId != null);
}
query
.eq("publish_status", publishStatus, hasText(publishStatus))
.eq("source_type", sourceType, hasText(sourceType));
String keyword = hasText(displayName) ? displayName : name;
if (hasText(keyword)) {
String pattern = SearchKeywordUtil.literalContainsPattern(keyword);
query.and("(name LIKE ? OR display_name LIKE ? OR description LIKE ?)", pattern, pattern, pattern);
}
if (hasText(capabilityType)) {
SkillCapabilityType normalizedCapabilityType = SkillCapabilityType.from(capabilityType);
query.and("EXISTS (SELECT 1 FROM tb_skill_capability_binding b "
+ "WHERE b.skill_id = tb_skill.id AND b.tenant_id = tb_skill.tenant_id "
+ "AND b.capability_type = ?)", normalizedCapabilityType.name());
query.eq("publish_status", publishStatus, hasText(publishStatus))
.eq("visibility_scope", visibilityScope, hasText(visibilityScope));
String effectiveKeyword = hasText(keyword) ? keyword : hasText(displayName) ? displayName : name;
if (hasText(effectiveKeyword)) {
String pattern = SearchKeywordUtil.literalContainsPattern(effectiveKeyword);
query.and("(name LIKE ? ESCAPE '\\\\' OR display_name LIKE ? ESCAPE '\\\\' "
+ "OR description LIKE ? ESCAPE '\\\\' OR EXISTS (SELECT 1 FROM tb_sys_account a "
+ "WHERE a.id = tb_skill.created_by AND a.tenant_id = tb_skill.tenant_id "
+ "AND (a.nickname LIKE ? ESCAPE '\\\\' OR a.login_name LIKE ? ESCAPE '\\\\'))) ",
pattern, pattern, pattern, pattern, pattern);
}
query.orderBy(resolveSortColumn(sortKey) + ("asc".equalsIgnoreCase(sortType) ? " asc" : " desc"));
Page<Skill> source = skillService.page(new Page<>(normalizedPage, normalizedSize), query);
@@ -189,10 +175,6 @@ public class SkillController {
@SaCheckPermission("/api/v1/skill/getDetail")
public Result<SkillView> detail(BigInteger id) {
Skill skill = skillService.getManagementDetail(id);
if (!StpUtil.hasPermission("/api/v1/skill/capability")) {
skill.setCapabilityBindings(null);
skill.setCapabilityHash(null);
}
fillListState(List.of(skill));
return Result.ok(toView(skill));
}
@@ -234,7 +216,7 @@ public class SkillController {
* @return 新建的 Skill 草稿
*/
@PostMapping("/copy")
@SaCheckPermission(value = {"/api/v1/skill/save", "/api/v1/skill/capability"})
@SaCheckPermission("/api/v1/skill/save")
public Result<SkillView> copy(@JsonBody(required = true, skipConvertError = false) SkillCopyRequest request) {
if (request == null) {
throw new BusinessException("复制参数不能为空");
@@ -247,7 +229,7 @@ public class SkillController {
* 在展示发布确认前执行发布级全量校验。
*
* @param id Skill ID
* @return 包含实时能力解析的结构化校验结果
* @return 标准包结构化校验结果
*/
@PostMapping("/validatePublish")
@SaCheckPermission("/api/v1/skill/submitPublishApproval")
@@ -343,6 +325,7 @@ public class SkillController {
*
* @param skillId Skill ID
* @param path 文件路径
* @param expectedContentHash 目标文件预期内容哈希,替换时必填
* @param file 上传文件
* @return 文件摘要
*/
@@ -384,77 +367,28 @@ public class SkillController {
}
/**
* 查询 Skill 能力绑定
* 批量预览标准 ZIP 导入内容
*
* @param skillId Skill ID
* @return 绑定列表
*/
@GetMapping("/capability/list")
@SaCheckPermission(value = {"/api/v1/skill/getDetail", "/api/v1/skill/capability"})
public Result<List<SkillView.CapabilityView>> capabilityList(BigInteger skillId) {
return Result.ok(capabilityBindingService.listVisibleBindings(skillId).stream()
.map(SkillView.CapabilityView::from).toList());
}
/**
* 查询当前用户可绑定的能力候选。
*
* @param type 能力类型
* @param keyword 关键词
* @return 候选列表
*/
@GetMapping("/capability/candidates")
@SaCheckPermission(value = {"/api/v1/skill/capability", "/api/v1/skill/import"}, mode = SaMode.OR)
public Result<List<SkillCapabilityCandidate>> capabilityCandidates(String type, String keyword) {
return Result.ok(capabilityBindingService.listCandidates(SkillCapabilityType.from(type), keyword));
}
/**
* 获取 MCP 工具名清单。
*
* @param targetId MCP ID
* @return MCP 候选详情
*/
@GetMapping("/capability/tools")
@SaCheckPermission(value = {"/api/v1/skill/capability", "/api/v1/skill/import"}, mode = SaMode.OR)
public Result<SkillCapabilityCandidate> capabilityTools(BigInteger targetId) {
return Result.ok(capabilityBindingService.getMcpTools(targetId));
}
/**
* 原子替换 Skill 能力绑定。
*
* @param skillId Skill ID
* @param requests 绑定白名单请求
* @return 保存后的绑定
*/
@PostMapping("/capability/replace")
@SaCheckPermission("/api/v1/skill/capability")
public Result<SkillCapabilityReplaceView> replaceCapabilities(
@JsonBody(value = "skillId", required = true, skipConvertError = false) BigInteger skillId,
@JsonBody(value = "expectedCapabilityHash", required = true, skipConvertError = false)
String expectedCapabilityHash,
@JsonBody(value = "bindings", required = true, skipConvertError = false)
List<SkillCapabilityBindingRequest> requests) {
List<SkillCapabilityBinding> bindings = requests == null ? List.of()
: requests.stream().map(SkillCapabilityBindingRequest::toEntity).toList();
List<SkillCapabilityBinding> saved = capabilityBindingService.replaceBindings(
skillId, bindings, expectedCapabilityHash);
return Result.ok(new SkillCapabilityReplaceView(
saved.stream().map(SkillView.CapabilityView::from).toList(),
capabilityBindingService.calculateHash(saved)));
}
/**
* 预览标准 ZIP 或 EasyFlow Bundle 导入内容。
*
* @param file 导入文件
* @return token 化预览
* @param files 导入文件
* @param file 兼容单文件字段
* @return 每个文件的 token 化预览
*/
@PostMapping(value = "/import/preview", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@SaCheckPermission("/api/v1/skill/import")
public Result<SkillImportPreview> importPreview(MultipartFile file) {
return Result.ok(skillImportService.preview(file));
public Result<List<SkillImportPreview>> importPreview(
@RequestPart(value = "files", required = false) List<MultipartFile> files,
@RequestPart(value = "file", required = false) MultipartFile file) {
List<MultipartFile> uploads = new java.util.ArrayList<>(files == null ? List.of() : files);
if (file != null) {
uploads.add(file);
}
if (uploads.isEmpty()) {
throw new BusinessException("请选择要导入的标准 Skill ZIP");
}
if (uploads.size() > 20) {
throw new BusinessException("单次最多预检 20 个 Skill ZIP");
}
return Result.ok(uploads.stream().map(skillImportService::preview).toList());
}
/**
@@ -470,6 +404,35 @@ public class SkillController {
return Result.ok(skillImportService.confirm(request).stream().map(this::toView).toList());
}
/**
* 独立确认多个已预检的标准 Skill ZIP单包业务失败不回滚其他包。
*
* @param requests 导入确认请求,按预检 token 一一对应
* @return 各包独立导入结果
*/
@PostMapping("/import/confirmBatch")
@SaCheckPermission("/api/v1/skill/import")
public Result<List<SkillImportBatchResultView>> importConfirmBatch(
@JsonBody(required = true, skipConvertError = false) List<SkillImportConfirmRequest> requests) {
if (requests == null || requests.isEmpty()) {
throw new BusinessException("请选择要确认导入的 Skill");
}
if (requests.size() > 20) {
throw new BusinessException("单次最多确认导入 20 个 Skill");
}
List<SkillImportBatchResultView> results = new java.util.ArrayList<>(requests.size());
for (SkillImportConfirmRequest request : requests) {
String token = request == null ? null : request.getImportToken();
try {
List<SkillView> skills = skillImportService.confirm(request).stream().map(this::toView).toList();
results.add(SkillImportBatchResultView.succeeded(token, skills));
} catch (BusinessException exception) {
results.add(SkillImportBatchResultView.failed(token, exception.getMessage()));
}
}
return Result.ok(results);
}
/**
* 取消导入并清理临时包。
*
@@ -485,7 +448,7 @@ public class SkillController {
}
/**
* 导出标准 Skill ZIP 或 EasyFlow 增强包
* 导出标准 Skill ZIP。
*
* @param request 导出请求
* @param response HTTP 响应
@@ -500,9 +463,7 @@ public class SkillController {
if (request.getIds().size() > 100) {
throw new BusinessException("单次最多导出 100 个 Skill");
}
SkillImportFormat format = SkillImportFormat.from(request.getFormat());
assertEnhancedExportPermission(format);
try (SkillExportArtifact artifact = skillExportService.prepare(request.getIds(), format)) {
try (SkillExportArtifact artifact = skillExportService.prepare(request.getIds())) {
response.setContentType(artifact.getMediaType());
response.setHeader("Content-Disposition", attachment(artifact.getFileName()));
artifact.transferTo(output(response));
@@ -510,32 +471,35 @@ public class SkillController {
}
/**
* 导出单个标准或增强 Skill 包。
* 导出单个标准 Skill 包。
*
* @param id Skill ID
* @param format 导出格式
* @param response HTTP 响应
*/
@GetMapping("/export")
@SaCheckPermission("/api/v1/skill/export")
public void exportOne(BigInteger id, String format, HttpServletResponse response) {
public void exportOne(BigInteger id, HttpServletResponse response) {
if (id == null) {
throw new BusinessException("Skill ID 不能为空");
}
writeExport(List.of(id), SkillImportFormat.from(format), response);
writeExport(List.of(id), response);
}
/**
* 提交发布审批。
*
* @param id Skill ID
* @param applicationReason 发布说明
* @return 审批实例 ID
*/
@PostMapping("/submitPublishApproval")
@SaCheckPermission("/api/v1/skill/submitPublishApproval")
public Result<BigInteger> submitPublishApproval(
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id) {
return approvalResult(skillPublishAppService.submitPublishApproval(id), "已提交发布审批", "已直接发布");
@JsonBody(value = "id", required = true, skipConvertError = false) BigInteger id,
@JsonBody(value = "applicationReason", required = true, skipConvertError = false)
String applicationReason) {
return approvalResult(skillPublishAppService.submitPublishApproval(id, applicationReason),
"已提交发布审批", "已直接发布");
}
/**
@@ -587,31 +551,18 @@ public class SkillController {
private QueryWrapper descriptorQuery() {
return QueryWrapper.create().select("id", "tenant_id", "dept_id", "category_id", "name", "display_name", "description",
"enabled", "visibility_scope", "source_type", "package_hash", "capability_hash", "snapshot_hash",
"resource_count", "capability_count", "reference_count", "script_count", "asset_count",
"visibility_scope", "package_hash", "snapshot_hash",
"publish_status", "current_approval_instance_id", "created", "created_by", "modified", "modified_by");
}
private void writeExport(List<BigInteger> ids, SkillImportFormat format, HttpServletResponse response) {
assertEnhancedExportPermission(format);
try (SkillExportArtifact artifact = skillExportService.prepare(ids, format)) {
private void writeExport(List<BigInteger> ids, HttpServletResponse response) {
try (SkillExportArtifact artifact = skillExportService.prepare(ids)) {
response.setContentType(artifact.getMediaType());
response.setHeader("Content-Disposition", attachment(artifact.getFileName()));
artifact.transferTo(output(response));
}
}
/**
* EasyFlow 增强包包含能力配置,导出时额外校验能力绑定查看权限。
*
* @param format 导出格式
*/
void assertEnhancedExportPermission(SkillImportFormat format) {
if (SkillImportFormat.EASYFLOW == format) {
StpUtil.checkPermission("/api/v1/skill/capability");
}
}
private void fillListState(List<Skill> skills) {
skillApprovalStateService.fillSkillApprovalState(skills);
creatorNameSupport.fillSkillCreatorNames(skills);

View File

@@ -1,59 +0,0 @@
package tech.easyflow.admin.controller.skill.vo;
import tech.easyflow.skill.entity.SkillCapabilityBinding;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* Skill 能力绑定写入白名单。
*
* @param capabilityType 能力类型
* @param targetId 当前环境目标 ID
* @param targetLogicalRef 跨环境逻辑引用
* @param runtimeName 运行时名称
* @param enabled 是否启用
* @param selectionMode MCP 工具选择模式
* @param selectedToolNamesJson 已选 MCP 工具
* @param executionMode 执行模式
* @param hitlEnabled 是否需要人工确认
* @param hitlConfigJson 人工确认安全配置
* @param optionsJson 执行安全配置
* @param sortNo 排序号
*/
public record SkillCapabilityBindingRequest(String capabilityType,
BigInteger targetId,
String targetLogicalRef,
String runtimeName,
Boolean enabled,
String selectionMode,
List<String> selectedToolNamesJson,
String executionMode,
Boolean hitlEnabled,
Map<String, Object> hitlConfigJson,
Map<String, Object> optionsJson,
Integer sortNo) {
/**
* 转换为能力绑定业务实体。
*
* @return 仅包含可写字段的绑定实体
*/
public SkillCapabilityBinding toEntity() {
SkillCapabilityBinding binding = new SkillCapabilityBinding();
binding.setCapabilityType(capabilityType);
binding.setTargetId(targetId);
binding.setTargetLogicalRef(targetLogicalRef);
binding.setRuntimeName(runtimeName);
binding.setEnabled(enabled);
binding.setSelectionMode(selectionMode);
binding.setSelectedToolNamesJson(selectedToolNamesJson);
binding.setExecutionMode(executionMode);
binding.setHitlEnabled(hitlEnabled);
binding.setHitlConfigJson(hitlConfigJson);
binding.setOptionsJson(optionsJson);
binding.setSortNo(sortNo);
return binding;
}
}

View File

@@ -1,13 +0,0 @@
package tech.easyflow.admin.controller.skill.vo;
import java.util.List;
/**
* 能力绑定原子替换结果。
*
* @param bindings 保存后的白名单绑定视图
* @param capabilityHash 新能力配置哈希
*/
public record SkillCapabilityReplaceView(List<SkillView.CapabilityView> bindings,
String capabilityHash) {
}

View File

@@ -11,14 +11,12 @@ import java.math.BigInteger;
* @param categoryId 分类 ID
* @param displayName 展示名称
* @param skillContent SKILL.md 内容,仅创建时使用;已有草稿正文通过文件接口原子保存
* @param enabled 是否启用
* @param visibilityScope 可见范围
*/
public record SkillDraftRequest(BigInteger id,
BigInteger categoryId,
String displayName,
String skillContent,
Boolean enabled,
String visibilityScope) {
/**
@@ -32,7 +30,6 @@ public record SkillDraftRequest(BigInteger id,
skill.setCategoryId(categoryId);
skill.setDisplayName(displayName);
skill.setSkillContent(skillContent);
skill.setEnabled(enabled);
skill.setVisibilityScope(visibilityScope);
return skill;
}

View File

@@ -0,0 +1,41 @@
package tech.easyflow.admin.controller.skill.vo;
import java.util.List;
/**
* 单个标准 Skill ZIP 的独立导入结果。
*
* @param importToken 预检 token
* @param success 是否成功
* @param message 失败原因,成功时为空
* @param skills 导入成功的 Skill
*/
public record SkillImportBatchResultView(
String importToken,
boolean success,
String message,
List<SkillView> skills
) {
/**
* 构造成功结果。
*
* @param importToken 预检 token
* @param skills 导入的 Skill
* @return 成功结果
*/
public static SkillImportBatchResultView succeeded(String importToken, List<SkillView> skills) {
return new SkillImportBatchResultView(importToken, true, null, List.copyOf(skills));
}
/**
* 构造失败结果。
*
* @param importToken 预检 token
* @param message 失败原因
* @return 失败结果
*/
public static SkillImportBatchResultView failed(String importToken, String message) {
return new SkillImportBatchResultView(importToken, false, message, List.of());
}
}

View File

@@ -1,66 +1,46 @@
package tech.easyflow.admin.controller.skill.vo;
import com.easyagents.skill.util.SkillResources;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillCapabilityBinding;
import tech.easyflow.skill.entity.SkillResource;
import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 管理端 Skill 视图,不暴露租户字段、二进制内部引用和发布快照
* 管理端 Skill 安全视图。
*
* @param id Skill ID
* @param categoryId 分类 ID
* @param name 规范名称
* @param name 标准名称
* @param displayName 展示名称
* @param description 描述
* @param metadataJson frontmatter 扩展元数据
* @param description 用途描述
* @param skillContent SKILL.md 内容
* @param enabled 是否启用
* @param visibilityScope 可见范围
* @param sourceType 来源类型
* @param packageHash 包 hash
* @param capabilityHash 能力 hash
* @param snapshotHash 发布快照 hash
* @param resourceCount 资源数
* @param capabilityCount 能力数
* @param referenceCount 参考文档数
* @param scriptCount 脚本数
* @param assetCount 二进制资源数
* @param visibilityScope 使用范围
* @param packageHash 标准包哈希
* @param snapshotHash 发布快照哈希
* @param publishStatus 发布状态
* @param currentApprovalInstanceId 当前审批实例 ID
* @param approvalPending 是否审批中
* @param currentApprovalActionType 当前审批动作
* @param displayPublishStatus 展示发布状态
* @param displayPublishStatus 展示状态
* @param created 创建时间
* @param modified 修改时间
* @param createdByName 创建人名称
* @param readable 当前用户是否可读
* @param manageable 当前用户是否可管理
* @param resources 包内资源摘要
* @param bindings 能力绑定
* @param createdByName 创建人昵称与账号
* @param readable 是否可读
* @param manageable 是否可管理
* @param resources 资源摘要
*/
public record SkillView(BigInteger id,
BigInteger categoryId,
String name,
String displayName,
String description,
Map<String, Object> metadataJson,
String skillContent,
Boolean enabled,
String visibilityScope,
String sourceType,
String packageHash,
String capabilityHash,
String snapshotHash,
Integer resourceCount,
Integer capabilityCount,
Integer referenceCount,
Integer scriptCount,
Integer assetCount,
String publishStatus,
BigInteger currentApprovalInstanceId,
Boolean approvalPending,
@@ -71,114 +51,50 @@ public record SkillView(BigInteger id,
String createdByName,
boolean readable,
boolean manageable,
List<ResourceView> resources,
List<CapabilityView> bindings) {
List<ResourceView> resources) {
/**
* 从业务实体创建安全视图。
* 从领域实体构造管理端视图。
*
* @param skill Skill 实体
* @param readable 是否可读
* @param manageable 是否可管理
* @return Skill 管理视图
* @return 管理视图
*/
public static SkillView from(Skill skill, boolean readable, boolean manageable) {
List<ResourceView> resources = skill.getResources() == null ? null
: skill.getResources().stream().map(ResourceView::from).toList();
List<CapabilityView> bindings = skill.getCapabilityBindings() == null ? null
: skill.getCapabilityBindings().stream()
.map(binding -> CapabilityView.from(binding, manageable)).toList();
return new SkillView(skill.getId(), skill.getCategoryId(), skill.getName(), skill.getDisplayName(),
skill.getDescription(), skill.getMetadataJson(), skill.getSkillContent(), skill.getEnabled(),
skill.getVisibilityScope(), skill.getSourceType(), skill.getPackageHash(), skill.getCapabilityHash(),
skill.getSnapshotHash(), skill.getResourceCount(), skill.getCapabilityCount(), skill.getReferenceCount(),
skill.getScriptCount(), skill.getAssetCount(), skill.getPublishStatus(),
skill.getCurrentApprovalInstanceId(), skill.getApprovalPending(), skill.getCurrentApprovalActionType(),
skill.getDisplayPublishStatus(), skill.getCreated(), skill.getModified(), skill.getCreatedByName(),
readable, manageable, resources, bindings);
skill.getDescription(), skill.getSkillContent(), skill.getVisibilityScope(), skill.getPackageHash(),
skill.getSnapshotHash(), skill.getPublishStatus(), skill.getCurrentApprovalInstanceId(),
skill.getApprovalPending(), skill.getCurrentApprovalActionType(), skill.getDisplayPublishStatus(),
skill.getCreated(), skill.getModified(), skill.getCreatedByName(), readable, manageable, resources);
}
/**
* Skill 包内资源摘要。
*
* @param id 资源 ID
* @param path 路径
* @param kind 类型
* @param language 脚本语言
* @param path 标准相对路径
* @param kind 按路径派生的语义类型
* @param mediaType 媒体类型
* @param isText 是否文本
* @param contentHash 内容 hash
* @param contentHash 内容哈希
* @param size 字节数
* @param metadataJson 扩展元数据
*/
public record ResourceView(BigInteger id, String path, String kind, String language, String mediaType,
Boolean isText, String contentHash, Long size, Map<String, Object> metadataJson) {
public record ResourceView(BigInteger id, String path, String kind, String mediaType,
Boolean isText, String contentHash, Long size) {
/**
* 转换资源实体。
*
* @param resource 资源实体
* @return 资源视图
* @return 资源摘要
*/
public static ResourceView from(SkillResource resource) {
return new ResourceView(resource.getId(), resource.getNormalizedPath(), resource.getKind(),
resource.getLanguage(), resource.getMediaType(), resource.getIsText(), resource.getContentHash(),
resource.getSize(), resource.getMetadataJson());
}
}
/**
* Skill 能力绑定视图。
*
* @param id 绑定 ID
* @param capabilityType 能力类型
* @param targetId 目标 ID
* @param targetLogicalRef 跨环境逻辑引用
* @param runtimeName 运行时名称
* @param enabled 是否启用
* @param selectionMode 工具选择模式
* @param selectedToolNamesJson 已选工具
* @param executionMode 执行模式
* @param hitlEnabled 是否人工确认
* @param hitlConfigJson 人工确认安全配置
* @param optionsJson 执行安全配置
* @param sortNo 排序号
* @param targetName 目标名称
* @param targetStatus 目标状态
* @param resolvedToolNames 已解析工具
*/
public record CapabilityView(BigInteger id, String capabilityType, BigInteger targetId, String targetLogicalRef,
String runtimeName, Boolean enabled, String selectionMode,
List<String> selectedToolNamesJson, String executionMode, Boolean hitlEnabled,
Map<String, Object> hitlConfigJson, Map<String, Object> optionsJson, Integer sortNo,
String targetName, String targetStatus, List<String> resolvedToolNames) {
/**
* 转换绑定实体。
*
* @param binding 绑定实体
* @return 绑定视图
*/
public static CapabilityView from(SkillCapabilityBinding binding) {
return from(binding, true);
}
/**
* 按管理权限转换绑定实体READ 用户看不到当前环境内部目标 ID。
*
* @param binding 绑定实体
* @param includeTargetId 是否包含目标 ID
* @return 绑定视图
*/
public static CapabilityView from(SkillCapabilityBinding binding, boolean includeTargetId) {
boolean hideUnavailableTarget = !includeTargetId && "NO_PERMISSION".equals(binding.getTargetStatus());
return new CapabilityView(binding.getId(), binding.getCapabilityType(),
includeTargetId ? binding.getTargetId() : null,
binding.getTargetLogicalRef(), binding.getRuntimeName(), binding.getEnabled(),
binding.getSelectionMode(), binding.getSelectedToolNamesJson(), binding.getExecutionMode(),
binding.getHitlEnabled(), binding.getHitlConfigJson(), binding.getOptionsJson(), binding.getSortNo(),
hideUnavailableTarget ? null : binding.getTargetName(), binding.getTargetStatus(),
hideUnavailableTarget ? List.of() : binding.getResolvedToolNames());
String path = resource.getNormalizedPath();
return new ResourceView(resource.getId(), path, SkillResources.classify(path).name(),
resource.getMediaType(), resource.getIsText(), resource.getContentHash(), resource.getSize());
}
}
}