feat: 增加工作流和知识库三级权限

- 抽取统一资源访问骨架与部门可见范围判断

- 接入工作流和知识库的 READ/MANAGE 权限校验

- 增加可见范围配置与只读态前端交互
This commit is contained in:
2026-03-29 17:25:55 +08:00
parent f49d94e2fe
commit 22ceabff96
58 changed files with 3053 additions and 85 deletions

View File

@@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
@@ -11,8 +15,13 @@ 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.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
@@ -25,6 +34,13 @@ import java.util.List;
@RequestMapping("/api/v1/botKnowledge")
@UsePermission(moduleName = "/api/v1/bot")
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
@Resource
private ResourceAccessService resourceAccessService;
public BotDocumentCollectionController(BotDocumentCollectionService service) {
super(service);
}
@@ -35,12 +51,32 @@ public class BotDocumentCollectionController extends BaseCurdController<BotDocum
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
return Result.ok(botDocumentCollections);
List<BotDocumentCollection> visibleList = new ArrayList<>();
KnowledgeReadAccessSnapshot snapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot();
for (BotDocumentCollection relation : botDocumentCollections) {
DocumentCollection knowledge = relation.getKnowledge();
if (knowledge == null || knowledgeVisibilityQueryHelper.canRead(knowledge, snapshot)) {
visibleList.add(relation);
}
}
return Result.ok(visibleList);
}
@PostMapping("updateBotKnowledgeIds")
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
if (knowledgeIds != null) {
for (BigInteger knowledgeId : knowledgeIds) {
if (knowledgeId == null) {
continue;
}
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
if (collection == null) {
continue;
}
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库");
}
}
service.saveBotAndKnowledge(botId, knowledgeIds);
return Result.ok();
}
}
}

View File

@@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai;
import org.springframework.web.bind.annotation.PostMapping;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.permission.WorkflowReadAccessSnapshot;
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.tree.Tree;
@@ -12,10 +16,16 @@ 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.web.jsonbody.JsonBody;
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.ArrayList;
import java.util.List;
import javax.annotation.Resource;
/**
* 控制层。
*
@@ -26,6 +36,13 @@ import java.util.List;
@RequestMapping("/api/v1/botWorkflow")
@UsePermission(moduleName = "/api/v1/bot")
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
@Resource
private WorkflowService workflowService;
@Resource
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
@Resource
private ResourceAccessService resourceAccessService;
public BotWorkflowController(BotWorkflowService service) {
super(service);
}
@@ -36,13 +53,33 @@ public class BotWorkflowController extends BaseCurdController<BotWorkflowService
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
List<BotWorkflow> list = Tree.tryToTree(botWorkflows, asTree);
List<BotWorkflow> visibleList = new ArrayList<>();
WorkflowReadAccessSnapshot snapshot = workflowVisibilityQueryHelper.getCurrentReadSnapshot();
for (BotWorkflow botWorkflow : botWorkflows) {
Workflow workflow = botWorkflow.getWorkflow();
if (workflow == null || workflowVisibilityQueryHelper.canRead(workflow, snapshot)) {
visibleList.add(botWorkflow);
}
}
List<BotWorkflow> list = Tree.tryToTree(visibleList, asTree);
return Result.ok(list);
}
@PostMapping("updateBotWorkflowIds")
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
if (workflowIds != null) {
for (BigInteger workflowId : workflowIds) {
if (workflowId == null) {
continue;
}
Workflow workflow = workflowService.getById(workflowId);
if (workflow == null) {
continue;
}
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.READ, "无权限绑定工作流");
}
}
service.saveBotAndWorkflowTool(botId, workflowIds);
return Result.ok();
}
}
}

View File

@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.easyagents.core.model.embedding.EmbeddingModel;
import com.mybatisflex.core.paginate.Page;
import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Model;
@@ -16,9 +17,15 @@ import com.easyagents.core.document.Document;
import com.easyagents.core.store.DocumentStore;
import com.easyagents.core.store.StoreOptions;
import com.easyagents.core.store.StoreResult;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import javax.annotation.Resource;
import java.math.BigInteger;
@@ -51,8 +58,29 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
super(service);
}
@GetMapping("page")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.DOCUMENT_ID,
idExpr = "#request.getParameter('documentId')",
denyMessage = "无权限访问知识库"
)
@Override
public Result<Page<DocumentChunk>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
return super.page(request, sortKey, sortType, pageNumber, pageSize);
}
@PostMapping("update")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
idExpr = "#documentChunk.id",
denyMessage = "无权限管理知识库"
)
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
boolean success = service.updateById(documentChunk);
if (success){
@@ -87,6 +115,13 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
@PostMapping("removeChunk")
@SaCheckPermission("/api/v1/documentCollection/remove")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.DOCUMENT_CHUNK_ID,
idExpr = "#chunkId",
denyMessage = "无权限管理知识库"
)
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
DocumentChunk docChunk = documentChunkService.getById(chunkId);
if (docChunk == null) {

View File

@@ -2,14 +2,19 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.easyagents.core.document.Document;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
import tech.easyflow.ai.documentimport.DocumentImportDtos;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.DocumentChunkService;
import tech.easyflow.ai.service.DocumentCollectionService;
@@ -17,6 +22,13 @@ import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.io.Serializable;
@@ -41,6 +53,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
@Resource
private BotDocumentCollectionService botDocumentCollectionService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
super(service);
@@ -50,6 +66,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
@Override
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
normalizeVisibilityScope(entity, isSave);
if (!isSave && entity.getId() != null) {
DocumentCollection existed = requireKnowledge(String.valueOf(entity.getId()));
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, existed, ResourceAction.MANAGE, "无权限管理知识库");
}
String alias = entity.getAlias();
String collectionType = entity.getCollectionType();
@@ -96,6 +117,13 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
@GetMapping("search")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#knowledgeId",
denyMessage = "无权限访问知识库"
)
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
return Result.ok(service.search(knowledgeId, keyword));
}
@@ -103,6 +131,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
@Override
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
for (Serializable id : ids) {
DocumentCollection collection = requireKnowledge(String.valueOf(id));
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.MANAGE, "无权限管理知识库");
}
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
@@ -116,7 +148,90 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
}
@Override
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID_OR_SLUG,
idExpr = "#id",
denyMessage = "无权限访问知识库"
)
public Result<DocumentCollection> detail(String id) {
return Result.ok(service.getDetail(id));
DocumentCollection detail = service.getDetail(id);
return Result.ok(detail);
}
@GetMapping("modelList")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<List<Model>> modelList(Model entity, Boolean asTree, String sortKey, String sortType) {
return Result.ok(llmService.listSelectableModels(entity, asTree, sortKey, sortType));
}
@PostMapping("splitterProfile/save")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#request.knowledgeId",
denyMessage = "无权限管理知识库"
)
public Result<Boolean> saveSplitterProfile(@JsonBody DocumentImportDtos.SplitterProfileSaveRequest request) {
if (request.getKnowledgeId() == null) {
throw new BusinessException("知识库ID不能为空");
}
DocumentCollection collection = service.getById(request.getKnowledgeId());
if (collection == null) {
throw new BusinessException("知识库不存在");
}
if (collection.isFaqCollection()) {
throw new BusinessException("FAQ知识库不支持文档导入策略");
}
Map<String, Object> options = collection.getOptions() == null
? new HashMap<>()
: new HashMap<>(collection.getOptions());
options.put(DocumentCollection.KEY_SPLITTER_DEFAULT_STRATEGY, request.getDefaultStrategyCode());
options.put(DocumentCollection.KEY_SPLITTER_AUTO_RECOMMEND_ENABLED, request.getAutoRecommendEnabled());
options.put(DocumentCollection.KEY_SPLITTER_FALLBACK_STRATEGY, request.getFallbackStrategyCode());
options.put(DocumentCollection.KEY_SPLITTER_STRATEGY_PROFILES, request.getStrategyProfiles());
DocumentCollection update = new DocumentCollection();
update.setId(collection.getId());
update.setOptions(options);
return Result.ok(service.updateById(update));
}
@Override
public Result<List<DocumentCollection>> list(DocumentCollection entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
@Override
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
return super.queryPage(page, queryWrapper);
}
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
if (entity == null) {
return;
}
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
if (isSave) {
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
}
return;
}
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
}
private DocumentCollection requireKnowledge(String idOrAlias) {
DocumentCollection collection = service.getDetail(idOrAlias);
if (collection == null) {
throw new BusinessException("知识库不存在");
}
return collection;
}
}

View File

@@ -1,13 +1,16 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.io.IoUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import tech.easyflow.ai.documentimport.DocumentImportDtos;
import tech.easyflow.ai.entity.Document;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
@@ -24,11 +27,21 @@ import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import tech.easyflow.system.service.ResourceAccessService;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
@@ -58,6 +71,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
@Autowired
private RedisLockExecutor redisLockExecutor;
@Resource(name = "default")
private FileStorageService storageService;
@Autowired
private ResourceAccessService resourceAccessService;
@Value("${easyflow.storage.local.root:}")
private String fileUploadPath;
@@ -73,6 +91,8 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
@Transactional
@SaCheckPermission("/api/v1/documentCollection/remove")
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
Document document = requireDocument(new BigInteger(id));
getDocumentCollection(document.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
List<Serializable> ids = Collections.singletonList(id);
Result<?> result = onRemoveBefore(ids);
if (result != null) return result;
@@ -104,7 +124,7 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
throw new BusinessException("知识库id不能为空");
}
DocumentCollection knowledge = getDocumentCollection(kbSlug);
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(Document::getCollectionId, knowledge.getId());
@@ -121,11 +141,33 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
if (StringUtil.noText(kbSlug)) {
throw new BusinessException("知识库id不能为空");
}
DocumentCollection knowledge = getDocumentCollection(kbSlug);
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
return Result.ok(documentList);
}
@GetMapping("download")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.DOCUMENT_ID,
idExpr = "#documentId",
denyMessage = "无权限访问知识库"
)
public void download(@RequestParam BigInteger documentId, HttpServletResponse response) throws IOException {
Document document = requireDocument(documentId);
String fileName = resolveDownloadFileName(document);
response.setContentType("application/octet-stream");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName);
try (InputStream inputStream = storageService.readStream(document.getDocumentPath())) {
IoUtil.copy(inputStream, response.getOutputStream());
response.flushBuffer();
}
}
@Override
protected String getDefaultOrderBy() {
@@ -138,6 +180,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
@Transactional
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<Boolean> update(@JsonBody Document entity) {
if (entity.getId() == null) {
throw new BusinessException("文档不存在");
}
Document current = requireDocument(entity.getId());
getDocumentCollection(current.getCollectionId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
super.update(entity);
return Result.ok(updatePosition(entity));
}
@@ -152,10 +199,40 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
if (documentCollectionSplitParams.getKnowledgeId() == null) {
throw new BusinessException("知识库id不能为空");
}
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString());
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
return documentService.textSplit(documentCollectionSplitParams);
}
@PostMapping("import/analyze")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<DocumentImportDtos.AnalyzeResponse> analyzeImport(@JsonBody DocumentImportDtos.AnalyzeRequest request) {
if (request.getKnowledgeId() == null) {
throw new BusinessException("知识库id不能为空");
}
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
return documentService.analyzeImport(request);
}
@PostMapping("import/preview")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<DocumentImportDtos.PreviewResponse> previewImport(@JsonBody DocumentImportDtos.PreviewRequest request) {
if (request.getKnowledgeId() == null) {
throw new BusinessException("知识库id不能为空");
}
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
return documentService.previewImport(request);
}
@PostMapping("import/commit")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<DocumentImportDtos.CommitResponse> commitImport(@JsonBody DocumentImportDtos.CommitRequest request) {
if (request.getKnowledgeId() == null) {
throw new BusinessException("知识库id不能为空");
}
getDocumentCollection(request.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
return documentService.commitImport(request);
}
/**
* 更新 entity
*
@@ -219,17 +296,42 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
}
}
private DocumentCollection getDocumentCollection(String idOrSlug) {
private DocumentCollection getDocumentCollection(String idOrSlug, ResourceAction action, String denyMessage) {
DocumentCollection knowledge = StringUtil.isNumeric(idOrSlug)
? knowledgeService.getById(idOrSlug)
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
if (knowledge == null) {
throw new BusinessException("知识库不存在");
}
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, action, denyMessage);
if (knowledge.isFaqCollection()) {
throw new BusinessException("FAQ知识库不支持文档操作");
}
return knowledge;
}
private Document requireDocument(BigInteger documentId) {
if (documentId == null) {
throw new BusinessException("文档不存在");
}
Document document = service.getById(documentId);
if (document == null) {
throw new BusinessException("文档不存在");
}
return document;
}
private String resolveDownloadFileName(Document document) {
String fileName = document.getTitle();
if (!StringUtil.hasText(fileName)) {
String path = document.getDocumentPath();
if (!StringUtil.hasText(path)) {
return "document";
}
int slashIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return slashIndex >= 0 ? path.substring(slashIndex + 1) : path;
}
return fileName;
}
}

View File

@@ -12,6 +12,10 @@ import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import java.io.Serializable;
import java.math.BigInteger;
@@ -29,6 +33,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
@Override
@GetMapping("list")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#entity == null ? null : #entity.collectionId",
denyMessage = "无权限访问知识库"
)
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
if (collectionId == null) {
@@ -40,6 +51,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
@Override
@PostMapping("save")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#entity.collectionId",
denyMessage = "无权限管理知识库"
)
public Result<?> save(@JsonBody FaqCategory entity) {
return Result.ok(service.saveCategory(entity));
}
@@ -47,6 +65,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
@Override
@PostMapping("update")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.FAQ_CATEGORY_ID,
idExpr = "#entity.id",
denyMessage = "无权限管理知识库"
)
public Result<?> update(@JsonBody FaqCategory entity) {
return Result.ok(service.updateCategory(entity));
}
@@ -54,6 +79,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
@Override
@PostMapping("remove")
@SaCheckPermission("/api/v1/documentCollection/remove")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.FAQ_CATEGORY_ID,
idExpr = "#id",
denyMessage = "无权限管理知识库"
)
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id))));
}

View File

@@ -25,6 +25,10 @@ import tech.easyflow.common.vo.UploadResVo;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import javax.annotation.Resource;
import java.io.Serializable;
@@ -67,13 +71,31 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@Override
@GetMapping("list")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#entity == null ? null : #entity.collectionId",
denyMessage = "无权限访问知识库"
)
public Result<java.util.List<FaqItem>> list(FaqItem entity, Boolean asTree, String sortKey, String sortType) {
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");
}
return super.list(entity, asTree, sortKey, sortType);
}
@Override
@GetMapping("page")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#request.getParameter('collectionId')",
denyMessage = "无权限访问知识库"
)
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
@@ -123,6 +145,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@Override
@GetMapping("detail")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.FAQ_ITEM_ID,
idExpr = "#id",
denyMessage = "无权限访问知识库"
)
public Result<FaqItem> detail(String id) {
return super.detail(id);
}
@@ -130,6 +159,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@Override
@PostMapping("save")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#entity.collectionId",
denyMessage = "无权限管理知识库"
)
public Result<?> save(@JsonBody FaqItem entity) {
return Result.ok(service.saveFaqItem(entity));
}
@@ -137,6 +173,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@Override
@PostMapping("update")
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.FAQ_ITEM_ID,
idExpr = "#entity.id",
denyMessage = "无权限管理知识库"
)
public Result<?> update(@JsonBody FaqItem entity) {
return Result.ok(service.updateFaqItem(entity));
}
@@ -144,12 +187,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@Override
@PostMapping("remove")
@SaCheckPermission("/api/v1/documentCollection/remove")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.FAQ_ITEM_ID,
idExpr = "#id",
denyMessage = "无权限管理知识库"
)
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
}
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#collectionId",
denyMessage = "无权限管理知识库"
)
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");
@@ -180,12 +237,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@SaCheckPermission("/api/v1/documentCollection/save")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.MANAGE,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#collectionId",
denyMessage = "无权限管理知识库"
)
public Result<FaqImportResultVo> importExcel(MultipartFile file, BigInteger collectionId) {
return Result.ok(service.importFromExcel(collectionId, file));
}
@GetMapping("downloadImportTemplate")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#collectionId",
denyMessage = "无权限访问知识库"
)
public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception {
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");
@@ -206,6 +277,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@GetMapping("exportExcel")
@SaCheckPermission("/api/v1/documentCollection/query")
@RequireResourceAccess(
resource = CategoryResourceType.KNOWLEDGE,
action = ResourceAction.READ,
lookup = ResourceLookup.KNOWLEDGE_ID,
idExpr = "#collectionId",
denyMessage = "无权限访问知识库"
)
public void exportExcel(BigInteger collectionId, HttpServletResponse response) throws Exception {
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");

View File

@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.mybatisflex.core.paginate.Page;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
@@ -12,6 +13,7 @@ import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
@@ -30,6 +32,12 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.ResourceLookup;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.permission.resource.RequireResourceAccess;
import tech.easyflow.system.service.ResourceAccessService;
import tech.easyflow.system.service.SysApiKeyService;
import javax.annotation.Resource;
@@ -67,6 +75,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
private CodeEngineCapabilityService codeEngineCapabilityService;
@Resource
private WorkflowCheckService workflowCheckService;
@Resource
private ResourceAccessService resourceAccessService;
@Resource
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
public WorkflowController(WorkflowService service, ModelService modelService) {
super(service);
@@ -78,6 +90,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
*/
@PostMapping("/singleRun")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#workflowId",
denyMessage = "无权限运行工作流"
)
public Result<?> singleRun(
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
@JsonBody(value = "nodeId", required = true) String nodeId,
@@ -96,6 +115,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
*/
@PostMapping("/runAsync")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限运行工作流"
)
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
@JsonBody("variables") Map<String, Object> variables) {
if (variables == null) {
@@ -117,6 +143,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
* 获取工作流运行状态 - v2
*/
@PostMapping("/getChainStatus")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.EXEC_KEY,
idExpr = "#executeId",
denyMessage = "无权限访问该执行记录"
)
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
@JsonBody("nodes") List<NodeInfo> nodes) {
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
@@ -128,6 +161,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
*/
@PostMapping("/resume")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.USE,
lookup = ResourceLookup.EXEC_KEY,
idExpr = "#executeId",
denyMessage = "无权限恢复工作流执行"
)
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
chainExecutor.resumeAsync(executeId, confirmParams);
@@ -137,6 +177,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@PostMapping("/importWorkFlow")
@SaCheckPermission("/api/v1/workflow/save")
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
if (workflow.getId() != null) {
Workflow sourceWorkflow = requireWorkflow(String.valueOf(workflow.getId()));
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, sourceWorkflow, ResourceAction.MANAGE, "无权限管理工作流");
}
InputStream is = jsonFile.getInputStream();
String content = IoUtil.read(is, StandardCharsets.UTF_8);
workflow.setContent(content);
@@ -147,13 +191,30 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@GetMapping("/exportWorkFlow")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<String> exportWorkFlow(BigInteger id) {
Workflow workflow = service.getById(id);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
return Result.ok("", workflow.getContent());
}
@GetMapping("getRunningParameters")
@SaCheckPermission("/api/v1/workflow/query")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
Workflow workflow = service.getById(id);
@@ -186,6 +247,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
public Result<WorkflowCheckResult> check(@JsonBody("id") BigInteger id,
@JsonBody("content") String content,
@JsonBody(value = "stage", required = true) String stage) {
if (id != null) {
Workflow workflow = requireWorkflow(String.valueOf(id));
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
}
WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage);
WorkflowCheckResult checkResult;
if (StringUtils.hasLength(content)) {
@@ -199,6 +264,14 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
}
@Override
@GetMapping("detail")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<Workflow> detail(String id) {
Workflow workflow = service.getDetail(id);
return Result.ok(workflow);
@@ -206,9 +279,19 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@GetMapping("/copy")
@SaCheckPermission("/api/v1/workflow/save")
@RequireResourceAccess(
resource = CategoryResourceType.WORKFLOW,
action = ResourceAction.READ,
lookup = ResourceLookup.WORKFLOW_ID,
idExpr = "#id",
denyMessage = "无权限访问工作流"
)
public Result<Void> copy(BigInteger id) {
LoginAccount account = SaTokenUtil.getLoginAccount();
Workflow workflow = service.getById(id);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
workflow.setId(null);
workflow.setAlias(IdUtil.fastSimpleUUID());
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
@@ -218,6 +301,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@Override
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
normalizeVisibilityScope(entity, isSave);
if (!isSave && entity.getId() != null) {
Workflow existed = requireWorkflow(String.valueOf(entity.getId()));
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, existed, ResourceAction.MANAGE, "无权限管理工作流");
}
if (StringUtils.hasLength(entity.getContent())) {
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
}
@@ -241,8 +329,26 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
return null;
}
@Override
public Result<List<Workflow>> list(Workflow entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
return Result.ok(service.list(queryWrapper));
}
@Override
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
return super.queryPage(page, queryWrapper);
}
@Override
protected Result onRemoveBefore(Collection<Serializable> ids) {
for (Serializable id : ids) {
Workflow workflow = requireWorkflow(String.valueOf(id));
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
}
QueryWrapper queryWrapper = QueryWrapper.create();
queryWrapper.in("workflow_id", ids);
boolean exists = botWorkflowService.exists(queryWrapper);
@@ -251,4 +357,25 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
}
return null;
}
private void normalizeVisibilityScope(Workflow entity, boolean isSave) {
if (entity == null) {
return;
}
if (!StringUtils.hasLength(entity.getVisibilityScope())) {
if (isSave) {
entity.setVisibilityScope(VisibilityScope.PRIVATE.name());
}
return;
}
entity.setVisibilityScope(VisibilityScope.from(entity.getVisibilityScope()).name());
}
private Workflow requireWorkflow(String idOrAlias) {
Workflow workflow = service.getDetail(idOrAlias);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
return workflow;
}
}