feat: 增加工作流和知识库三级权限
- 抽取统一资源访问骨架与部门可见范围判断 - 接入工作流和知识库的 READ/MANAGE 权限校验 - 增加可见范围配置与只读态前端交互
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
@@ -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不能为空");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import tech.easyflow.ai.entity.base.DocumentCollectionBase;
|
||||
import tech.easyflow.common.util.PropertiesUtil;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Map;
|
||||
@@ -32,7 +33,7 @@ import java.util.Map;
|
||||
*/
|
||||
|
||||
@Table("tb_document_collection")
|
||||
public class DocumentCollection extends DocumentCollectionBase {
|
||||
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
||||
|
||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||
public static final String TYPE_FAQ = "FAQ";
|
||||
@@ -71,6 +72,10 @@ public class DocumentCollection extends DocumentCollectionBase {
|
||||
* 是否启用重排模型
|
||||
*/
|
||||
public static final String KEY_RERANK_ENABLE = "rerankEnable";
|
||||
public static final String KEY_SPLITTER_DEFAULT_STRATEGY = "splitter.defaultStrategyCode";
|
||||
public static final String KEY_SPLITTER_AUTO_RECOMMEND_ENABLED = "splitter.autoRecommendEnabled";
|
||||
public static final String KEY_SPLITTER_FALLBACK_STRATEGY = "splitter.fallbackStrategyCode";
|
||||
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
|
||||
|
||||
public DocumentStore toDocumentStore() {
|
||||
String storeType = this.getVectorStoreType();
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
/**
|
||||
* 实体类。
|
||||
@@ -13,7 +14,7 @@ import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||
*/
|
||||
|
||||
@Table("tb_workflow")
|
||||
public class Workflow extends WorkflowBase {
|
||||
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new WorkflowTool(this, needEnglishName);
|
||||
|
||||
@@ -3,8 +3,10 @@ package tech.easyflow.ai.entity.base;
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class DocumentChunkBase implements Serializable {
|
||||
@@ -38,6 +40,12 @@ public class DocumentChunkBase implements Serializable {
|
||||
@Column(comment = "分割顺序")
|
||||
private Integer sorting;
|
||||
|
||||
/**
|
||||
* 扩展元信息
|
||||
*/
|
||||
@Column(typeHandler = FastjsonTypeHandler.class, comment = "扩展元信息")
|
||||
private Map<String, Object> options;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable {
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
public Map<String, Object> getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
public void setOptions(Map<String, Object> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -160,6 +160,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "分类ID")
|
||||
private BigInteger categoryId;
|
||||
|
||||
/**
|
||||
* 可见范围
|
||||
*/
|
||||
@Column(comment = "可见范围")
|
||||
private String visibilityScope;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getVisibilityScope() {
|
||||
return visibilityScope;
|
||||
}
|
||||
|
||||
public void setVisibilityScope(String visibilityScope) {
|
||||
this.visibilityScope = visibilityScope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "分类ID")
|
||||
private BigInteger categoryId;
|
||||
|
||||
/**
|
||||
* 可见范围
|
||||
*/
|
||||
@Column(comment = "可见范围")
|
||||
private String visibilityScope;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getVisibilityScope() {
|
||||
return visibilityScope;
|
||||
}
|
||||
|
||||
public void setVisibilityScope(String visibilityScope) {
|
||||
this.visibilityScope = visibilityScope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class DocumentChunkIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentChunkService documentChunkService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_CHUNK_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("文档分段不存在");
|
||||
}
|
||||
DocumentChunk documentChunk = documentChunkService.getById(String.valueOf(identifier));
|
||||
if (documentChunk == null) {
|
||||
throw new BusinessException("文档分段不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(documentChunk.getDocumentCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class DocumentIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentService documentService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.DOCUMENT_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
Document document = documentService.getById(String.valueOf(identifier));
|
||||
if (document == null) {
|
||||
throw new BusinessException("文档不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(document.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class FaqCategoryIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private FaqCategoryService faqCategoryService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_CATEGORY_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
FaqCategory faqCategory = faqCategoryService.getById(String.valueOf(identifier));
|
||||
if (faqCategory == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(faqCategory.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class FaqItemIdKnowledgeResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private FaqItemService faqItemService;
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.FAQ_ITEM_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
FaqItem faqItem = faqItemService.getById(String.valueOf(identifier));
|
||||
if (faqItem == null) {
|
||||
throw new BusinessException("FAQ不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(faqItem.getCollectionId());
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class KnowledgeIdOrSlugResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID_OR_SLUG == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class KnowledgeIdResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.KNOWLEDGE == resourceType && ResourceLookup.KNOWLEDGE_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(String.valueOf(identifier));
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(collection, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class KnowledgeReadAccessSnapshot {
|
||||
|
||||
private final RoleCategoryAccessSnapshot categoryAccess;
|
||||
private final Set<BigInteger> readableDeptIds;
|
||||
|
||||
public KnowledgeReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
|
||||
this.categoryAccess = categoryAccess;
|
||||
this.readableDeptIds = readableDeptIds == null
|
||||
? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
|
||||
}
|
||||
|
||||
public BigInteger getAccountId() {
|
||||
return categoryAccess == null ? null : categoryAccess.getAccountId();
|
||||
}
|
||||
|
||||
public boolean isSuperAdmin() {
|
||||
return categoryAccess != null && categoryAccess.isSuperAdmin();
|
||||
}
|
||||
|
||||
public boolean isRestricted() {
|
||||
return categoryAccess == null || categoryAccess.isRestricted();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getCategoryIds() {
|
||||
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getReadableDeptIds() {
|
||||
return readableDeptIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.DocumentCollectionTableDef.DOCUMENT_COLLECTION;
|
||||
|
||||
@Component
|
||||
public class KnowledgeVisibilityQueryHelper {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
public KnowledgeReadAccessSnapshot getCurrentReadSnapshot() {
|
||||
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.KNOWLEDGE.getCode());
|
||||
if (categoryAccess.isSuperAdmin()) {
|
||||
return new KnowledgeReadAccessSnapshot(categoryAccess, Collections.emptySet());
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
Set<BigInteger> deptIds = loginAccount == null
|
||||
? Collections.emptySet()
|
||||
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
|
||||
return new KnowledgeReadAccessSnapshot(categoryAccess, deptIds);
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper) {
|
||||
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper, KnowledgeReadAccessSnapshot snapshot) {
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId == null) {
|
||||
queryWrapper.eq("id", BigInteger.valueOf(-1));
|
||||
return;
|
||||
}
|
||||
QueryCondition ownerCondition = DOCUMENT_COLLECTION.CREATED_BY.eq(accountId);
|
||||
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.and(ownerCondition);
|
||||
return;
|
||||
}
|
||||
QueryCondition visibilityCondition = DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
|
||||
if (!snapshot.getReadableDeptIds().isEmpty()) {
|
||||
visibilityCondition = visibilityCondition.or(
|
||||
DOCUMENT_COLLECTION.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
|
||||
.and(DOCUMENT_COLLECTION.DEPT_ID.in(snapshot.getReadableDeptIds()))
|
||||
);
|
||||
}
|
||||
QueryCondition readableCondition = visibilityCondition;
|
||||
if (snapshot.isRestricted()) {
|
||||
readableCondition = DOCUMENT_COLLECTION.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
|
||||
}
|
||||
queryWrapper.and(ownerCondition.or(readableCondition));
|
||||
}
|
||||
|
||||
public boolean canRead(DocumentCollection collection) {
|
||||
return canRead(collection, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public boolean canRead(DocumentCollection collection, KnowledgeReadAccessSnapshot snapshot) {
|
||||
if (collection == null) {
|
||||
return false;
|
||||
}
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId != null && accountId.equals(collection.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (snapshot.isRestricted() && (collection.getCategoryId() == null || !snapshot.getCategoryIds().contains(collection.getCategoryId()))) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(collection.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
return VisibilityScope.DEPT == scope
|
||||
&& collection.getDeptId() != null
|
||||
&& snapshot.getReadableDeptIds().contains(collection.getDeptId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class WorkflowExecKeyResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private WorkflowExecResultService workflowExecResultService;
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.EXEC_KEY == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("工作流执行记录不存在");
|
||||
}
|
||||
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(String.valueOf(identifier));
|
||||
if (execResult == null) {
|
||||
throw new BusinessException("工作流执行记录不存在");
|
||||
}
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(execResult.getWorkflowId()));
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(workflow, execResult.getCreatedBy());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.permission.resource.ResolvedResourceAccess;
|
||||
import tech.easyflow.system.permission.resource.ResourceAccessResolver;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class WorkflowIdResourceAccessResolver implements ResourceAccessResolver {
|
||||
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
|
||||
@Override
|
||||
public boolean supports(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return CategoryResourceType.WORKFLOW == resourceType && ResourceLookup.WORKFLOW_ID == lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvedResourceAccess resolve(Object identifier) {
|
||||
if (identifier == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("工作流不存在");
|
||||
}
|
||||
return new ResolvedResourceAccess(workflow, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class WorkflowReadAccessSnapshot {
|
||||
|
||||
private final RoleCategoryAccessSnapshot categoryAccess;
|
||||
private final Set<BigInteger> readableDeptIds;
|
||||
|
||||
public WorkflowReadAccessSnapshot(RoleCategoryAccessSnapshot categoryAccess, Set<BigInteger> readableDeptIds) {
|
||||
this.categoryAccess = categoryAccess;
|
||||
this.readableDeptIds = readableDeptIds == null
|
||||
? Collections.emptySet()
|
||||
: Collections.unmodifiableSet(new LinkedHashSet<>(readableDeptIds));
|
||||
}
|
||||
|
||||
public BigInteger getAccountId() {
|
||||
return categoryAccess == null ? null : categoryAccess.getAccountId();
|
||||
}
|
||||
|
||||
public boolean isSuperAdmin() {
|
||||
return categoryAccess != null && categoryAccess.isSuperAdmin();
|
||||
}
|
||||
|
||||
public boolean isRestricted() {
|
||||
return categoryAccess == null || categoryAccess.isRestricted();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getCategoryIds() {
|
||||
return categoryAccess == null ? Collections.emptySet() : categoryAccess.getCategoryIds();
|
||||
}
|
||||
|
||||
public Set<BigInteger> getReadableDeptIds() {
|
||||
return readableDeptIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import com.mybatisflex.core.query.QueryCondition;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import static tech.easyflow.ai.entity.table.WorkflowTableDef.WORKFLOW;
|
||||
|
||||
@Component
|
||||
public class WorkflowVisibilityQueryHelper {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
public WorkflowReadAccessSnapshot getCurrentReadSnapshot() {
|
||||
RoleCategoryAccessSnapshot categoryAccess = categoryPermissionService.getCurrentAccess(CategoryResourceType.WORKFLOW.getCode());
|
||||
if (categoryAccess.isSuperAdmin()) {
|
||||
return new WorkflowReadAccessSnapshot(categoryAccess, Collections.emptySet());
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
Set<BigInteger> deptIds = loginAccount == null
|
||||
? Collections.emptySet()
|
||||
: sysDeptService.getSelfAndAncestorDeptIds(loginAccount.getDeptId());
|
||||
return new WorkflowReadAccessSnapshot(categoryAccess, deptIds);
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper) {
|
||||
applyReadableAccess(queryWrapper, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public void applyReadableAccess(QueryWrapper queryWrapper, WorkflowReadAccessSnapshot snapshot) {
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId == null) {
|
||||
queryWrapper.eq("id", BigInteger.valueOf(-1));
|
||||
return;
|
||||
}
|
||||
QueryCondition ownerCondition = WORKFLOW.CREATED_BY.eq(accountId);
|
||||
if (snapshot.isRestricted() && snapshot.getCategoryIds().isEmpty()) {
|
||||
queryWrapper.and(ownerCondition);
|
||||
return;
|
||||
}
|
||||
QueryCondition visibilityCondition = WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.PUBLIC.name());
|
||||
if (!snapshot.getReadableDeptIds().isEmpty()) {
|
||||
visibilityCondition = visibilityCondition.or(
|
||||
WORKFLOW.VISIBILITY_SCOPE.eq(VisibilityScope.DEPT.name())
|
||||
.and(WORKFLOW.DEPT_ID.in(snapshot.getReadableDeptIds()))
|
||||
);
|
||||
}
|
||||
QueryCondition readableCondition = visibilityCondition;
|
||||
if (snapshot.isRestricted()) {
|
||||
readableCondition = WORKFLOW.CATEGORY_ID.in(snapshot.getCategoryIds()).and(visibilityCondition);
|
||||
}
|
||||
queryWrapper.and(ownerCondition.or(readableCondition));
|
||||
}
|
||||
|
||||
public boolean canRead(Workflow workflow) {
|
||||
return canRead(workflow, getCurrentReadSnapshot());
|
||||
}
|
||||
|
||||
public boolean canRead(Workflow workflow, WorkflowReadAccessSnapshot snapshot) {
|
||||
if (workflow == null) {
|
||||
return false;
|
||||
}
|
||||
if (snapshot.isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
BigInteger accountId = snapshot.getAccountId();
|
||||
if (accountId != null && accountId.equals(workflow.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (snapshot.isRestricted() && (workflow.getCategoryId() == null || !snapshot.getCategoryIds().contains(workflow.getCategoryId()))) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(workflow.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
return VisibilityScope.DEPT == scope
|
||||
&& workflow.getDeptId() != null
|
||||
&& snapshot.getReadableDeptIds().contains(workflow.getDeptId());
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ public interface ModelService extends IService<Model> {
|
||||
|
||||
List<Model> listInvokeModels();
|
||||
|
||||
List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType);
|
||||
|
||||
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
|
||||
|
||||
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled);
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||
import com.easyagents.core.model.rerank.RerankModel;
|
||||
import com.easyagents.core.store.VectorData;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.util.StringUtil;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -22,6 +23,9 @@ import tech.easyflow.ai.entity.ModelProvider;
|
||||
import tech.easyflow.ai.mapper.ModelMapper;
|
||||
import tech.easyflow.ai.service.ModelProviderService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.util.SqlOperatorsUtil;
|
||||
import tech.easyflow.common.util.SqlUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -249,6 +253,18 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create(
|
||||
entity,
|
||||
entity == null ? com.mybatisflex.core.query.SqlOperators.empty() : SqlOperatorsUtil.build(entity.getClass())
|
||||
);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType));
|
||||
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
|
||||
list.forEach(this::decorateModelTitle);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
|
||||
Model existing = getModelInstance(id);
|
||||
@@ -281,6 +297,21 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
||||
return updatedModels;
|
||||
}
|
||||
|
||||
private void decorateModelTitle(Model model) {
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
String providerName = Optional.ofNullable(model.getModelProvider())
|
||||
.map(ModelProvider::getProviderName)
|
||||
.orElse("-");
|
||||
model.setTitle(providerName + "/" + model.getTitle());
|
||||
}
|
||||
|
||||
private String buildOrderBy(String sortKey, String sortType) {
|
||||
sortKey = StringUtil.camelToUnderline(sortKey);
|
||||
return SqlUtil.buildOrderBy(sortKey, sortType, "id desc");
|
||||
}
|
||||
|
||||
private String buildDefaultInvokeCode(String modelName) {
|
||||
if (StrUtil.isBlank(modelName)) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package tech.easyflow.ai.permission;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class KnowledgeVisibilityQueryHelperTest {
|
||||
|
||||
private final KnowledgeVisibilityQueryHelper helper = new KnowledgeVisibilityQueryHelper();
|
||||
|
||||
@Test
|
||||
public void canRead_shouldAllowCreatorForPrivateKnowledge() {
|
||||
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PRIVATE");
|
||||
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
|
||||
BigInteger.valueOf(11),
|
||||
false,
|
||||
false,
|
||||
Collections.emptySet(),
|
||||
Collections.emptySet()
|
||||
);
|
||||
|
||||
Assert.assertTrue(helper.canRead(collection, snapshot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRead_shouldRejectWhenCategoryNotMatched() {
|
||||
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC");
|
||||
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
|
||||
BigInteger.valueOf(12),
|
||||
false,
|
||||
false,
|
||||
setOf(BigInteger.valueOf(99)),
|
||||
Collections.emptySet()
|
||||
);
|
||||
|
||||
Assert.assertFalse(helper.canRead(collection, snapshot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRead_shouldAllowDeptScopedKnowledgeForDescendantUser() {
|
||||
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT");
|
||||
collection.setDeptId(BigInteger.valueOf(3));
|
||||
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
|
||||
BigInteger.valueOf(12),
|
||||
false,
|
||||
false,
|
||||
setOf(BigInteger.valueOf(21)),
|
||||
setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9))
|
||||
);
|
||||
|
||||
Assert.assertTrue(helper.canRead(collection, snapshot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRead_shouldRejectDeptScopedKnowledgeWithoutDeptMatch() {
|
||||
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "DEPT");
|
||||
collection.setDeptId(BigInteger.valueOf(7));
|
||||
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
|
||||
BigInteger.valueOf(12),
|
||||
false,
|
||||
false,
|
||||
setOf(BigInteger.valueOf(21)),
|
||||
setOf(BigInteger.valueOf(1), BigInteger.valueOf(3), BigInteger.valueOf(9))
|
||||
);
|
||||
|
||||
Assert.assertFalse(helper.canRead(collection, snapshot));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void canRead_shouldAllowPublicKnowledgeWhenCategoryMatched() {
|
||||
DocumentCollection collection = buildCollection(BigInteger.valueOf(11), BigInteger.valueOf(21), "PUBLIC");
|
||||
KnowledgeReadAccessSnapshot snapshot = buildSnapshot(
|
||||
BigInteger.valueOf(12),
|
||||
false,
|
||||
false,
|
||||
setOf(BigInteger.valueOf(21)),
|
||||
Collections.emptySet()
|
||||
);
|
||||
|
||||
Assert.assertTrue(helper.canRead(collection, snapshot));
|
||||
}
|
||||
|
||||
private DocumentCollection buildCollection(BigInteger createdBy, BigInteger categoryId, String visibilityScope) {
|
||||
DocumentCollection collection = new DocumentCollection();
|
||||
collection.setCreatedBy(createdBy);
|
||||
collection.setCategoryId(categoryId);
|
||||
collection.setVisibilityScope(visibilityScope);
|
||||
return collection;
|
||||
}
|
||||
|
||||
private KnowledgeReadAccessSnapshot buildSnapshot(BigInteger accountId,
|
||||
boolean superAdmin,
|
||||
boolean allAccess,
|
||||
Set<BigInteger> categoryIds,
|
||||
Set<BigInteger> deptIds) {
|
||||
RoleCategoryAccessSnapshot accessSnapshot = new RoleCategoryAccessSnapshot(
|
||||
"KNOWLEDGE",
|
||||
accountId,
|
||||
superAdmin,
|
||||
allAccess,
|
||||
categoryIds
|
||||
);
|
||||
return new KnowledgeReadAccessSnapshot(accessSnapshot, deptIds);
|
||||
}
|
||||
|
||||
private Set<BigInteger> setOf(BigInteger... values) {
|
||||
Set<BigInteger> result = new LinkedHashSet<>();
|
||||
Collections.addAll(result, values);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
public enum ResourceAction {
|
||||
READ,
|
||||
USE,
|
||||
MANAGE
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
public enum ResourceLookup {
|
||||
WORKFLOW_ID,
|
||||
EXEC_KEY,
|
||||
KNOWLEDGE_ID,
|
||||
KNOWLEDGE_ID_OR_SLUG,
|
||||
DOCUMENT_ID,
|
||||
DOCUMENT_CHUNK_ID,
|
||||
FAQ_ITEM_ID,
|
||||
FAQ_CATEGORY_ID
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package tech.easyflow.system.enums;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum VisibilityScope {
|
||||
|
||||
PRIVATE,
|
||||
DEPT,
|
||||
PUBLIC;
|
||||
|
||||
public static VisibilityScope from(String code) {
|
||||
if (code == null || code.isBlank()) {
|
||||
throw new IllegalArgumentException("visibilityScope不能为空");
|
||||
}
|
||||
String normalized = code.trim().toUpperCase(Locale.ROOT);
|
||||
return Arrays.stream(values())
|
||||
.filter(item -> item.name().equals(normalized))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("不支持的visibilityScope: " + code));
|
||||
}
|
||||
|
||||
public static VisibilityScope fromOrDefault(String code, VisibilityScope defaultValue) {
|
||||
if (code == null || code.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
return from(code);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireResourceAccess {
|
||||
|
||||
CategoryResourceType resource();
|
||||
|
||||
ResourceAction action();
|
||||
|
||||
ResourceLookup lookup();
|
||||
|
||||
String idExpr();
|
||||
|
||||
String denyMessage() default "无权限访问该资源";
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.context.expression.MethodBasedEvaluationContext;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class RequireResourceAccessAspect {
|
||||
|
||||
private final List<ResourceAccessResolver> resolvers;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
private final CategoryPermissionService categoryPermissionService;
|
||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
public RequireResourceAccessAspect(List<ResourceAccessResolver> resolvers,
|
||||
ResourceAccessService resourceAccessService,
|
||||
CategoryPermissionService categoryPermissionService) {
|
||||
this.resolvers = resolvers;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
this.categoryPermissionService = categoryPermissionService;
|
||||
}
|
||||
|
||||
@Around("@annotation(requireResourceAccess)")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) throws Throwable {
|
||||
Object identifier = resolveIdentifier(joinPoint, requireResourceAccess);
|
||||
ResourceAccessResolver resolver = findResolver(requireResourceAccess.resource(), requireResourceAccess.lookup());
|
||||
ResolvedResourceAccess resolved = resolver.resolve(identifier);
|
||||
assertExecutionOwner(resolved);
|
||||
resourceAccessService.assertAccess(
|
||||
requireResourceAccess.resource(),
|
||||
resolved.getResource(),
|
||||
requireResourceAccess.action(),
|
||||
requireResourceAccess.denyMessage()
|
||||
);
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, RequireResourceAccess requireResourceAccess) {
|
||||
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
|
||||
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
|
||||
joinPoint.getTarget(),
|
||||
method,
|
||||
joinPoint.getArgs(),
|
||||
parameterNameDiscoverer
|
||||
);
|
||||
return expressionParser.parseExpression(requireResourceAccess.idExpr()).getValue(context);
|
||||
}
|
||||
|
||||
private ResourceAccessResolver findResolver(CategoryResourceType resourceType, ResourceLookup lookup) {
|
||||
return resolvers.stream()
|
||||
.filter(item -> item.supports(resourceType, lookup))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("未找到资源访问解析器: " + resourceType + "/" + lookup));
|
||||
}
|
||||
|
||||
private void assertExecutionOwner(ResolvedResourceAccess resolved) {
|
||||
String executionOwnerKey = resolved.getExecutionOwnerKey();
|
||||
if (executionOwnerKey == null || executionOwnerKey.isBlank() || categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
String accountId = loginAccount == null || loginAccount.getId() == null ? null : loginAccount.getId().toString();
|
||||
if (!executionOwnerKey.equals(accountId)) {
|
||||
throw new BusinessException("无权限访问该执行记录");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
public class ResolvedResourceAccess {
|
||||
|
||||
private final VisibilityResource resource;
|
||||
private final String executionOwnerKey;
|
||||
|
||||
public ResolvedResourceAccess(VisibilityResource resource, String executionOwnerKey) {
|
||||
this.resource = resource;
|
||||
this.executionOwnerKey = executionOwnerKey;
|
||||
}
|
||||
|
||||
public VisibilityResource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public String getExecutionOwnerKey() {
|
||||
return executionOwnerKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceLookup;
|
||||
|
||||
public interface ResourceAccessResolver {
|
||||
|
||||
boolean supports(CategoryResourceType resourceType, ResourceLookup lookup);
|
||||
|
||||
ResolvedResourceAccess resolve(Object identifier);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.system.permission.resource;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface VisibilityResource {
|
||||
|
||||
BigInteger getCreatedBy();
|
||||
|
||||
BigInteger getDeptId();
|
||||
|
||||
BigInteger getCategoryId();
|
||||
|
||||
String getVisibilityScope();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.system.service;
|
||||
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
public interface ResourceAccessService {
|
||||
|
||||
boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action);
|
||||
|
||||
void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message);
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package tech.easyflow.system.service;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门表 服务层。
|
||||
*
|
||||
@@ -11,4 +14,7 @@ import com.mybatisflex.core.service.IService;
|
||||
*/
|
||||
public interface SysDeptService extends IService<SysDept> {
|
||||
|
||||
Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId);
|
||||
|
||||
boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package tech.easyflow.system.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
import tech.easyflow.system.service.SysDeptService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
|
||||
@Service
|
||||
public class ResourceAccessServiceImpl implements ResourceAccessService {
|
||||
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Resource
|
||||
private SysDeptService sysDeptService;
|
||||
|
||||
@Override
|
||||
public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) {
|
||||
if (resource == null) {
|
||||
return false;
|
||||
}
|
||||
LoginAccount loginAccount = SaTokenUtil.getLoginAccount();
|
||||
if (loginAccount == null || loginAccount.getId() == null) {
|
||||
return false;
|
||||
}
|
||||
BigInteger accountId = loginAccount.getId();
|
||||
if (categoryPermissionService.isCurrentSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (accountId.equals(resource.getCreatedBy())) {
|
||||
return true;
|
||||
}
|
||||
if (ResourceAction.MANAGE == action) {
|
||||
return false;
|
||||
}
|
||||
if (!categoryPermissionService.canAccessCategory(resourceType.getCode(), resource.getCreatedBy(), resource.getCategoryId())) {
|
||||
return false;
|
||||
}
|
||||
VisibilityScope scope = VisibilityScope.fromOrDefault(resource.getVisibilityScope(), VisibilityScope.PRIVATE);
|
||||
if (VisibilityScope.PUBLIC == scope) {
|
||||
return true;
|
||||
}
|
||||
if (VisibilityScope.DEPT == scope) {
|
||||
return sysDeptService.canUserAccessDeptScopedResource(loginAccount.getDeptId(), resource.getDeptId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) {
|
||||
if (!canAccess(resourceType, resource, action)) {
|
||||
throw new BusinessException(message == null ? "无权限访问该资源" : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import tech.easyflow.system.service.SysDeptService;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 部门表 服务层实现。
|
||||
*
|
||||
@@ -15,4 +20,39 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDept> implements SysDeptService {
|
||||
|
||||
@Override
|
||||
public Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId) {
|
||||
if (currentDeptId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
SysDept currentDept = getById(currentDeptId);
|
||||
if (currentDept == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<BigInteger> deptIds = new LinkedHashSet<>();
|
||||
String ancestors = currentDept.getAncestors();
|
||||
if (ancestors != null && !ancestors.isBlank()) {
|
||||
String[] items = ancestors.split(",");
|
||||
for (String item : items) {
|
||||
if (item == null || item.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
BigInteger deptId = new BigInteger(item.trim());
|
||||
if (BigInteger.ZERO.equals(deptId)) {
|
||||
continue;
|
||||
}
|
||||
deptIds.add(deptId);
|
||||
}
|
||||
}
|
||||
deptIds.add(currentDeptId);
|
||||
return deptIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId) {
|
||||
if (currentDeptId == null || resourceDeptId == null) {
|
||||
return false;
|
||||
}
|
||||
return getSelfAndAncestorDeptIds(currentDeptId).contains(resourceDeptId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package tech.easyflow.system.service.impl;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Set;
|
||||
|
||||
public class SysDeptServiceImplTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnSelfAndAncestors() {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setId(BigInteger.valueOf(5));
|
||||
dept.setAncestors("0,1,3");
|
||||
|
||||
SysDeptServiceImpl service = new SysDeptServiceImpl() {
|
||||
@Override
|
||||
public SysDept getById(Serializable id) {
|
||||
return dept;
|
||||
}
|
||||
};
|
||||
|
||||
Set<BigInteger> deptIds = service.getSelfAndAncestorDeptIds(BigInteger.valueOf(5));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(1)));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(3)));
|
||||
Assert.assertTrue(deptIds.contains(BigInteger.valueOf(5)));
|
||||
Assert.assertFalse(deptIds.contains(BigInteger.ZERO));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMatchDeptScopedResourceByAncestorChain() {
|
||||
SysDept dept = new SysDept();
|
||||
dept.setId(BigInteger.valueOf(9));
|
||||
dept.setAncestors("1,5");
|
||||
|
||||
SysDeptServiceImpl service = new SysDeptServiceImpl() {
|
||||
@Override
|
||||
public SysDept getById(Serializable id) {
|
||||
return dept;
|
||||
}
|
||||
};
|
||||
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(1)));
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(5)));
|
||||
Assert.assertTrue(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(9)));
|
||||
Assert.assertFalse(service.canUserAccessDeptScopedResource(BigInteger.valueOf(9), BigInteger.valueOf(7)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE tb_document_collection
|
||||
ADD COLUMN visibility_scope VARCHAR(32) NULL COMMENT '可见范围';
|
||||
|
||||
UPDATE tb_document_collection
|
||||
SET visibility_scope = 'PUBLIC'
|
||||
WHERE visibility_scope IS NULL OR visibility_scope = '';
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE tb_workflow
|
||||
ADD COLUMN visibility_scope VARCHAR(32) NULL COMMENT '可见范围';
|
||||
|
||||
UPDATE tb_workflow
|
||||
SET visibility_scope = 'PUBLIC'
|
||||
WHERE visibility_scope IS NULL OR visibility_scope = '';
|
||||
@@ -46,6 +46,9 @@ export interface CardListProps {
|
||||
titleField?: string;
|
||||
descField?: string;
|
||||
actions?: ActionButton[];
|
||||
cornerTagField?: string;
|
||||
cornerTagMap?: Record<string, string>;
|
||||
cornerTagTypeMap?: Record<string, string>;
|
||||
defaultIcon: any;
|
||||
data: any[];
|
||||
primaryAction?: CardPrimaryAction;
|
||||
@@ -58,6 +61,9 @@ const props = withDefaults(defineProps<CardListProps>(), {
|
||||
titleField: 'title',
|
||||
descField: 'description',
|
||||
actions: () => [],
|
||||
cornerTagField: '',
|
||||
cornerTagMap: () => ({}),
|
||||
cornerTagTypeMap: () => ({}),
|
||||
primaryAction: undefined,
|
||||
tagField: '',
|
||||
tagMap: () => ({}),
|
||||
@@ -154,6 +160,23 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
{{ item[descField] }}
|
||||
</ElText>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
|
||||
class="card-corner-tag"
|
||||
>
|
||||
<slot name="corner" :item="item">
|
||||
<ElTag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="cornerTagTypeMap[item[cornerTagField]] || 'info'"
|
||||
round
|
||||
>
|
||||
{{
|
||||
cornerTagMap[item[cornerTagField]] || item[cornerTagField]
|
||||
}}
|
||||
</ElTag>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -343,6 +366,26 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.card-corner-tag {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
min-height: 28px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.card-corner-tag :deep(.el-tag) {
|
||||
--el-tag-border-radius: 999px;
|
||||
--el-tag-font-size: 12px;
|
||||
--el-tag-border-color: transparent;
|
||||
|
||||
padding: 0 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"englishName": "EnglishName",
|
||||
"status": "ShowInUserCenter",
|
||||
"categoryId": "Category",
|
||||
"visibilityScope": "Visibility Scope",
|
||||
"visibilityScopePrivate": "Personal",
|
||||
"visibilityScopePrivateDesc": "Only the creator can access it",
|
||||
"visibilityScopeDept": "Dept",
|
||||
"visibilityScopeDeptDesc": "Available to the dept and descendants",
|
||||
"visibilityScopePublic": "Public",
|
||||
"visibilityScopePublicDesc": "Available to internal users matched by category",
|
||||
"params": "Params",
|
||||
"steps": "Steps",
|
||||
"result": "Result",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"collectionTypeDocument": "Document",
|
||||
"collectionTypeFaq": "FAQ",
|
||||
"description": "Description",
|
||||
"visibilityScope": "Visibility Scope",
|
||||
"visibilityScopePrivate": "Personal",
|
||||
"visibilityScopePrivateDesc": "Only the creator can access it",
|
||||
"visibilityScopeDept": "Dept",
|
||||
"visibilityScopeDeptDesc": "Available to the dept and descendants",
|
||||
"visibilityScopePublic": "Public",
|
||||
"visibilityScopePublicDesc": "Available to internal users matched by category",
|
||||
"slug": "Slug",
|
||||
"vectorStoreEnable": "VectorStoreEnable",
|
||||
"vectorStoreType": "VectorStoreType",
|
||||
@@ -46,12 +53,24 @@
|
||||
},
|
||||
"importDoc": {
|
||||
"fileUpload": "File upload",
|
||||
"parameterSettings": "ParameterSettings",
|
||||
"parameterSettings": "Parameter settings",
|
||||
"strategyAnalysis": "Strategy analysis",
|
||||
"segmentedPreview": "SegmentedPreview",
|
||||
"confirmImport": "ConfirmImport",
|
||||
"fileName": "File Name",
|
||||
"progressUpload": "Progress of file upload",
|
||||
"fileSize": "File size"
|
||||
"fileSize": "File size",
|
||||
"analysisTip": "The system analyzes multilingual structure first and recommends a splitting strategy. You can still adjust each file manually.",
|
||||
"confidence": "Confidence",
|
||||
"recommendReason": "Reasons",
|
||||
"candidateStrategies": "Candidates",
|
||||
"strategySelection": "Strategy",
|
||||
"previewTip": "The preview result is the final import basis. Confirm it before committing.",
|
||||
"previewEmpty": "No preview data",
|
||||
"warningCount": "Warnings",
|
||||
"chunkCount": "Chunks",
|
||||
"resultEmpty": "No import result",
|
||||
"importFailed": "Import failed"
|
||||
},
|
||||
"splitterDoc": {
|
||||
"fileType": "FileType",
|
||||
@@ -64,6 +83,12 @@
|
||||
"simpleTokenizeSplitter": "SimpleTokenizeSplitter",
|
||||
"regexDocumentSplitter": "RegexDocumentSplitter",
|
||||
"markdownHeaderSplitter": "MarkdownHeaderSplitter",
|
||||
"autoStrategy": "Auto recommendation",
|
||||
"markdownSection": "Markdown headings",
|
||||
"outlineSection": "Outline sections",
|
||||
"qaPair": "Q&A pairs",
|
||||
"paragraphLength": "Paragraph length",
|
||||
"customRegex": "Custom regex",
|
||||
"mdSplitterLevel": "MarkdownSplitterLevel",
|
||||
"uploadStatus": "UploadStatus",
|
||||
"pendingUpload": "PendingUpload",
|
||||
@@ -139,5 +164,6 @@
|
||||
"tencentCloud": "tencentCloud",
|
||||
"vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model",
|
||||
"dimensionOfVectorModelTips": "After successful vector data, it is not allowed to modify the dimensions of the vector model",
|
||||
"dimensionOfVectorModel": "Dimension of vector model"
|
||||
"dimensionOfVectorModel": "Dimension of vector model",
|
||||
"managePermissionHint": "Only the creator or super admin can modify this knowledge base"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
"englishName": "英文名称",
|
||||
"status": "在用户中心显示",
|
||||
"categoryId": "分类",
|
||||
"visibilityScope": "可见范围",
|
||||
"visibilityScopePrivate": "个人",
|
||||
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||
"visibilityScopeDept": "部门",
|
||||
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||
"visibilityScopePublic": "公开",
|
||||
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||
"params": "执行参数",
|
||||
"steps": "执行步骤",
|
||||
"result": "执行结果",
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
"collectionTypeDocument": "文档",
|
||||
"collectionTypeFaq": "FAQ",
|
||||
"description": "描述",
|
||||
"visibilityScope": "可见范围",
|
||||
"visibilityScopePrivate": "个人",
|
||||
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||
"visibilityScopeDept": "部门",
|
||||
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||
"visibilityScopePublic": "公开",
|
||||
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||
"slug": "URL 别名",
|
||||
"vectorStoreEnable": "是否启用向量数据库",
|
||||
"vectorStoreType": "向量数据库类型",
|
||||
@@ -47,11 +54,23 @@
|
||||
"importDoc": {
|
||||
"fileUpload": "文件上传",
|
||||
"parameterSettings": "参数设置",
|
||||
"strategyAnalysis": "策略分析",
|
||||
"segmentedPreview": "分段预览",
|
||||
"confirmImport": "确认导入",
|
||||
"fileName": "文件名称",
|
||||
"progressUpload": "文件上传进度",
|
||||
"fileSize": "文件大小"
|
||||
"fileSize": "文件大小",
|
||||
"analysisTip": "系统会先基于文档结构做中英文规则分析,再推荐拆分策略,你也可以逐个文件手动调整。",
|
||||
"confidence": "置信度",
|
||||
"recommendReason": "推荐理由",
|
||||
"candidateStrategies": "备选策略",
|
||||
"strategySelection": "拆分策略",
|
||||
"previewTip": "预览结果就是最终入库依据,确认无误后再执行导入。",
|
||||
"previewEmpty": "暂无可预览内容",
|
||||
"warningCount": "警告数",
|
||||
"chunkCount": "分块数",
|
||||
"resultEmpty": "暂无导入结果",
|
||||
"importFailed": "导入失败"
|
||||
},
|
||||
"splitterDoc": {
|
||||
"fileType": "文件类型",
|
||||
@@ -64,6 +83,12 @@
|
||||
"simpleTokenizeSplitter": "简单分词器",
|
||||
"regexDocumentSplitter": "正则文档分割器",
|
||||
"markdownHeaderSplitter": "Markdown标题层级拆分器",
|
||||
"autoStrategy": "自动推荐",
|
||||
"markdownSection": "Markdown 标题拆分",
|
||||
"outlineSection": "章节标题拆分",
|
||||
"qaPair": "问答对拆分",
|
||||
"paragraphLength": "自然段长度拆分",
|
||||
"customRegex": "自定义正则拆分",
|
||||
"mdSplitterLevel": "Markdown标题等级",
|
||||
"uploadStatus": "上传状态",
|
||||
"pendingUpload": "待上传",
|
||||
@@ -139,5 +164,6 @@
|
||||
"tencentCloud": "腾讯云",
|
||||
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",
|
||||
"dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度",
|
||||
"dimensionOfVectorModel": "向量模型维度"
|
||||
"dimensionOfVectorModel": "向量模型维度",
|
||||
"managePermissionHint": "仅创建者或超级管理员可修改当前知识库"
|
||||
}
|
||||
|
||||
@@ -27,14 +27,26 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const pageDataRef = ref();
|
||||
const handleEdit = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
form.value = { id: row.id, content: row.content };
|
||||
openDialog();
|
||||
};
|
||||
const handleDelete = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
@@ -68,6 +80,10 @@ const queryParams = ref({
|
||||
sortType: 'asc',
|
||||
});
|
||||
const save = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
|
||||
btnLoading.value = false;
|
||||
@@ -103,7 +119,12 @@ const form = ref({
|
||||
:label="$t('documentCollection.content')"
|
||||
min-width="240"
|
||||
/>
|
||||
<ElTableColumn :label="$t('common.handle')" width="100" align="right">
|
||||
<ElTableColumn
|
||||
v-if="props.manageable"
|
||||
:label="$t('common.handle')"
|
||||
width="100"
|
||||
align="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton link type="primary" @click="handleEdit(row)">
|
||||
@@ -130,6 +151,7 @@ const form = ref({
|
||||
</template>
|
||||
</PageData>
|
||||
<EasyFlowFormModal
|
||||
v-if="props.manageable"
|
||||
v-model:open="dialogVisible"
|
||||
:closable="!btnLoading"
|
||||
:title="$t('button.edit')"
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElImage } from 'element-plus';
|
||||
@@ -20,11 +22,34 @@ import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearch
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
const knowledgeId = ref<string>((route.query.id as string) || '');
|
||||
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
||||
const knowledgeInfo = ref<any>({});
|
||||
const selectedCategory = ref('');
|
||||
const canManageKnowledgePermission = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/documentCollection/save']),
|
||||
);
|
||||
|
||||
const isSuperAdmin = computed(() => {
|
||||
return (
|
||||
String(userStore.userInfo?.id || '') === '1' ||
|
||||
(userStore.userRoles || []).includes('super_admin')
|
||||
);
|
||||
});
|
||||
|
||||
const canManageCurrentKnowledge = computed(() => {
|
||||
if (!knowledgeInfo.value?.id || !canManageKnowledgePermission.value) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isSuperAdmin.value ||
|
||||
String(userStore.userInfo?.id || '') ===
|
||||
String(knowledgeInfo.value.createdBy || '')
|
||||
);
|
||||
});
|
||||
|
||||
const syncNavTitle = (title: string) => {
|
||||
if (!title) {
|
||||
@@ -163,6 +188,9 @@ const backDoc = () => {
|
||||
<div class="description">
|
||||
{{ knowledgeInfo.description || '' }}
|
||||
</div>
|
||||
<div v-if="!canManageCurrentKnowledge" class="permission-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-top-menu">
|
||||
<button
|
||||
@@ -184,7 +212,7 @@ const backDoc = () => {
|
||||
<div class="doc-header" v-if="!viewDocVisible">
|
||||
<HeaderSearch
|
||||
v-if="!isFaqCollection"
|
||||
:buttons="headerButtons"
|
||||
:buttons="canManageCurrentKnowledge ? headerButtons : []"
|
||||
@search="handleSearch"
|
||||
@button-click="handleButtonClick"
|
||||
/>
|
||||
@@ -192,6 +220,7 @@ const backDoc = () => {
|
||||
<DocumentTable
|
||||
ref="documentTableRef"
|
||||
:knowledge-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
@view-doc="viewDoc"
|
||||
v-if="!viewDocVisible"
|
||||
/>
|
||||
@@ -199,24 +228,32 @@ const backDoc = () => {
|
||||
<ChunkDocumentTable
|
||||
v-else
|
||||
:document-id="documentId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
:default-summary-prompt="knowledgeInfo.summaryPrompt"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="selectedCategory === 'faqList'" class="doc-table">
|
||||
<FaqTable :knowledge-id="knowledgeId" />
|
||||
<FaqTable
|
||||
:knowledge-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
/>
|
||||
</div>
|
||||
<!--知识检索-->
|
||||
<div
|
||||
v-if="selectedCategory === 'knowledgeSearch'"
|
||||
class="doc-search-container"
|
||||
>
|
||||
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
|
||||
<KnowledgeSearchConfig
|
||||
:document-collection-id="knowledgeId"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
/>
|
||||
<KnowledgeSearch :knowledge-id="knowledgeId" />
|
||||
</div>
|
||||
<!--配置-->
|
||||
<div v-if="selectedCategory === 'config'">
|
||||
<DocumentCollectionDataConfig
|
||||
:detail-data="knowledgeInfo"
|
||||
:manageable="canManageCurrentKnowledge"
|
||||
@reload="getKnowledge"
|
||||
/>
|
||||
</div>
|
||||
@@ -306,6 +343,13 @@ const backDoc = () => {
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.permission-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.doc-table {
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import type {
|
||||
ActionButton,
|
||||
CardPrimaryAction,
|
||||
} from '#/components/page/CardList.vue';
|
||||
|
||||
import {computed, onMounted, ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { useAccess } from '@easyflow/access';
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
|
||||
import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue';
|
||||
import {tryit} from 'radash';
|
||||
import {
|
||||
Check,
|
||||
Delete,
|
||||
Edit,
|
||||
Lock,
|
||||
Notebook,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
Search,
|
||||
} from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
import defaultIcon from '#/assets/ai/knowledge/book.svg';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardPage from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
const collectionTypeLabelMap = {
|
||||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
||||
};
|
||||
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
|
||||
|
||||
const canManageKnowledgePermission = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/documentCollection/save']),
|
||||
);
|
||||
const updatingScopeId = ref<null | number | string>(null);
|
||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||
const visibilityScopeMeta = computed(() => ({
|
||||
PRIVATE: {
|
||||
label: $t('documentCollection.visibilityScopePrivate'),
|
||||
description: $t('documentCollection.visibilityScopePrivateDesc'),
|
||||
icon: Lock,
|
||||
tone: 'private',
|
||||
},
|
||||
DEPT: {
|
||||
label: $t('documentCollection.visibilityScopeDept'),
|
||||
description: $t('documentCollection.visibilityScopeDeptDesc'),
|
||||
icon: OfficeBuilding,
|
||||
tone: 'dept',
|
||||
},
|
||||
PUBLIC: {
|
||||
label: $t('documentCollection.visibilityScopePublic'),
|
||||
description: $t('documentCollection.visibilityScopePublicDesc'),
|
||||
icon: Promotion,
|
||||
tone: 'public',
|
||||
},
|
||||
}));
|
||||
const visibilityScopeOptions = computed(() =>
|
||||
(['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
|
||||
value,
|
||||
...visibilityScopeMeta.value[value],
|
||||
})),
|
||||
);
|
||||
|
||||
function isSuperAdmin() {
|
||||
return (
|
||||
String(userStore.userInfo?.id || '') === '1' ||
|
||||
(userStore.userRoles || []).includes('super_admin')
|
||||
);
|
||||
}
|
||||
|
||||
function canManageKnowledgeItem(row: Record<string, any>) {
|
||||
if (!canManageKnowledgePermission.value) {
|
||||
return false;
|
||||
}
|
||||
const currentUserId = String(userStore.userInfo?.id || '');
|
||||
return isSuperAdmin() || currentUserId === String(row?.createdBy || '');
|
||||
}
|
||||
|
||||
function ensureManageKnowledgeItem(row: Record<string, any>) {
|
||||
if (canManageKnowledgeItem(row)) {
|
||||
return true;
|
||||
}
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return false;
|
||||
}
|
||||
|
||||
function resolveNavTitle(row: Record<string, any>) {
|
||||
return row?.title || row?.name || '';
|
||||
}
|
||||
|
||||
function openKnowledgeDetail(row: { id: string; name?: string; title?: string }) {
|
||||
function openKnowledgeDetail(row: {
|
||||
id: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
}) {
|
||||
router.push({
|
||||
path: '/ai/documentCollection/document',
|
||||
query: {
|
||||
@@ -56,7 +142,7 @@ interface FieldDefinition {
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: Notebook,
|
||||
text: $t('documentCollection.actions.knowledge'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
permission: '/api/v1/documentCollection/query',
|
||||
onClick(row) {
|
||||
openKnowledgeDetail(row);
|
||||
},
|
||||
@@ -69,6 +155,9 @@ const actions: ActionButton[] = [
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'inline',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
aiKnowledgeModalRef.value.openDialog(row);
|
||||
},
|
||||
},
|
||||
@@ -95,6 +184,9 @@ const actions: ActionButton[] = [
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
placement: 'inline',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
handleDelete(row);
|
||||
},
|
||||
},
|
||||
@@ -178,6 +270,9 @@ const formRules = computed(() => {
|
||||
const handleSearch = (params: any) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
const reloadKnowledgeList = () => {
|
||||
pageDataRef.value?.reload?.();
|
||||
};
|
||||
const formData = ref<any>({});
|
||||
const dialogVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
@@ -189,7 +284,7 @@ function showControlDialog(item: any) {
|
||||
const categoryList = ref<any[]>([]);
|
||||
const getCategoryList = async () => {
|
||||
const [, res] = await tryit(api.get)(
|
||||
'/api/v1/documentCollectionCategory/list',
|
||||
'/api/v1/documentCollectionCategory/visibleList',
|
||||
{
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
},
|
||||
@@ -258,6 +353,51 @@ const footerButton = {
|
||||
},
|
||||
};
|
||||
const saveLoading = ref(false);
|
||||
function resolveVisibilityScopeMeta(scope?: string) {
|
||||
return (
|
||||
visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
|
||||
visibilityScopeMeta.value.PRIVATE
|
||||
);
|
||||
}
|
||||
function setVisibilityScopePopoverRef(id: number | string, el: any) {
|
||||
const cacheKey = String(id);
|
||||
if (el) {
|
||||
visibilityScopePopoverRefs.value[cacheKey] = el;
|
||||
return;
|
||||
}
|
||||
delete visibilityScopePopoverRefs.value[cacheKey];
|
||||
}
|
||||
function closeVisibilityScopePopover(id: number | string) {
|
||||
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
|
||||
}
|
||||
async function updateVisibilityScope(
|
||||
row: any,
|
||||
visibilityScope: VisibilityScope,
|
||||
) {
|
||||
if (
|
||||
!canManageKnowledgeItem(row) ||
|
||||
!row?.id ||
|
||||
updatingScopeId.value === row.id ||
|
||||
row.visibilityScope === visibilityScope
|
||||
) {
|
||||
closeVisibilityScopePopover(row.id);
|
||||
return;
|
||||
}
|
||||
updatingScopeId.value = row.id;
|
||||
try {
|
||||
const res = await api.post('/api/v1/documentCollection/update', {
|
||||
id: row.id,
|
||||
visibilityScope,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
row.visibilityScope = visibilityScope;
|
||||
ElMessage.success($t('message.updateOkMessage'));
|
||||
closeVisibilityScopePopover(row.id);
|
||||
}
|
||||
} finally {
|
||||
updatingScopeId.value = null;
|
||||
}
|
||||
}
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate((valid) => {
|
||||
if (valid) {
|
||||
@@ -318,7 +458,97 @@ function changeCategory(category: any) {
|
||||
:actions="actions"
|
||||
tag-field="collectionType"
|
||||
:tag-map="collectionTypeLabelMap"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageKnowledgeItem(item)"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="knowledge-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="knowledge-scope-chip"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="knowledge-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="knowledge-scope-option"
|
||||
:class="[
|
||||
`knowledge-scope-option--${option.tone}`,
|
||||
{
|
||||
'knowledge-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="knowledge-scope-option__leading">
|
||||
<span class="knowledge-scope-option__icon-wrap">
|
||||
<ElIcon class="knowledge-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="knowledge-scope-option__text">
|
||||
<span class="knowledge-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="knowledge-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="knowledge-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="knowledge-scope-chip knowledge-scope-chip--readonly"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardPage>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
@@ -362,7 +592,10 @@ function changeCategory(category: any) {
|
||||
</EasyFlowFormModal>
|
||||
|
||||
<!-- 新增知识库模态框-->
|
||||
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
||||
<DocumentCollectionModal
|
||||
ref="aiKnowledgeModalRef"
|
||||
@reload="reloadKnowledgeList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -372,4 +605,206 @@ h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 0.92);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 4px hsl(var(--primary) / 0.12),
|
||||
0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.09);
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
border-color: hsl(var(--warning) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
border-color: hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.knowledge-scope-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.knowledge-scope-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
.knowledge-scope-option:hover {
|
||||
background: hsl(var(--foreground) / 0.04);
|
||||
}
|
||||
|
||||
.knowledge-scope-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__leading {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.knowledge-scope-option__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.knowledge-scope-option__desc {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.knowledge-scope-option__check {
|
||||
font-size: 16px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public .knowledge-scope-option__icon-wrap {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private.knowledge-scope-option--active {
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept.knowledge-scope-option--active {
|
||||
background: hsl(var(--warning) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public.knowledge-scope-option--active {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.knowledge-scope-option--private .knowledge-scope-option__check {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--dept .knowledge-scope-option__check {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.knowledge-scope-option--public .knowledge-scope-option__check {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
:global(.knowledge-visibility-popover.el-popover.el-popper) {
|
||||
padding: 8px;
|
||||
border-radius: 16px;
|
||||
border-color: hsl(var(--line-subtle));
|
||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
@@ -13,13 +17,9 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {computed, onMounted, ref, watch} from 'vue';
|
||||
|
||||
import {InfoFilled} from '@element-plus/icons-vue';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import {$t} from '#/locales';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps({
|
||||
detailData: {
|
||||
@@ -45,7 +45,10 @@ const props = defineProps({
|
||||
searchEngineEnable: false,
|
||||
englishName: '',
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,7 +57,7 @@ const emit = defineEmits(['reload']);
|
||||
const normalizeEntity = (raw: any) => {
|
||||
const options = {
|
||||
canUpdateEmbeddingModel: true,
|
||||
...(raw?.options || {}),
|
||||
...raw?.options,
|
||||
};
|
||||
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
||||
options.rerankEnable = !!raw?.rerankModelId;
|
||||
@@ -102,7 +105,7 @@ const vectorStoreConfigPlaceholder = computed(() => {
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
@@ -115,7 +118,9 @@ const getEmbeddingLlmListData = async () => {
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollection/modelList?modelType=rerankModel',
|
||||
);
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
@@ -156,6 +161,10 @@ const rules = ref({
|
||||
});
|
||||
|
||||
async function save() {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const valid = await saveForm.value?.validate();
|
||||
if (!valid) return;
|
||||
@@ -190,9 +199,13 @@ async function save() {
|
||||
label-width="150px"
|
||||
ref="saveForm"
|
||||
:model="entity"
|
||||
:disabled="!props.manageable"
|
||||
status-icon
|
||||
:rules="rules"
|
||||
>
|
||||
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
<ElFormItem
|
||||
prop="icon"
|
||||
:label="$t('documentCollection.icon')"
|
||||
@@ -371,7 +384,7 @@ async function save() {
|
||||
type="primary"
|
||||
@click="save"
|
||||
:loading="btnLoading"
|
||||
:disabled="btnLoading"
|
||||
:disabled="btnLoading || !props.manageable"
|
||||
>
|
||||
{{ $t('button.save') }}
|
||||
</ElButton>
|
||||
@@ -385,4 +398,11 @@ async function save() {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.config-readonly-tip {
|
||||
margin-bottom: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
@@ -29,7 +29,7 @@ const rerankerLlmList = ref<any>([]);
|
||||
|
||||
const getEmbeddingLlmListData = async () => {
|
||||
try {
|
||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
||||
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||
const res = await api.get(url, {});
|
||||
if (res.errorCode === 0) {
|
||||
embeddingLlmList.value = res.data;
|
||||
@@ -42,7 +42,9 @@ const getEmbeddingLlmListData = async () => {
|
||||
|
||||
const getRerankerLlmListData = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
|
||||
const res = await api.get(
|
||||
'/api/v1/documentCollection/modelList?modelType=rerankModel',
|
||||
);
|
||||
rerankerLlmList.value = res.data;
|
||||
} catch (error) {
|
||||
ElMessage.error($t('message.apiError'));
|
||||
@@ -87,8 +89,9 @@ const defaultEntity = {
|
||||
rerankEnable: false,
|
||||
},
|
||||
rerankModelId: '',
|
||||
searchEngineEnable: '',
|
||||
searchEngineEnable: false,
|
||||
englishName: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
};
|
||||
const normalizeEntity = (raw: any = {}) => {
|
||||
const options = {
|
||||
@@ -135,6 +138,20 @@ const rules = ref({
|
||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||
],
|
||||
});
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopePrivate'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopeDept'),
|
||||
value: 'DEPT',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.visibilityScopePublic'),
|
||||
value: 'PUBLIC',
|
||||
},
|
||||
]);
|
||||
const collectionTypeList = [
|
||||
{
|
||||
label: $t('documentCollection.collectionTypeDocument'),
|
||||
@@ -257,6 +274,19 @@ defineExpose({
|
||||
dict-code="aiDocumentCollectionCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
:label="$t('documentCollection.visibilityScope')"
|
||||
>
|
||||
<ElSelect v-model="entity.visibilityScope" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in visibilityScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||
|
||||
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
@@ -25,6 +26,10 @@ const props = defineProps({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['viewDoc']);
|
||||
defineExpose({
|
||||
@@ -38,10 +43,20 @@ const pageDataRef = ref();
|
||||
const handleView = (row: any) => {
|
||||
emits('viewDoc', row.id);
|
||||
};
|
||||
const handleDownload = (row: any) => {
|
||||
window.open(row.documentPath, '_blank');
|
||||
const handleDownload = async (row: any) => {
|
||||
const blob = await api.download(
|
||||
`/api/v1/document/download?documentId=${row.id}`,
|
||||
);
|
||||
downloadFileFromBlob({
|
||||
fileName: row.title || 'document',
|
||||
source: blob,
|
||||
});
|
||||
};
|
||||
const handleDelete = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
@@ -122,7 +137,10 @@ const handleDelete = (row: any) => {
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem @click="handleDelete(row)">
|
||||
<ElDropdownItem
|
||||
v-if="props.manageable"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<ElButton link :icon="Delete" type="danger">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
ElDropdown,
|
||||
ElDropdownItem,
|
||||
ElDropdownMenu,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElInput,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTree,
|
||||
@@ -43,6 +43,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const pageDataRef = ref();
|
||||
@@ -143,6 +147,10 @@ const handleResetSearch = () => {
|
||||
};
|
||||
|
||||
const openAddDialog = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
editData.value = {
|
||||
collectionId: props.knowledgeId,
|
||||
categoryId:
|
||||
@@ -189,12 +197,17 @@ const exportFaqExcel = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportSuccess = () => {
|
||||
const handleImportSuccess = async () => {
|
||||
await reloadCategoryTree();
|
||||
refreshList();
|
||||
};
|
||||
|
||||
const handleMoreActionCommand = (command: string) => {
|
||||
if (command === 'import') {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
importDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
@@ -208,6 +221,10 @@ const handleMoreActionCommand = (command: string) => {
|
||||
};
|
||||
|
||||
const openEditDialog = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
editData.value = {
|
||||
id: row.id,
|
||||
collectionId: row.collectionId,
|
||||
@@ -223,6 +240,10 @@ const openEditDialog = (row: any) => {
|
||||
};
|
||||
|
||||
const saveFaq = async (payload: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const url = payload.id ? '/api/v1/faqItem/update' : '/api/v1/faqItem/save';
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode === 0) {
|
||||
@@ -237,6 +258,10 @@ const saveFaq = async (payload: any) => {
|
||||
};
|
||||
|
||||
const removeFaq = (row: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
@@ -259,6 +284,10 @@ const handleCategoryClick = (data: any) => {
|
||||
};
|
||||
|
||||
const openAddRootCategory = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.addCategory');
|
||||
categoryDialogDisableParent.value = false;
|
||||
categoryEditData.value = {
|
||||
@@ -271,6 +300,10 @@ const openAddRootCategory = () => {
|
||||
};
|
||||
|
||||
const openAddSiblingCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.addSiblingCategory');
|
||||
categoryDialogDisableParent.value = false;
|
||||
categoryEditData.value = {
|
||||
@@ -286,6 +319,10 @@ const openAddSiblingCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const openAddChildCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
if (node.isDefault) {
|
||||
ElMessage.warning(
|
||||
$t('documentCollection.faq.defaultCategoryChildForbidden'),
|
||||
@@ -308,6 +345,10 @@ const openAddChildCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const openEditCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
categoryDialogTitle.value = $t('documentCollection.faq.editCategory');
|
||||
categoryDialogDisableParent.value = !!node.isDefault;
|
||||
categoryEditData.value = {
|
||||
@@ -324,6 +365,10 @@ const openEditCategory = (node: any) => {
|
||||
};
|
||||
|
||||
const removeCategory = (node: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
if (node.isDefault) {
|
||||
ElMessage.warning(
|
||||
$t('documentCollection.faq.defaultCategoryDeleteForbidden'),
|
||||
@@ -612,6 +657,10 @@ const demoteCategory = async (node: any) => {
|
||||
};
|
||||
|
||||
const saveCategory = async (payload: any) => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const url = payload.id
|
||||
? '/api/v1/faqCategory/update'
|
||||
: '/api/v1/faqCategory/save';
|
||||
@@ -692,6 +741,7 @@ onMounted(() => {
|
||||
<div class="faq-category-header">
|
||||
<span>{{ $t('documentCollection.faq.categoryTree') }}</span>
|
||||
<ElButton
|
||||
v-if="props.manageable"
|
||||
link
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@@ -719,7 +769,7 @@ onMounted(() => {
|
||||
data.categoryName
|
||||
}}</span>
|
||||
<div
|
||||
v-if="!data.isVirtual && !data.isDefault"
|
||||
v-if="props.manageable && !data.isVirtual && !data.isDefault"
|
||||
class="faq-category-node-actions"
|
||||
@click.stop
|
||||
>
|
||||
@@ -770,7 +820,10 @@ onMounted(() => {
|
||||
>
|
||||
{{ $t('documentCollection.faq.addChildCategory') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem :icon="Edit" @click="openEditCategory(data)">
|
||||
<ElDropdownItem
|
||||
:icon="Edit"
|
||||
@click="openEditCategory(data)"
|
||||
>
|
||||
{{ $t('button.edit') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
@@ -791,6 +844,9 @@ onMounted(() => {
|
||||
|
||||
<div class="faq-content-pane">
|
||||
<div class="faq-header">
|
||||
<div v-if="!props.manageable" class="faq-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
<div class="faq-toolbar">
|
||||
<div class="faq-search-actions">
|
||||
<ElInput
|
||||
@@ -809,19 +865,25 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="faq-primary-actions">
|
||||
<ElButton type="primary" :icon="Plus" @click="openAddDialog">
|
||||
<ElButton
|
||||
v-if="props.manageable"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openAddDialog"
|
||||
>
|
||||
{{ $t('button.add') }}
|
||||
</ElButton>
|
||||
<ElDropdown
|
||||
trigger="click"
|
||||
@command="handleMoreActionCommand"
|
||||
>
|
||||
<ElDropdown trigger="click" @command="handleMoreActionCommand">
|
||||
<ElButton :icon="MoreFilled">
|
||||
{{ $t('documentCollection.faq.import.moreActions') }}
|
||||
</ElButton>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem command="import" :icon="Upload">
|
||||
<ElDropdownItem
|
||||
v-if="props.manageable"
|
||||
command="import"
|
||||
:icon="Upload"
|
||||
>
|
||||
{{ $t('button.import') }}
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem
|
||||
@@ -871,6 +933,7 @@ onMounted(() => {
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn
|
||||
v-if="props.manageable"
|
||||
:label="$t('common.handle')"
|
||||
width="170"
|
||||
align="right"
|
||||
@@ -1002,6 +1065,13 @@ onMounted(() => {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.faq-readonly-tip {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.faq-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1062,8 +1132,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
:deep(
|
||||
.faq-category-tree > .el-tree-node > .el-tree-node__content > .el-tree-node__expand-icon
|
||||
) {
|
||||
.faq-category-tree
|
||||
> .el-tree-node
|
||||
> .el-tree-node__content
|
||||
> .el-tree-node__expand-icon
|
||||
) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1077,7 +1150,9 @@ onMounted(() => {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
:deep(.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions) {
|
||||
:deep(
|
||||
.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions
|
||||
) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1086,7 +1161,9 @@ onMounted(() => {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions) {
|
||||
:deep(
|
||||
.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions
|
||||
) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, reactive, ref} from 'vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {$t} from '@easyflow/locales';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {InfoFilled} from '@element-plus/icons-vue';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElForm,
|
||||
@@ -16,13 +16,17 @@ import {
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
|
||||
import {api} from '#/api/request';
|
||||
import { api } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
documentCollectionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
manageable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
@@ -55,6 +59,10 @@ const searchConfig = reactive({
|
||||
});
|
||||
|
||||
const submitConfig = () => {
|
||||
if (!props.manageable) {
|
||||
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||
return;
|
||||
}
|
||||
const submitData = {
|
||||
id: props.documentCollectionId,
|
||||
options: {
|
||||
@@ -89,6 +97,9 @@ const searchEngineOptions = [
|
||||
},
|
||||
];
|
||||
const handleSearchEngineEnableChange = () => {
|
||||
if (!props.manageable) {
|
||||
return;
|
||||
}
|
||||
api.post('/api/v1/documentCollection/update', {
|
||||
id: props.documentCollectionId,
|
||||
searchEngineEnable: searchEngineEnable.value,
|
||||
@@ -100,11 +111,15 @@ const handleSearchEngineEnableChange = () => {
|
||||
<div class="search-config-sidebar">
|
||||
<div class="config-header">
|
||||
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
|
||||
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||
{{ $t('documentCollection.managePermissionHint') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElForm
|
||||
class="config-form"
|
||||
:model="searchConfig"
|
||||
:disabled="!props.manageable"
|
||||
label-width="100%"
|
||||
size="small"
|
||||
>
|
||||
@@ -227,7 +242,12 @@ const handleSearchEngineEnableChange = () => {
|
||||
</ElForm>
|
||||
|
||||
<div class="config-footer">
|
||||
<ElButton type="primary" @click="submitConfig" class="submit-btn">
|
||||
<ElButton
|
||||
type="primary"
|
||||
@click="submitConfig"
|
||||
class="submit-btn"
|
||||
:disabled="!props.manageable"
|
||||
>
|
||||
{{ $t('documentCollectionSearch.button.save') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
@@ -256,6 +276,13 @@ const handleSearchEngineEnableChange = () => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-readonly-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.config-form {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type {FormInstance} from 'element-plus';
|
||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElIcon,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElPopover,
|
||||
} from 'element-plus';
|
||||
|
||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
|
||||
import {computed, markRaw, onMounted, ref} from 'vue';
|
||||
|
||||
import {useAccess} from '@easyflow/access';
|
||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
Check,
|
||||
CopyDocument,
|
||||
Delete,
|
||||
Download,
|
||||
Edit,
|
||||
Lock,
|
||||
OfficeBuilding,
|
||||
Plus,
|
||||
Promotion,
|
||||
Tickets,
|
||||
Upload,
|
||||
VideoPlay,
|
||||
@@ -47,6 +61,8 @@ interface FieldDefinition {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
|
||||
|
||||
const primaryAction: CardPrimaryAction = {
|
||||
icon: DesignIcon,
|
||||
text: $t('button.design'),
|
||||
@@ -56,6 +72,39 @@ const primaryAction: CardPrimaryAction = {
|
||||
},
|
||||
};
|
||||
|
||||
const {hasAccessByCodes} = useAccess();
|
||||
const canManageWorkflow = computed(() =>
|
||||
hasAccessByCodes(['/api/v1/workflow/save']),
|
||||
);
|
||||
const updatingScopeId = ref<string | number | null>(null);
|
||||
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
|
||||
const visibilityScopeMeta = computed(() => ({
|
||||
PRIVATE: {
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
description: $t('aiWorkflow.visibilityScopePrivateDesc'),
|
||||
icon: Lock,
|
||||
tone: 'private',
|
||||
},
|
||||
DEPT: {
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
description: $t('aiWorkflow.visibilityScopeDeptDesc'),
|
||||
icon: OfficeBuilding,
|
||||
tone: 'dept',
|
||||
},
|
||||
PUBLIC: {
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
description: $t('aiWorkflow.visibilityScopePublicDesc'),
|
||||
icon: Promotion,
|
||||
tone: 'public',
|
||||
},
|
||||
}));
|
||||
const visibilityScopeOptions = computed(() => {
|
||||
return (['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
|
||||
value,
|
||||
...visibilityScopeMeta.value[value],
|
||||
}));
|
||||
});
|
||||
|
||||
const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Edit,
|
||||
@@ -96,6 +145,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Download,
|
||||
text: $t('button.export'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
exportJson(row);
|
||||
@@ -104,6 +154,7 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: CopyDocument,
|
||||
text: $t('button.copy'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
showDialog({
|
||||
@@ -151,6 +202,50 @@ const headerButtons = [
|
||||
function initDict() {
|
||||
dictStore.fetchDictionary('dataStatus');
|
||||
}
|
||||
function resolveVisibilityScopeMeta(scope?: string) {
|
||||
return visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
|
||||
visibilityScopeMeta.value.PRIVATE;
|
||||
}
|
||||
function setVisibilityScopePopoverRef(id: string | number, el: any) {
|
||||
const cacheKey = String(id);
|
||||
if (el) {
|
||||
visibilityScopePopoverRefs.value[cacheKey] = el;
|
||||
return;
|
||||
}
|
||||
delete visibilityScopePopoverRefs.value[cacheKey];
|
||||
}
|
||||
function closeVisibilityScopePopover(id: string | number) {
|
||||
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
|
||||
}
|
||||
async function updateVisibilityScope(
|
||||
row: any,
|
||||
visibilityScope: VisibilityScope,
|
||||
) {
|
||||
if (
|
||||
!canManageWorkflow.value ||
|
||||
!row?.id ||
|
||||
updatingScopeId.value === row.id ||
|
||||
row.visibilityScope === visibilityScope
|
||||
) {
|
||||
closeVisibilityScopePopover(row.id);
|
||||
return;
|
||||
}
|
||||
updatingScopeId.value = row.id;
|
||||
try {
|
||||
const res = await api.post('/api/v1/workflow/update', {
|
||||
id: row.id,
|
||||
visibilityScope,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
row.visibilityScope = visibilityScope;
|
||||
ElMessage.success(res.message);
|
||||
closeVisibilityScopePopover(row.id);
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
} finally {
|
||||
updatingScopeId.value = null;
|
||||
}
|
||||
}
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
};
|
||||
@@ -341,7 +436,7 @@ function handleSubmit() {
|
||||
});
|
||||
}
|
||||
const getSideList = async () => {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
|
||||
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/visibleList', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
});
|
||||
|
||||
@@ -394,7 +489,90 @@ function handleHeaderButtonClick(data: any) {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageWorkflow"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="workflow-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="workflow-scope-chip"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="workflow-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="workflow-scope-option"
|
||||
:class="[
|
||||
`workflow-scope-option--${option.tone}`,
|
||||
{
|
||||
'workflow-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="workflow-scope-option__leading">
|
||||
<span class="workflow-scope-option__icon-wrap">
|
||||
<ElIcon class="workflow-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="workflow-scope-option__text">
|
||||
<span class="workflow-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="workflow-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="workflow-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="workflow-scope-chip workflow-scope-chip--readonly"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
@@ -440,4 +618,205 @@ function handleHeaderButtonClick(data: any) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.workflow-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 0.92);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
button.workflow-scope-chip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 4px hsl(var(--primary) / 0.12),
|
||||
0 10px 22px -18px hsl(var(--foreground) / 0.32);
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.09);
|
||||
border-color: hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
border-color: hsl(var(--warning) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
border-color: hsl(var(--success) / 0.2);
|
||||
}
|
||||
|
||||
.workflow-scope-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.workflow-scope-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
color 0.18s ease;
|
||||
}
|
||||
|
||||
.workflow-scope-option:hover {
|
||||
background: hsl(var(--foreground) / 0.04);
|
||||
}
|
||||
|
||||
.workflow-scope-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.workflow-scope-option__leading {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.workflow-scope-option__icon {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workflow-scope-option__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--text-strong));
|
||||
}
|
||||
|
||||
.workflow-scope-option__desc {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: hsl(var(--text-muted));
|
||||
}
|
||||
|
||||
.workflow-scope-option__check {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__icon-wrap {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 0.12);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private.workflow-scope-option--active {
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept.workflow-scope-option--active {
|
||||
background: hsl(var(--warning) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--public.workflow-scope-option--active {
|
||||
background: hsl(var(--success) / 0.08);
|
||||
}
|
||||
|
||||
.workflow-scope-option--private .workflow-scope-option__check {
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.workflow-scope-option--dept .workflow-scope-option__check {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.workflow-scope-option--public .workflow-scope-option__check {
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
:global(.workflow-visibility-popover.el-popover.el-popper) {
|
||||
padding: 8px;
|
||||
border-radius: 16px;
|
||||
border-color: hsl(var(--line-subtle));
|
||||
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElForm, ElFormItem, ElInput, ElMessage, ElUpload } from 'element-plus';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElUpload,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
@@ -28,7 +36,7 @@ const isImport = ref(false);
|
||||
const jsonFile = ref<any>(null);
|
||||
const uploadFileList = ref<any[]>([]);
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const entity = ref<any>({
|
||||
const createDefaultEntity = () => ({
|
||||
alias: '',
|
||||
deptId: '',
|
||||
title: '',
|
||||
@@ -36,8 +44,24 @@ const entity = ref<any>({
|
||||
icon: '',
|
||||
content: '',
|
||||
englishName: '',
|
||||
visibilityScope: 'PRIVATE',
|
||||
});
|
||||
const entity = ref<any>(createDefaultEntity());
|
||||
const btnLoading = ref(false);
|
||||
const visibilityScopeOptions = computed(() => [
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePrivate'),
|
||||
value: 'PRIVATE',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopeDept'),
|
||||
value: 'DEPT',
|
||||
},
|
||||
{
|
||||
label: $t('aiWorkflow.visibilityScopePublic'),
|
||||
value: 'PUBLIC',
|
||||
},
|
||||
]);
|
||||
const jsonFileModel = computed({
|
||||
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
||||
set: (value: any) => {
|
||||
@@ -57,10 +81,11 @@ const rules = computed(() => ({
|
||||
// functions
|
||||
function openDialog(row: any, importMode = false) {
|
||||
isImport.value = importMode;
|
||||
if (row.id) {
|
||||
isAdd.value = false;
|
||||
}
|
||||
entity.value = row;
|
||||
isAdd.value = !row?.id;
|
||||
entity.value = {
|
||||
...createDefaultEntity(),
|
||||
...(row || {}),
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
@@ -137,7 +162,7 @@ function closeDialog() {
|
||||
jsonFile.value = null;
|
||||
isAdd.value = true;
|
||||
isImport.value = false;
|
||||
entity.value = {};
|
||||
entity.value = createDefaultEntity();
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
</script>
|
||||
@@ -198,6 +223,19 @@ function closeDialog() {
|
||||
dict-code="aiWorkFlowCategory"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="visibilityScope"
|
||||
:label="$t('aiWorkflow.visibilityScope')"
|
||||
>
|
||||
<ElSelect v-model="entity.visibilityScope" class="w-full">
|
||||
<ElOption
|
||||
v-for="item in visibilityScopeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
|
||||
<ElInput v-model.trim="entity.alias" />
|
||||
</ElFormItem>
|
||||
|
||||
Reference in New Issue
Block a user