feat: 支持 Skill 仓库扫描与批量导入

- 统一支持本地单包、多包和 Git 候选最多 50 项导入

- 固定提交并限制 SSRF、DNS、并发、频率、超时和仓库资源

- 增加导入对话框、接口契约和安全回归测试
This commit is contained in:
2026-08-19 22:39:55 +08:00
parent 4e8640dcaf
commit 9579619384
28 changed files with 3949 additions and 86 deletions

View File

@@ -16,6 +16,8 @@ import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryPrepareRequest;
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryScanRequest;
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
import tech.easyflow.admin.controller.skill.vo.SkillView;
import tech.easyflow.admin.controller.skill.vo.SkillToolBindingUpdateRequest;
@@ -39,6 +41,8 @@ import tech.easyflow.skill.imports.SkillExportService;
import tech.easyflow.skill.imports.SkillImportConfirmRequest;
import tech.easyflow.skill.imports.SkillImportPreview;
import tech.easyflow.skill.imports.SkillImportService;
import tech.easyflow.skill.gitimport.SkillGitImportService;
import tech.easyflow.skill.gitimport.SkillGitScanResult;
import tech.easyflow.skill.publish.SkillPublishAppService;
import tech.easyflow.skill.security.SkillVisibilityQueryHelper;
import tech.easyflow.skill.service.SkillApprovalStateService;
@@ -77,6 +81,7 @@ public class SkillController {
private final SkillApprovalStateService skillApprovalStateService;
private final SkillPublishAppService skillPublishAppService;
private final SkillImportService skillImportService;
private final SkillGitImportService skillGitImportService;
private final SkillExportService skillExportService;
private final SkillFileService skillFileService;
private final SkillToolBindingService skillToolBindingService;
@@ -93,6 +98,7 @@ public class SkillController {
* @param skillApprovalStateService 审批状态服务
* @param skillPublishAppService 发布服务
* @param skillImportService 导入服务
* @param skillGitImportService Git 仓库导入服务
* @param skillExportService 导出服务
* @param skillFileService 文件服务
* @param skillToolBindingService Skill Tool 绑定服务
@@ -106,6 +112,7 @@ public class SkillController {
SkillApprovalStateService skillApprovalStateService,
SkillPublishAppService skillPublishAppService,
SkillImportService skillImportService,
SkillGitImportService skillGitImportService,
SkillExportService skillExportService,
SkillFileService skillFileService,
SkillToolBindingService skillToolBindingService,
@@ -118,6 +125,7 @@ public class SkillController {
this.skillApprovalStateService = skillApprovalStateService;
this.skillPublishAppService = skillPublishAppService;
this.skillImportService = skillImportService;
this.skillGitImportService = skillGitImportService;
this.skillExportService = skillExportService;
this.skillFileService = skillFileService;
this.skillToolBindingService = skillToolBindingService;
@@ -447,10 +455,43 @@ public class SkillController {
if (uploads.isEmpty()) {
throw new BusinessException("请选择要导入的标准 Skill ZIP");
}
if (uploads.size() > 20) {
throw new BusinessException("单次最多预检 20 个 Skill ZIP");
if (uploads.size() > SkillImportService.MAX_BATCH_SKILL_COUNT) {
throw new BusinessException("单次最多选择 "
+ SkillImportService.MAX_BATCH_SKILL_COUNT + " 个 Skill ZIP");
}
return Result.ok(uploads.stream().map(skillImportService::preview).toList());
return Result.ok(skillImportService.previewBatch(uploads));
}
/**
* 扫描 HTTPS Git 仓库中的标准 Skill 候选。
*
* @param request 仓库地址请求
* @return 固定提交的候选列表
*/
@PostMapping("/import/repository/scan")
@SaCheckPermission("/api/v1/skill/import")
public Result<SkillGitScanResult> scanGitRepository(
@JsonBody(required = true, skipConvertError = false) SkillGitRepositoryScanRequest request) {
if (request == null) {
throw new BusinessException("请输入 Git 仓库地址");
}
return Result.ok(skillGitImportService.scan(request.repositoryUrl()));
}
/**
* 将选中的 Git Skill 候选转换为既有标准 ZIP 导入预览。
*
* @param request 扫描令牌与候选 ID
* @return 与本地 ZIP 导入一致的预览列表
*/
@PostMapping("/import/repository/prepare")
@SaCheckPermission("/api/v1/skill/import")
public Result<List<SkillImportPreview>> prepareGitRepositoryImport(
@JsonBody(required = true, skipConvertError = false) SkillGitRepositoryPrepareRequest request) {
if (request == null) {
throw new BusinessException("请选择要导入的 Git Skill");
}
return Result.ok(skillGitImportService.prepare(request.scanToken(), request.candidateIds()));
}
/**
@@ -479,8 +520,9 @@ public class SkillController {
if (requests == null || requests.isEmpty()) {
throw new BusinessException("请选择要确认导入的 Skill");
}
if (requests.size() > 20) {
throw new BusinessException("单次最多确认导入 20 个 Skill");
if (requests.size() > SkillImportService.MAX_BATCH_SKILL_COUNT) {
throw new BusinessException("单次最多确认导入 "
+ SkillImportService.MAX_BATCH_SKILL_COUNT + " 个 Skill");
}
List<SkillImportBatchResultView> results = new java.util.ArrayList<>(requests.size());
for (SkillImportConfirmRequest request : requests) {

View File

@@ -0,0 +1,12 @@
package tech.easyflow.admin.controller.skill.vo;
import java.util.List;
/**
* 选中 Git Skill 候选的标准导入预览准备请求。
*
* @param scanToken 短期扫描令牌
* @param candidateIds 选中的候选 ID
*/
public record SkillGitRepositoryPrepareRequest(String scanToken, List<String> candidateIds) {
}

View File

@@ -0,0 +1,9 @@
package tech.easyflow.admin.controller.skill.vo;
/**
* Git 仓库 Skill 扫描请求。
*
* @param repositoryUrl HTTPS Git 仓库地址,可省略 .git 后缀
*/
public record SkillGitRepositoryScanRequest(String repositoryUrl) {
}