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 org.springframework.web.bind.annotation.PostMapping;
|
||||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
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.BotDocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +34,13 @@ import java.util.List;
|
|||||||
@RequestMapping("/api/v1/botKnowledge")
|
@RequestMapping("/api/v1/botKnowledge")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
|
public class BotDocumentCollectionController extends BaseCurdController<BotDocumentCollectionService, BotDocumentCollection> {
|
||||||
|
@Resource
|
||||||
|
private DocumentCollectionService documentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
public BotDocumentCollectionController(BotDocumentCollectionService service) {
|
public BotDocumentCollectionController(BotDocumentCollectionService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -35,12 +51,32 @@ public class BotDocumentCollectionController extends BaseCurdController<BotDocum
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
List<BotDocumentCollection> botDocumentCollections = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
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")
|
@PostMapping("updateBotKnowledgeIds")
|
||||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
|
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);
|
service.saveBotAndKnowledge(botId, knowledgeIds);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import tech.easyflow.ai.entity.BotWorkflow;
|
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.BotWorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.tree.Tree;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 控制层。
|
* 控制层。
|
||||||
*
|
*
|
||||||
@@ -26,6 +36,13 @@ import java.util.List;
|
|||||||
@RequestMapping("/api/v1/botWorkflow")
|
@RequestMapping("/api/v1/botWorkflow")
|
||||||
@UsePermission(moduleName = "/api/v1/bot")
|
@UsePermission(moduleName = "/api/v1/bot")
|
||||||
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
|
public class BotWorkflowController extends BaseCurdController<BotWorkflowService, BotWorkflow> {
|
||||||
|
@Resource
|
||||||
|
private WorkflowService workflowService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
public BotWorkflowController(BotWorkflowService service) {
|
public BotWorkflowController(BotWorkflowService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
@@ -36,13 +53,33 @@ public class BotWorkflowController extends BaseCurdController<BotWorkflowService
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
List<BotWorkflow> botWorkflows = service.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
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);
|
return Result.ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("updateBotWorkflowIds")
|
@PostMapping("updateBotWorkflowIds")
|
||||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("workflowIds") BigInteger [] workflowIds) {
|
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);
|
service.saveBotAndWorkflowTool(botId, workflowIds);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.easyagents.core.model.embedding.EmbeddingModel;
|
import com.easyagents.core.model.embedding.EmbeddingModel;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import tech.easyflow.ai.entity.DocumentChunk;
|
import tech.easyflow.ai.entity.DocumentChunk;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.Model;
|
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.DocumentStore;
|
||||||
import com.easyagents.core.store.StoreOptions;
|
import com.easyagents.core.store.StoreOptions;
|
||||||
import com.easyagents.core.store.StoreResult;
|
import com.easyagents.core.store.StoreResult;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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 javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -51,8 +58,29 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
super(service);
|
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")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@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) {
|
public Result<?> update(@JsonBody DocumentChunk documentChunk) {
|
||||||
boolean success = service.updateById(documentChunk);
|
boolean success = service.updateById(documentChunk);
|
||||||
if (success){
|
if (success){
|
||||||
@@ -87,6 +115,13 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
|
|
||||||
@PostMapping("removeChunk")
|
@PostMapping("removeChunk")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@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) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) BigInteger chunkId) {
|
||||||
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
DocumentChunk docChunk = documentChunkService.getById(chunkId);
|
||||||
if (docChunk == null) {
|
if (docChunk == null) {
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import com.easyagents.core.document.Document;
|
import com.easyagents.core.document.Document;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.util.StringUtils;
|
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.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.BotDocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
import tech.easyflow.ai.service.DocumentChunkService;
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
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.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
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 javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -41,6 +53,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BotDocumentCollectionService botDocumentCollectionService;
|
private BotDocumentCollectionService botDocumentCollectionService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
|
||||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -50,6 +66,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onSaveOrUpdateBefore(DocumentCollection entity, boolean isSave) {
|
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 alias = entity.getAlias();
|
||||||
String collectionType = entity.getCollectionType();
|
String collectionType = entity.getCollectionType();
|
||||||
@@ -96,6 +117,13 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@GetMapping("search")
|
@GetMapping("search")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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) {
|
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
|
||||||
return Result.ok(service.search(knowledgeId, keyword));
|
return Result.ok(service.search(knowledgeId, keyword));
|
||||||
}
|
}
|
||||||
@@ -103,6 +131,10 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<Void> onRemoveBefore(Collection<Serializable> ids) {
|
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 queryWrapper = QueryWrapper.create();
|
||||||
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
queryWrapper.in(BotDocumentCollection::getDocumentCollectionId, ids);
|
||||||
@@ -116,7 +148,90 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID_OR_SLUG,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问知识库"
|
||||||
|
)
|
||||||
public Result<DocumentCollection> detail(String id) {
|
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;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||||
import tech.easyflow.ai.entity.Document;
|
import tech.easyflow.ai.entity.Document;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.DocumentCollectionSplitParams;
|
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.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -58,6 +71,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisLockExecutor redisLockExecutor;
|
private RedisLockExecutor redisLockExecutor;
|
||||||
|
|
||||||
|
@Resource(name = "default")
|
||||||
|
private FileStorageService storageService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
@Value("${easyflow.storage.local.root:}")
|
@Value("${easyflow.storage.local.root:}")
|
||||||
private String fileUploadPath;
|
private String fileUploadPath;
|
||||||
@@ -73,6 +91,8 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Transactional
|
@Transactional
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) String id) {
|
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);
|
List<Serializable> ids = Collections.singletonList(id);
|
||||||
Result<?> result = onRemoveBefore(ids);
|
Result<?> result = onRemoveBefore(ids);
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
@@ -104,7 +124,7 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
throw new BusinessException("知识库id不能为空");
|
throw new BusinessException("知识库id不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentCollection knowledge = getDocumentCollection(kbSlug);
|
DocumentCollection knowledge = getDocumentCollection(kbSlug, ResourceAction.READ, "无权限访问知识库");
|
||||||
|
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
.eq(Document::getCollectionId, knowledge.getId());
|
.eq(Document::getCollectionId, knowledge.getId());
|
||||||
@@ -121,11 +141,33 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
if (StringUtil.noText(kbSlug)) {
|
if (StringUtil.noText(kbSlug)) {
|
||||||
throw new BusinessException("知识库id不能为空");
|
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);
|
Page<Document> documentList = documentService.getDocumentList(knowledge.getId().toString(), pageSize, pageNumber,fileName);
|
||||||
return Result.ok(documentList);
|
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
|
@Override
|
||||||
protected String getDefaultOrderBy() {
|
protected String getDefaultOrderBy() {
|
||||||
@@ -138,6 +180,11 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
@Transactional
|
@Transactional
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
public Result<Boolean> update(@JsonBody Document entity) {
|
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);
|
super.update(entity);
|
||||||
return Result.ok(updatePosition(entity));
|
return Result.ok(updatePosition(entity));
|
||||||
}
|
}
|
||||||
@@ -152,10 +199,40 @@ public class DocumentController extends BaseCurdController<DocumentService, Docu
|
|||||||
if (documentCollectionSplitParams.getKnowledgeId() == null) {
|
if (documentCollectionSplitParams.getKnowledgeId() == null) {
|
||||||
throw new BusinessException("知识库id不能为空");
|
throw new BusinessException("知识库id不能为空");
|
||||||
}
|
}
|
||||||
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString());
|
getDocumentCollection(documentCollectionSplitParams.getKnowledgeId().toString(), ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
return documentService.textSplit(documentCollectionSplitParams);
|
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
|
* 更新 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)
|
DocumentCollection knowledge = StringUtil.isNumeric(idOrSlug)
|
||||||
? knowledgeService.getById(idOrSlug)
|
? knowledgeService.getById(idOrSlug)
|
||||||
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
|
: knowledgeService.getOne(QueryWrapper.create().eq(DocumentCollection::getSlug, idOrSlug));
|
||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
throw new BusinessException("知识库不存在");
|
throw new BusinessException("知识库不存在");
|
||||||
}
|
}
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, action, denyMessage);
|
||||||
if (knowledge.isFaqCollection()) {
|
if (knowledge.isFaqCollection()) {
|
||||||
throw new BusinessException("FAQ知识库不支持文档操作");
|
throw new BusinessException("FAQ知识库不支持文档操作");
|
||||||
}
|
}
|
||||||
return knowledge;
|
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.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -29,6 +33,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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) {
|
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
@@ -40,6 +51,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/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) {
|
public Result<?> save(@JsonBody FaqCategory entity) {
|
||||||
return Result.ok(service.saveCategory(entity));
|
return Result.ok(service.saveCategory(entity));
|
||||||
}
|
}
|
||||||
@@ -47,6 +65,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("update")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@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) {
|
public Result<?> update(@JsonBody FaqCategory entity) {
|
||||||
return Result.ok(service.updateCategory(entity));
|
return Result.ok(service.updateCategory(entity));
|
||||||
}
|
}
|
||||||
@@ -54,6 +79,13 @@ public class FaqCategoryController extends BaseCurdController<FaqCategoryService
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/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) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(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.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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 javax.annotation.Resource;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
@@ -67,13 +71,31 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("list")
|
@GetMapping("list")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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) {
|
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);
|
return super.list(entity, asTree, sortKey, sortType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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) {
|
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||||
if (pageNumber == null || pageNumber < 1) {
|
if (pageNumber == null || pageNumber < 1) {
|
||||||
pageNumber = 1L;
|
pageNumber = 1L;
|
||||||
@@ -123,6 +145,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@GetMapping("detail")
|
@GetMapping("detail")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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) {
|
public Result<FaqItem> detail(String id) {
|
||||||
return super.detail(id);
|
return super.detail(id);
|
||||||
}
|
}
|
||||||
@@ -130,6 +159,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/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) {
|
public Result<?> save(@JsonBody FaqItem entity) {
|
||||||
return Result.ok(service.saveFaqItem(entity));
|
return Result.ok(service.saveFaqItem(entity));
|
||||||
}
|
}
|
||||||
@@ -137,6 +173,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("update")
|
@PostMapping("update")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@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) {
|
public Result<?> update(@JsonBody FaqItem entity) {
|
||||||
return Result.ok(service.updateFaqItem(entity));
|
return Result.ok(service.updateFaqItem(entity));
|
||||||
}
|
}
|
||||||
@@ -144,12 +187,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/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) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
|
return Result.ok(service.removeFaqItem(new java.math.BigInteger(String.valueOf(id))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
@PostMapping(value = "uploadImage", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@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) {
|
public Result<UploadResVo> uploadImage(MultipartFile file, BigInteger collectionId) {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
@@ -180,12 +237,26 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
|
|
||||||
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@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) {
|
public Result<FaqImportResultVo> importExcel(MultipartFile file, BigInteger collectionId) {
|
||||||
return Result.ok(service.importFromExcel(collectionId, file));
|
return Result.ok(service.importFromExcel(collectionId, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("downloadImportTemplate")
|
@GetMapping("downloadImportTemplate")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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 {
|
public void downloadImportTemplate(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
@@ -206,6 +277,13 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
|
|||||||
|
|
||||||
@GetMapping("exportExcel")
|
@GetMapping("exportExcel")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/query")
|
@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 {
|
public void exportExcel(BigInteger collectionId, HttpServletResponse response) throws Exception {
|
||||||
if (collectionId == null) {
|
if (collectionId == null) {
|
||||||
throw new BusinessException("知识库ID不能为空");
|
throw new BusinessException("知识库ID不能为空");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
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.ChainDefinition;
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
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.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
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.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
|
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.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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 tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -67,6 +75,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
|
||||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -78,6 +90,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/singleRun")
|
@PostMapping("/singleRun")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.USE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#workflowId",
|
||||||
|
denyMessage = "无权限运行工作流"
|
||||||
|
)
|
||||||
public Result<?> singleRun(
|
public Result<?> singleRun(
|
||||||
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
@JsonBody(value = "workflowId", required = true) BigInteger workflowId,
|
||||||
@JsonBody(value = "nodeId", required = true) String nodeId,
|
@JsonBody(value = "nodeId", required = true) String nodeId,
|
||||||
@@ -96,6 +115,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@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,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables) {
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
@@ -117,6 +143,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
* 获取工作流运行状态 - v2
|
* 获取工作流运行状态 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@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,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes) {
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
@@ -128,6 +161,13 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@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,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
@@ -137,6 +177,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@PostMapping("/importWorkFlow")
|
@PostMapping("/importWorkFlow")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
public Result<Void> importWorkFlow(Workflow workflow, MultipartFile jsonFile) throws Exception {
|
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();
|
InputStream is = jsonFile.getInputStream();
|
||||||
String content = IoUtil.read(is, StandardCharsets.UTF_8);
|
String content = IoUtil.read(is, StandardCharsets.UTF_8);
|
||||||
workflow.setContent(content);
|
workflow.setContent(content);
|
||||||
@@ -147,13 +191,30 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
|
|
||||||
@GetMapping("/exportWorkFlow")
|
@GetMapping("/exportWorkFlow")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@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) {
|
public Result<String> exportWorkFlow(BigInteger id) {
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
return Result.ok("", workflow.getContent());
|
return Result.ok("", workflow.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
@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) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
||||||
Workflow workflow = service.getById(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,
|
public Result<WorkflowCheckResult> check(@JsonBody("id") BigInteger id,
|
||||||
@JsonBody("content") String content,
|
@JsonBody("content") String content,
|
||||||
@JsonBody(value = "stage", required = true) String stage) {
|
@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);
|
WorkflowCheckStage checkStage = WorkflowCheckStage.from(stage);
|
||||||
WorkflowCheckResult checkResult;
|
WorkflowCheckResult checkResult;
|
||||||
if (StringUtils.hasLength(content)) {
|
if (StringUtils.hasLength(content)) {
|
||||||
@@ -199,6 +264,14 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@GetMapping("detail")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.READ,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限访问工作流"
|
||||||
|
)
|
||||||
public Result<Workflow> detail(String id) {
|
public Result<Workflow> detail(String id) {
|
||||||
Workflow workflow = service.getDetail(id);
|
Workflow workflow = service.getDetail(id);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
@@ -206,9 +279,19 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
|
|
||||||
@GetMapping("/copy")
|
@GetMapping("/copy")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@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) {
|
public Result<Void> copy(BigInteger id) {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
Workflow workflow = service.getById(id);
|
Workflow workflow = service.getById(id);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
workflow.setId(null);
|
workflow.setId(null);
|
||||||
workflow.setAlias(IdUtil.fastSimpleUUID());
|
workflow.setAlias(IdUtil.fastSimpleUUID());
|
||||||
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
commonFiled(workflow, account.getId(), account.getTenantId(), account.getDeptId());
|
||||||
@@ -218,6 +301,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result onSaveOrUpdateBefore(Workflow entity, boolean isSave) {
|
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())) {
|
if (StringUtils.hasLength(entity.getContent())) {
|
||||||
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
workflowCheckService.checkOrThrow(entity.getContent(), WorkflowCheckStage.SAVE, entity.getId());
|
||||||
}
|
}
|
||||||
@@ -241,8 +329,26 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
return null;
|
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
|
@Override
|
||||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
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 queryWrapper = QueryWrapper.create();
|
||||||
queryWrapper.in("workflow_id", ids);
|
queryWrapper.in("workflow_id", ids);
|
||||||
boolean exists = botWorkflowService.exists(queryWrapper);
|
boolean exists = botWorkflowService.exists(queryWrapper);
|
||||||
@@ -251,4 +357,25 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
}
|
}
|
||||||
return null;
|
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.PropertiesUtil;
|
||||||
import tech.easyflow.common.util.StringUtil;
|
import tech.easyflow.common.util.StringUtil;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -32,7 +33,7 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@Table("tb_document_collection")
|
@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_DOCUMENT = "DOCUMENT";
|
||||||
public static final String TYPE_FAQ = "FAQ";
|
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_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() {
|
public DocumentStore toDocumentStore() {
|
||||||
String storeType = this.getVectorStoreType();
|
String storeType = this.getVectorStoreType();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.easyagents.core.model.chat.tool.Tool;
|
|||||||
import com.mybatisflex.annotation.Table;
|
import com.mybatisflex.annotation.Table;
|
||||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
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")
|
@Table("tb_workflow")
|
||||||
public class Workflow extends WorkflowBase {
|
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||||
|
|
||||||
public Tool toFunction(boolean needEnglishName) {
|
public Tool toFunction(boolean needEnglishName) {
|
||||||
return new WorkflowTool(this, 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.Column;
|
||||||
import com.mybatisflex.annotation.Id;
|
import com.mybatisflex.annotation.Id;
|
||||||
import com.mybatisflex.annotation.KeyType;
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
public class DocumentChunkBase implements Serializable {
|
public class DocumentChunkBase implements Serializable {
|
||||||
@@ -38,6 +40,12 @@ public class DocumentChunkBase implements Serializable {
|
|||||||
@Column(comment = "分割顺序")
|
@Column(comment = "分割顺序")
|
||||||
private Integer sorting;
|
private Integer sorting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展元信息
|
||||||
|
*/
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class, comment = "扩展元信息")
|
||||||
|
private Map<String, Object> options;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable {
|
|||||||
this.sorting = sorting;
|
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")
|
@Column(comment = "分类ID")
|
||||||
private BigInteger categoryId;
|
private BigInteger categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可见范围
|
||||||
|
*/
|
||||||
|
@Column(comment = "可见范围")
|
||||||
|
private String visibilityScope;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
|
|||||||
this.categoryId = categoryId;
|
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")
|
@Column(comment = "分类ID")
|
||||||
private BigInteger categoryId;
|
private BigInteger categoryId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可见范围
|
||||||
|
*/
|
||||||
|
@Column(comment = "可见范围")
|
||||||
|
private String visibilityScope;
|
||||||
|
|
||||||
public BigInteger getId() {
|
public BigInteger getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
|
|||||||
this.categoryId = categoryId;
|
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> listInvokeModels();
|
||||||
|
|
||||||
|
List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType);
|
||||||
|
|
||||||
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
|
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
|
||||||
|
|
||||||
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, 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.model.rerank.RerankModel;
|
||||||
import com.easyagents.core.store.VectorData;
|
import com.easyagents.core.store.VectorData;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import com.mybatisflex.core.util.StringUtil;
|
||||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -22,6 +23,9 @@ import tech.easyflow.ai.entity.ModelProvider;
|
|||||||
import tech.easyflow.ai.mapper.ModelMapper;
|
import tech.easyflow.ai.mapper.ModelMapper;
|
||||||
import tech.easyflow.ai.service.ModelProviderService;
|
import tech.easyflow.ai.service.ModelProviderService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
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 tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -249,6 +253,18 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
|||||||
.collect(Collectors.toList());
|
.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
|
@Override
|
||||||
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
|
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
|
||||||
Model existing = getModelInstance(id);
|
Model existing = getModelInstance(id);
|
||||||
@@ -281,6 +297,21 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
|||||||
return updatedModels;
|
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) {
|
private String buildDefaultInvokeCode(String modelName) {
|
||||||
if (StrUtil.isBlank(modelName)) {
|
if (StrUtil.isBlank(modelName)) {
|
||||||
return null;
|
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 tech.easyflow.system.entity.SysDept;
|
||||||
import com.mybatisflex.core.service.IService;
|
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> {
|
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 com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||||
import org.springframework.stereotype.Service;
|
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
|
@Service
|
||||||
public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDept> implements SysDeptService {
|
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;
|
titleField?: string;
|
||||||
descField?: string;
|
descField?: string;
|
||||||
actions?: ActionButton[];
|
actions?: ActionButton[];
|
||||||
|
cornerTagField?: string;
|
||||||
|
cornerTagMap?: Record<string, string>;
|
||||||
|
cornerTagTypeMap?: Record<string, string>;
|
||||||
defaultIcon: any;
|
defaultIcon: any;
|
||||||
data: any[];
|
data: any[];
|
||||||
primaryAction?: CardPrimaryAction;
|
primaryAction?: CardPrimaryAction;
|
||||||
@@ -58,6 +61,9 @@ const props = withDefaults(defineProps<CardListProps>(), {
|
|||||||
titleField: 'title',
|
titleField: 'title',
|
||||||
descField: 'description',
|
descField: 'description',
|
||||||
actions: () => [],
|
actions: () => [],
|
||||||
|
cornerTagField: '',
|
||||||
|
cornerTagMap: () => ({}),
|
||||||
|
cornerTagTypeMap: () => ({}),
|
||||||
primaryAction: undefined,
|
primaryAction: undefined,
|
||||||
tagField: '',
|
tagField: '',
|
||||||
tagMap: () => ({}),
|
tagMap: () => ({}),
|
||||||
@@ -154,6 +160,23 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
|||||||
{{ item[descField] }}
|
{{ item[descField] }}
|
||||||
</ElText>
|
</ElText>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -343,6 +366,26 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
|||||||
color: hsl(var(--text-muted));
|
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 {
|
.card-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
"englishName": "EnglishName",
|
"englishName": "EnglishName",
|
||||||
"status": "ShowInUserCenter",
|
"status": "ShowInUserCenter",
|
||||||
"categoryId": "Category",
|
"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",
|
"params": "Params",
|
||||||
"steps": "Steps",
|
"steps": "Steps",
|
||||||
"result": "Result",
|
"result": "Result",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
"collectionTypeDocument": "Document",
|
"collectionTypeDocument": "Document",
|
||||||
"collectionTypeFaq": "FAQ",
|
"collectionTypeFaq": "FAQ",
|
||||||
"description": "Description",
|
"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",
|
"slug": "Slug",
|
||||||
"vectorStoreEnable": "VectorStoreEnable",
|
"vectorStoreEnable": "VectorStoreEnable",
|
||||||
"vectorStoreType": "VectorStoreType",
|
"vectorStoreType": "VectorStoreType",
|
||||||
@@ -46,12 +53,24 @@
|
|||||||
},
|
},
|
||||||
"importDoc": {
|
"importDoc": {
|
||||||
"fileUpload": "File upload",
|
"fileUpload": "File upload",
|
||||||
"parameterSettings": "ParameterSettings",
|
"parameterSettings": "Parameter settings",
|
||||||
|
"strategyAnalysis": "Strategy analysis",
|
||||||
"segmentedPreview": "SegmentedPreview",
|
"segmentedPreview": "SegmentedPreview",
|
||||||
"confirmImport": "ConfirmImport",
|
"confirmImport": "ConfirmImport",
|
||||||
"fileName": "File Name",
|
"fileName": "File Name",
|
||||||
"progressUpload": "Progress of file upload",
|
"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": {
|
"splitterDoc": {
|
||||||
"fileType": "FileType",
|
"fileType": "FileType",
|
||||||
@@ -64,6 +83,12 @@
|
|||||||
"simpleTokenizeSplitter": "SimpleTokenizeSplitter",
|
"simpleTokenizeSplitter": "SimpleTokenizeSplitter",
|
||||||
"regexDocumentSplitter": "RegexDocumentSplitter",
|
"regexDocumentSplitter": "RegexDocumentSplitter",
|
||||||
"markdownHeaderSplitter": "MarkdownHeaderSplitter",
|
"markdownHeaderSplitter": "MarkdownHeaderSplitter",
|
||||||
|
"autoStrategy": "Auto recommendation",
|
||||||
|
"markdownSection": "Markdown headings",
|
||||||
|
"outlineSection": "Outline sections",
|
||||||
|
"qaPair": "Q&A pairs",
|
||||||
|
"paragraphLength": "Paragraph length",
|
||||||
|
"customRegex": "Custom regex",
|
||||||
"mdSplitterLevel": "MarkdownSplitterLevel",
|
"mdSplitterLevel": "MarkdownSplitterLevel",
|
||||||
"uploadStatus": "UploadStatus",
|
"uploadStatus": "UploadStatus",
|
||||||
"pendingUpload": "PendingUpload",
|
"pendingUpload": "PendingUpload",
|
||||||
@@ -139,5 +164,6 @@
|
|||||||
"tencentCloud": "tencentCloud",
|
"tencentCloud": "tencentCloud",
|
||||||
"vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model",
|
"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",
|
"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": "英文名称",
|
"englishName": "英文名称",
|
||||||
"status": "在用户中心显示",
|
"status": "在用户中心显示",
|
||||||
"categoryId": "分类",
|
"categoryId": "分类",
|
||||||
|
"visibilityScope": "可见范围",
|
||||||
|
"visibilityScopePrivate": "个人",
|
||||||
|
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||||
|
"visibilityScopeDept": "部门",
|
||||||
|
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||||
|
"visibilityScopePublic": "公开",
|
||||||
|
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||||
"params": "执行参数",
|
"params": "执行参数",
|
||||||
"steps": "执行步骤",
|
"steps": "执行步骤",
|
||||||
"result": "执行结果",
|
"result": "执行结果",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
"collectionTypeDocument": "文档",
|
"collectionTypeDocument": "文档",
|
||||||
"collectionTypeFaq": "FAQ",
|
"collectionTypeFaq": "FAQ",
|
||||||
"description": "描述",
|
"description": "描述",
|
||||||
|
"visibilityScope": "可见范围",
|
||||||
|
"visibilityScopePrivate": "个人",
|
||||||
|
"visibilityScopePrivateDesc": "仅创建者可访问",
|
||||||
|
"visibilityScopeDept": "部门",
|
||||||
|
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
|
||||||
|
"visibilityScopePublic": "公开",
|
||||||
|
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
|
||||||
"slug": "URL 别名",
|
"slug": "URL 别名",
|
||||||
"vectorStoreEnable": "是否启用向量数据库",
|
"vectorStoreEnable": "是否启用向量数据库",
|
||||||
"vectorStoreType": "向量数据库类型",
|
"vectorStoreType": "向量数据库类型",
|
||||||
@@ -47,11 +54,23 @@
|
|||||||
"importDoc": {
|
"importDoc": {
|
||||||
"fileUpload": "文件上传",
|
"fileUpload": "文件上传",
|
||||||
"parameterSettings": "参数设置",
|
"parameterSettings": "参数设置",
|
||||||
|
"strategyAnalysis": "策略分析",
|
||||||
"segmentedPreview": "分段预览",
|
"segmentedPreview": "分段预览",
|
||||||
"confirmImport": "确认导入",
|
"confirmImport": "确认导入",
|
||||||
"fileName": "文件名称",
|
"fileName": "文件名称",
|
||||||
"progressUpload": "文件上传进度",
|
"progressUpload": "文件上传进度",
|
||||||
"fileSize": "文件大小"
|
"fileSize": "文件大小",
|
||||||
|
"analysisTip": "系统会先基于文档结构做中英文规则分析,再推荐拆分策略,你也可以逐个文件手动调整。",
|
||||||
|
"confidence": "置信度",
|
||||||
|
"recommendReason": "推荐理由",
|
||||||
|
"candidateStrategies": "备选策略",
|
||||||
|
"strategySelection": "拆分策略",
|
||||||
|
"previewTip": "预览结果就是最终入库依据,确认无误后再执行导入。",
|
||||||
|
"previewEmpty": "暂无可预览内容",
|
||||||
|
"warningCount": "警告数",
|
||||||
|
"chunkCount": "分块数",
|
||||||
|
"resultEmpty": "暂无导入结果",
|
||||||
|
"importFailed": "导入失败"
|
||||||
},
|
},
|
||||||
"splitterDoc": {
|
"splitterDoc": {
|
||||||
"fileType": "文件类型",
|
"fileType": "文件类型",
|
||||||
@@ -64,6 +83,12 @@
|
|||||||
"simpleTokenizeSplitter": "简单分词器",
|
"simpleTokenizeSplitter": "简单分词器",
|
||||||
"regexDocumentSplitter": "正则文档分割器",
|
"regexDocumentSplitter": "正则文档分割器",
|
||||||
"markdownHeaderSplitter": "Markdown标题层级拆分器",
|
"markdownHeaderSplitter": "Markdown标题层级拆分器",
|
||||||
|
"autoStrategy": "自动推荐",
|
||||||
|
"markdownSection": "Markdown 标题拆分",
|
||||||
|
"outlineSection": "章节标题拆分",
|
||||||
|
"qaPair": "问答对拆分",
|
||||||
|
"paragraphLength": "自然段长度拆分",
|
||||||
|
"customRegex": "自定义正则拆分",
|
||||||
"mdSplitterLevel": "Markdown标题等级",
|
"mdSplitterLevel": "Markdown标题等级",
|
||||||
"uploadStatus": "上传状态",
|
"uploadStatus": "上传状态",
|
||||||
"pendingUpload": "待上传",
|
"pendingUpload": "待上传",
|
||||||
@@ -139,5 +164,6 @@
|
|||||||
"tencentCloud": "腾讯云",
|
"tencentCloud": "腾讯云",
|
||||||
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",
|
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",
|
||||||
"dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度",
|
"dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度",
|
||||||
"dimensionOfVectorModel": "向量模型维度"
|
"dimensionOfVectorModel": "向量模型维度",
|
||||||
|
"managePermissionHint": "仅创建者或超级管理员可修改当前知识库"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,26 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
manageable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
const handleEdit = (row: any) => {
|
const handleEdit = (row: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
form.value = { id: row.id, content: row.content };
|
form.value = { id: row.id, content: row.content };
|
||||||
openDialog();
|
openDialog();
|
||||||
};
|
};
|
||||||
const handleDelete = (row: any) => {
|
const handleDelete = (row: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||||
confirmButtonText: $t('message.ok'),
|
confirmButtonText: $t('message.ok'),
|
||||||
cancelButtonText: $t('message.cancel'),
|
cancelButtonText: $t('message.cancel'),
|
||||||
@@ -68,6 +80,10 @@ const queryParams = ref({
|
|||||||
sortType: 'asc',
|
sortType: 'asc',
|
||||||
});
|
});
|
||||||
const save = () => {
|
const save = () => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
btnLoading.value = true;
|
btnLoading.value = true;
|
||||||
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
|
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
@@ -103,7 +119,12 @@ const form = ref({
|
|||||||
:label="$t('documentCollection.content')"
|
:label="$t('documentCollection.content')"
|
||||||
min-width="240"
|
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 }">
|
<template #default="{ row }">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<ElButton link type="primary" @click="handleEdit(row)">
|
<ElButton link type="primary" @click="handleEdit(row)">
|
||||||
@@ -130,6 +151,7 @@ const form = ref({
|
|||||||
</template>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
<EasyFlowFormModal
|
<EasyFlowFormModal
|
||||||
|
v-if="props.manageable"
|
||||||
v-model:open="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
:closable="!btnLoading"
|
:closable="!btnLoading"
|
||||||
:title="$t('button.edit')"
|
:title="$t('button.edit')"
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { useAccess } from '@easyflow/access';
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
import { useUserStore } from '@easyflow/stores';
|
||||||
|
|
||||||
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
|
||||||
import { ElIcon, ElImage } from 'element-plus';
|
import { ElIcon, ElImage } from 'element-plus';
|
||||||
@@ -20,11 +22,34 @@ import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearch
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
|
|
||||||
const knowledgeId = ref<string>((route.query.id as string) || '');
|
const knowledgeId = ref<string>((route.query.id as string) || '');
|
||||||
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
|
||||||
const knowledgeInfo = ref<any>({});
|
const knowledgeInfo = ref<any>({});
|
||||||
const selectedCategory = ref('');
|
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) => {
|
const syncNavTitle = (title: string) => {
|
||||||
if (!title) {
|
if (!title) {
|
||||||
@@ -163,6 +188,9 @@ const backDoc = () => {
|
|||||||
<div class="description">
|
<div class="description">
|
||||||
{{ knowledgeInfo.description || '' }}
|
{{ knowledgeInfo.description || '' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!canManageCurrentKnowledge" class="permission-tip">
|
||||||
|
{{ $t('documentCollection.managePermissionHint') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="doc-top-menu">
|
<div class="doc-top-menu">
|
||||||
<button
|
<button
|
||||||
@@ -184,7 +212,7 @@ const backDoc = () => {
|
|||||||
<div class="doc-header" v-if="!viewDocVisible">
|
<div class="doc-header" v-if="!viewDocVisible">
|
||||||
<HeaderSearch
|
<HeaderSearch
|
||||||
v-if="!isFaqCollection"
|
v-if="!isFaqCollection"
|
||||||
:buttons="headerButtons"
|
:buttons="canManageCurrentKnowledge ? headerButtons : []"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@button-click="handleButtonClick"
|
@button-click="handleButtonClick"
|
||||||
/>
|
/>
|
||||||
@@ -192,6 +220,7 @@ const backDoc = () => {
|
|||||||
<DocumentTable
|
<DocumentTable
|
||||||
ref="documentTableRef"
|
ref="documentTableRef"
|
||||||
:knowledge-id="knowledgeId"
|
:knowledge-id="knowledgeId"
|
||||||
|
:manageable="canManageCurrentKnowledge"
|
||||||
@view-doc="viewDoc"
|
@view-doc="viewDoc"
|
||||||
v-if="!viewDocVisible"
|
v-if="!viewDocVisible"
|
||||||
/>
|
/>
|
||||||
@@ -199,24 +228,32 @@ const backDoc = () => {
|
|||||||
<ChunkDocumentTable
|
<ChunkDocumentTable
|
||||||
v-else
|
v-else
|
||||||
:document-id="documentId"
|
:document-id="documentId"
|
||||||
|
:manageable="canManageCurrentKnowledge"
|
||||||
:default-summary-prompt="knowledgeInfo.summaryPrompt"
|
:default-summary-prompt="knowledgeInfo.summaryPrompt"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedCategory === 'faqList'" class="doc-table">
|
<div v-if="selectedCategory === 'faqList'" class="doc-table">
|
||||||
<FaqTable :knowledge-id="knowledgeId" />
|
<FaqTable
|
||||||
|
:knowledge-id="knowledgeId"
|
||||||
|
:manageable="canManageCurrentKnowledge"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!--知识检索-->
|
<!--知识检索-->
|
||||||
<div
|
<div
|
||||||
v-if="selectedCategory === 'knowledgeSearch'"
|
v-if="selectedCategory === 'knowledgeSearch'"
|
||||||
class="doc-search-container"
|
class="doc-search-container"
|
||||||
>
|
>
|
||||||
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
|
<KnowledgeSearchConfig
|
||||||
|
:document-collection-id="knowledgeId"
|
||||||
|
:manageable="canManageCurrentKnowledge"
|
||||||
|
/>
|
||||||
<KnowledgeSearch :knowledge-id="knowledgeId" />
|
<KnowledgeSearch :knowledge-id="knowledgeId" />
|
||||||
</div>
|
</div>
|
||||||
<!--配置-->
|
<!--配置-->
|
||||||
<div v-if="selectedCategory === 'config'">
|
<div v-if="selectedCategory === 'config'">
|
||||||
<DocumentCollectionDataConfig
|
<DocumentCollectionDataConfig
|
||||||
:detail-data="knowledgeInfo"
|
:detail-data="knowledgeInfo"
|
||||||
|
:manageable="canManageCurrentKnowledge"
|
||||||
@reload="getKnowledge"
|
@reload="getKnowledge"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,6 +343,13 @@ const backDoc = () => {
|
|||||||
outline-offset: 1px;
|
outline-offset: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.permission-tip {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.doc-table {
|
.doc-table {
|
||||||
background-color: var(--el-bg-color);
|
background-color: var(--el-bg-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,123 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {FormInstance} from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
|
|
||||||
|
|
||||||
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
import type {
|
||||||
import CardPage from '#/components/page/CardList.vue';
|
ActionButton,
|
||||||
|
CardPrimaryAction,
|
||||||
|
} from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
import {computed, onMounted, ref} from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import {useRouter} from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
import { useAccess } from '@easyflow/access';
|
||||||
import {$t} from '@easyflow/locales';
|
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 {
|
||||||
import {tryit} from 'radash';
|
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 defaultIcon from '#/assets/ai/knowledge/book.svg';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
|
import CardPage from '#/components/page/CardList.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { hasAccessByCodes } = useAccess();
|
||||||
const collectionTypeLabelMap = {
|
const collectionTypeLabelMap = {
|
||||||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||||||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
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>) {
|
function resolveNavTitle(row: Record<string, any>) {
|
||||||
return row?.title || row?.name || '';
|
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({
|
router.push({
|
||||||
path: '/ai/documentCollection/document',
|
path: '/ai/documentCollection/document',
|
||||||
query: {
|
query: {
|
||||||
@@ -56,7 +142,7 @@ interface FieldDefinition {
|
|||||||
const primaryAction: CardPrimaryAction = {
|
const primaryAction: CardPrimaryAction = {
|
||||||
icon: Notebook,
|
icon: Notebook,
|
||||||
text: $t('documentCollection.actions.knowledge'),
|
text: $t('documentCollection.actions.knowledge'),
|
||||||
permission: '/api/v1/documentCollection/save',
|
permission: '/api/v1/documentCollection/query',
|
||||||
onClick(row) {
|
onClick(row) {
|
||||||
openKnowledgeDetail(row);
|
openKnowledgeDetail(row);
|
||||||
},
|
},
|
||||||
@@ -69,6 +155,9 @@ const actions: ActionButton[] = [
|
|||||||
permission: '/api/v1/documentCollection/save',
|
permission: '/api/v1/documentCollection/save',
|
||||||
placement: 'inline',
|
placement: 'inline',
|
||||||
onClick(row) {
|
onClick(row) {
|
||||||
|
if (!ensureManageKnowledgeItem(row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
aiKnowledgeModalRef.value.openDialog(row);
|
aiKnowledgeModalRef.value.openDialog(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -95,6 +184,9 @@ const actions: ActionButton[] = [
|
|||||||
permission: '/api/v1/documentCollection/remove',
|
permission: '/api/v1/documentCollection/remove',
|
||||||
placement: 'inline',
|
placement: 'inline',
|
||||||
onClick(row) {
|
onClick(row) {
|
||||||
|
if (!ensureManageKnowledgeItem(row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleDelete(row);
|
handleDelete(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -178,6 +270,9 @@ const formRules = computed(() => {
|
|||||||
const handleSearch = (params: any) => {
|
const handleSearch = (params: any) => {
|
||||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||||
};
|
};
|
||||||
|
const reloadKnowledgeList = () => {
|
||||||
|
pageDataRef.value?.reload?.();
|
||||||
|
};
|
||||||
const formData = ref<any>({});
|
const formData = ref<any>({});
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
@@ -189,7 +284,7 @@ function showControlDialog(item: any) {
|
|||||||
const categoryList = ref<any[]>([]);
|
const categoryList = ref<any[]>([]);
|
||||||
const getCategoryList = async () => {
|
const getCategoryList = async () => {
|
||||||
const [, res] = await tryit(api.get)(
|
const [, res] = await tryit(api.get)(
|
||||||
'/api/v1/documentCollectionCategory/list',
|
'/api/v1/documentCollectionCategory/visibleList',
|
||||||
{
|
{
|
||||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||||
},
|
},
|
||||||
@@ -258,6 +353,51 @@ const footerButton = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const saveLoading = ref(false);
|
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() {
|
function handleSubmit() {
|
||||||
formRef.value?.validate((valid) => {
|
formRef.value?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
@@ -318,7 +458,97 @@ function changeCategory(category: any) {
|
|||||||
:actions="actions"
|
:actions="actions"
|
||||||
tag-field="collectionType"
|
tag-field="collectionType"
|
||||||
:tag-map="collectionTypeLabelMap"
|
: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>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,7 +592,10 @@ function changeCategory(category: any) {
|
|||||||
</EasyFlowFormModal>
|
</EasyFlowFormModal>
|
||||||
|
|
||||||
<!-- 新增知识库模态框-->
|
<!-- 新增知识库模态框-->
|
||||||
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
|
<DocumentCollectionModal
|
||||||
|
ref="aiKnowledgeModalRef"
|
||||||
|
@reload="reloadKnowledgeList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -372,4 +605,206 @@ h1 {
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
color: #303133;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<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 {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElForm,
|
ElForm,
|
||||||
@@ -13,13 +17,9 @@ import {
|
|||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import {computed, onMounted, ref, watch} from 'vue';
|
import { api } from '#/api/request';
|
||||||
|
|
||||||
import {InfoFilled} from '@element-plus/icons-vue';
|
|
||||||
|
|
||||||
import {api} from '#/api/request';
|
|
||||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||||
import {$t} from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
detailData: {
|
detailData: {
|
||||||
@@ -45,7 +45,10 @@ const props = defineProps({
|
|||||||
searchEngineEnable: false,
|
searchEngineEnable: false,
|
||||||
englishName: '',
|
englishName: '',
|
||||||
}),
|
}),
|
||||||
required: true,
|
},
|
||||||
|
manageable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +57,7 @@ const emit = defineEmits(['reload']);
|
|||||||
const normalizeEntity = (raw: any) => {
|
const normalizeEntity = (raw: any) => {
|
||||||
const options = {
|
const options = {
|
||||||
canUpdateEmbeddingModel: true,
|
canUpdateEmbeddingModel: true,
|
||||||
...(raw?.options || {}),
|
...raw?.options,
|
||||||
};
|
};
|
||||||
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
if (options.rerankEnable === undefined || options.rerankEnable === null) {
|
||||||
options.rerankEnable = !!raw?.rerankModelId;
|
options.rerankEnable = !!raw?.rerankModelId;
|
||||||
@@ -102,7 +105,7 @@ const vectorStoreConfigPlaceholder = computed(() => {
|
|||||||
|
|
||||||
const getEmbeddingLlmListData = async () => {
|
const getEmbeddingLlmListData = async () => {
|
||||||
try {
|
try {
|
||||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||||
const res = await api.get(url, {});
|
const res = await api.get(url, {});
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
embeddingLlmList.value = res.data;
|
embeddingLlmList.value = res.data;
|
||||||
@@ -115,7 +118,9 @@ const getEmbeddingLlmListData = async () => {
|
|||||||
|
|
||||||
const getRerankerLlmListData = async () => {
|
const getRerankerLlmListData = async () => {
|
||||||
try {
|
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;
|
rerankerLlmList.value = res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error($t('message.apiError'));
|
ElMessage.error($t('message.apiError'));
|
||||||
@@ -156,6 +161,10 @@ const rules = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const valid = await saveForm.value?.validate();
|
const valid = await saveForm.value?.validate();
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
@@ -190,9 +199,13 @@ async function save() {
|
|||||||
label-width="150px"
|
label-width="150px"
|
||||||
ref="saveForm"
|
ref="saveForm"
|
||||||
:model="entity"
|
:model="entity"
|
||||||
|
:disabled="!props.manageable"
|
||||||
status-icon
|
status-icon
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
>
|
>
|
||||||
|
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||||
|
{{ $t('documentCollection.managePermissionHint') }}
|
||||||
|
</div>
|
||||||
<ElFormItem
|
<ElFormItem
|
||||||
prop="icon"
|
prop="icon"
|
||||||
:label="$t('documentCollection.icon')"
|
:label="$t('documentCollection.icon')"
|
||||||
@@ -371,7 +384,7 @@ async function save() {
|
|||||||
type="primary"
|
type="primary"
|
||||||
@click="save"
|
@click="save"
|
||||||
:loading="btnLoading"
|
:loading="btnLoading"
|
||||||
:disabled="btnLoading"
|
:disabled="btnLoading || !props.manageable"
|
||||||
>
|
>
|
||||||
{{ $t('button.save') }}
|
{{ $t('button.save') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -385,4 +398,11 @@ async function save() {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-readonly-tip {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ const rerankerLlmList = ref<any>([]);
|
|||||||
|
|
||||||
const getEmbeddingLlmListData = async () => {
|
const getEmbeddingLlmListData = async () => {
|
||||||
try {
|
try {
|
||||||
const url = `/api/v1/model/list?modelType=embeddingModel`;
|
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
|
||||||
const res = await api.get(url, {});
|
const res = await api.get(url, {});
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
embeddingLlmList.value = res.data;
|
embeddingLlmList.value = res.data;
|
||||||
@@ -42,7 +42,9 @@ const getEmbeddingLlmListData = async () => {
|
|||||||
|
|
||||||
const getRerankerLlmListData = async () => {
|
const getRerankerLlmListData = async () => {
|
||||||
try {
|
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;
|
rerankerLlmList.value = res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error($t('message.apiError'));
|
ElMessage.error($t('message.apiError'));
|
||||||
@@ -87,8 +89,9 @@ const defaultEntity = {
|
|||||||
rerankEnable: false,
|
rerankEnable: false,
|
||||||
},
|
},
|
||||||
rerankModelId: '',
|
rerankModelId: '',
|
||||||
searchEngineEnable: '',
|
searchEngineEnable: false,
|
||||||
englishName: '',
|
englishName: '',
|
||||||
|
visibilityScope: 'PRIVATE',
|
||||||
};
|
};
|
||||||
const normalizeEntity = (raw: any = {}) => {
|
const normalizeEntity = (raw: any = {}) => {
|
||||||
const options = {
|
const options = {
|
||||||
@@ -135,6 +138,20 @@ const rules = ref({
|
|||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{ 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 = [
|
const collectionTypeList = [
|
||||||
{
|
{
|
||||||
label: $t('documentCollection.collectionTypeDocument'),
|
label: $t('documentCollection.collectionTypeDocument'),
|
||||||
@@ -257,6 +274,19 @@ defineExpose({
|
|||||||
dict-code="aiDocumentCollectionCategory"
|
dict-code="aiDocumentCollectionCategory"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</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')">
|
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
|
||||||
<ElInput v-model.trim="entity.alias" />
|
<ElInput v-model.trim="entity.alias" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { $t } from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
|
import { downloadFileFromBlob } from '@easyflow/utils';
|
||||||
|
|
||||||
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,10 @@ const props = defineProps({
|
|||||||
required: true,
|
required: true,
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
manageable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const emits = defineEmits(['viewDoc']);
|
const emits = defineEmits(['viewDoc']);
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -38,10 +43,20 @@ const pageDataRef = ref();
|
|||||||
const handleView = (row: any) => {
|
const handleView = (row: any) => {
|
||||||
emits('viewDoc', row.id);
|
emits('viewDoc', row.id);
|
||||||
};
|
};
|
||||||
const handleDownload = (row: any) => {
|
const handleDownload = async (row: any) => {
|
||||||
window.open(row.documentPath, '_blank');
|
const blob = await api.download(
|
||||||
|
`/api/v1/document/download?documentId=${row.id}`,
|
||||||
|
);
|
||||||
|
downloadFileFromBlob({
|
||||||
|
fileName: row.title || 'document',
|
||||||
|
source: blob,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleDelete = (row: any) => {
|
const handleDelete = (row: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
cancelButtonText: $t('button.cancel'),
|
cancelButtonText: $t('button.cancel'),
|
||||||
@@ -122,7 +137,10 @@ const handleDelete = (row: any) => {
|
|||||||
{{ $t('button.download') }}
|
{{ $t('button.download') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem @click="handleDelete(row)">
|
<ElDropdownItem
|
||||||
|
v-if="props.manageable"
|
||||||
|
@click="handleDelete(row)"
|
||||||
|
>
|
||||||
<ElButton link :icon="Delete" type="danger">
|
<ElButton link :icon="Delete" type="danger">
|
||||||
{{ $t('button.delete') }}
|
{{ $t('button.delete') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import {
|
|||||||
ElDropdown,
|
ElDropdown,
|
||||||
ElDropdownItem,
|
ElDropdownItem,
|
||||||
ElDropdownMenu,
|
ElDropdownMenu,
|
||||||
|
ElInput,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
ElMessageBox,
|
ElMessageBox,
|
||||||
ElInput,
|
|
||||||
ElTable,
|
ElTable,
|
||||||
ElTableColumn,
|
ElTableColumn,
|
||||||
ElTree,
|
ElTree,
|
||||||
@@ -43,6 +43,10 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
manageable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageDataRef = ref();
|
const pageDataRef = ref();
|
||||||
@@ -143,6 +147,10 @@ const handleResetSearch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
editData.value = {
|
editData.value = {
|
||||||
collectionId: props.knowledgeId,
|
collectionId: props.knowledgeId,
|
||||||
categoryId:
|
categoryId:
|
||||||
@@ -189,12 +197,17 @@ const exportFaqExcel = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportSuccess = () => {
|
const handleImportSuccess = async () => {
|
||||||
|
await reloadCategoryTree();
|
||||||
refreshList();
|
refreshList();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMoreActionCommand = (command: string) => {
|
const handleMoreActionCommand = (command: string) => {
|
||||||
if (command === 'import') {
|
if (command === 'import') {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
importDialogVisible.value = true;
|
importDialogVisible.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -208,6 +221,10 @@ const handleMoreActionCommand = (command: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditDialog = (row: any) => {
|
const openEditDialog = (row: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
editData.value = {
|
editData.value = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
collectionId: row.collectionId,
|
collectionId: row.collectionId,
|
||||||
@@ -223,6 +240,10 @@ const openEditDialog = (row: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveFaq = async (payload: 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 url = payload.id ? '/api/v1/faqItem/update' : '/api/v1/faqItem/save';
|
||||||
const res = await api.post(url, payload);
|
const res = await api.post(url, payload);
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
@@ -237,6 +258,10 @@ const saveFaq = async (payload: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeFaq = (row: any) => {
|
const removeFaq = (row: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
cancelButtonText: $t('button.cancel'),
|
cancelButtonText: $t('button.cancel'),
|
||||||
@@ -259,6 +284,10 @@ const handleCategoryClick = (data: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openAddRootCategory = () => {
|
const openAddRootCategory = () => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
categoryDialogTitle.value = $t('documentCollection.faq.addCategory');
|
categoryDialogTitle.value = $t('documentCollection.faq.addCategory');
|
||||||
categoryDialogDisableParent.value = false;
|
categoryDialogDisableParent.value = false;
|
||||||
categoryEditData.value = {
|
categoryEditData.value = {
|
||||||
@@ -271,6 +300,10 @@ const openAddRootCategory = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openAddSiblingCategory = (node: any) => {
|
const openAddSiblingCategory = (node: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
categoryDialogTitle.value = $t('documentCollection.faq.addSiblingCategory');
|
categoryDialogTitle.value = $t('documentCollection.faq.addSiblingCategory');
|
||||||
categoryDialogDisableParent.value = false;
|
categoryDialogDisableParent.value = false;
|
||||||
categoryEditData.value = {
|
categoryEditData.value = {
|
||||||
@@ -286,6 +319,10 @@ const openAddSiblingCategory = (node: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openAddChildCategory = (node: any) => {
|
const openAddChildCategory = (node: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (node.isDefault) {
|
if (node.isDefault) {
|
||||||
ElMessage.warning(
|
ElMessage.warning(
|
||||||
$t('documentCollection.faq.defaultCategoryChildForbidden'),
|
$t('documentCollection.faq.defaultCategoryChildForbidden'),
|
||||||
@@ -308,6 +345,10 @@ const openAddChildCategory = (node: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openEditCategory = (node: any) => {
|
const openEditCategory = (node: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
categoryDialogTitle.value = $t('documentCollection.faq.editCategory');
|
categoryDialogTitle.value = $t('documentCollection.faq.editCategory');
|
||||||
categoryDialogDisableParent.value = !!node.isDefault;
|
categoryDialogDisableParent.value = !!node.isDefault;
|
||||||
categoryEditData.value = {
|
categoryEditData.value = {
|
||||||
@@ -324,6 +365,10 @@ const openEditCategory = (node: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const removeCategory = (node: any) => {
|
const removeCategory = (node: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (node.isDefault) {
|
if (node.isDefault) {
|
||||||
ElMessage.warning(
|
ElMessage.warning(
|
||||||
$t('documentCollection.faq.defaultCategoryDeleteForbidden'),
|
$t('documentCollection.faq.defaultCategoryDeleteForbidden'),
|
||||||
@@ -612,6 +657,10 @@ const demoteCategory = async (node: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveCategory = async (payload: any) => {
|
const saveCategory = async (payload: any) => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const url = payload.id
|
const url = payload.id
|
||||||
? '/api/v1/faqCategory/update'
|
? '/api/v1/faqCategory/update'
|
||||||
: '/api/v1/faqCategory/save';
|
: '/api/v1/faqCategory/save';
|
||||||
@@ -692,6 +741,7 @@ onMounted(() => {
|
|||||||
<div class="faq-category-header">
|
<div class="faq-category-header">
|
||||||
<span>{{ $t('documentCollection.faq.categoryTree') }}</span>
|
<span>{{ $t('documentCollection.faq.categoryTree') }}</span>
|
||||||
<ElButton
|
<ElButton
|
||||||
|
v-if="props.manageable"
|
||||||
link
|
link
|
||||||
type="primary"
|
type="primary"
|
||||||
:icon="Plus"
|
:icon="Plus"
|
||||||
@@ -719,7 +769,7 @@ onMounted(() => {
|
|||||||
data.categoryName
|
data.categoryName
|
||||||
}}</span>
|
}}</span>
|
||||||
<div
|
<div
|
||||||
v-if="!data.isVirtual && !data.isDefault"
|
v-if="props.manageable && !data.isVirtual && !data.isDefault"
|
||||||
class="faq-category-node-actions"
|
class="faq-category-node-actions"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@@ -770,7 +820,10 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
{{ $t('documentCollection.faq.addChildCategory') }}
|
{{ $t('documentCollection.faq.addChildCategory') }}
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem :icon="Edit" @click="openEditCategory(data)">
|
<ElDropdownItem
|
||||||
|
:icon="Edit"
|
||||||
|
@click="openEditCategory(data)"
|
||||||
|
>
|
||||||
{{ $t('button.edit') }}
|
{{ $t('button.edit') }}
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
@@ -791,6 +844,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div class="faq-content-pane">
|
<div class="faq-content-pane">
|
||||||
<div class="faq-header">
|
<div class="faq-header">
|
||||||
|
<div v-if="!props.manageable" class="faq-readonly-tip">
|
||||||
|
{{ $t('documentCollection.managePermissionHint') }}
|
||||||
|
</div>
|
||||||
<div class="faq-toolbar">
|
<div class="faq-toolbar">
|
||||||
<div class="faq-search-actions">
|
<div class="faq-search-actions">
|
||||||
<ElInput
|
<ElInput
|
||||||
@@ -809,19 +865,25 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="faq-primary-actions">
|
<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') }}
|
{{ $t('button.add') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElDropdown
|
<ElDropdown trigger="click" @command="handleMoreActionCommand">
|
||||||
trigger="click"
|
|
||||||
@command="handleMoreActionCommand"
|
|
||||||
>
|
|
||||||
<ElButton :icon="MoreFilled">
|
<ElButton :icon="MoreFilled">
|
||||||
{{ $t('documentCollection.faq.import.moreActions') }}
|
{{ $t('documentCollection.faq.import.moreActions') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<ElDropdownItem command="import" :icon="Upload">
|
<ElDropdownItem
|
||||||
|
v-if="props.manageable"
|
||||||
|
command="import"
|
||||||
|
:icon="Upload"
|
||||||
|
>
|
||||||
{{ $t('button.import') }}
|
{{ $t('button.import') }}
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
@@ -871,6 +933,7 @@ onMounted(() => {
|
|||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
|
v-if="props.manageable"
|
||||||
:label="$t('common.handle')"
|
:label="$t('common.handle')"
|
||||||
width="170"
|
width="170"
|
||||||
align="right"
|
align="right"
|
||||||
@@ -1002,6 +1065,13 @@ onMounted(() => {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.faq-readonly-tip {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.faq-toolbar {
|
.faq-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1062,8 +1132,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(
|
: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;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1077,7 +1150,9 @@ onMounted(() => {
|
|||||||
background-color: var(--el-fill-color-light);
|
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;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,7 +1161,9 @@ onMounted(() => {
|
|||||||
color: hsl(var(--primary));
|
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;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<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 {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElForm,
|
ElForm,
|
||||||
@@ -16,13 +16,17 @@ import {
|
|||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import {api} from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
documentCollectionId: {
|
documentCollectionId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
manageable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -55,6 +59,10 @@ const searchConfig = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const submitConfig = () => {
|
const submitConfig = () => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
ElMessage.warning($t('documentCollection.managePermissionHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const submitData = {
|
const submitData = {
|
||||||
id: props.documentCollectionId,
|
id: props.documentCollectionId,
|
||||||
options: {
|
options: {
|
||||||
@@ -89,6 +97,9 @@ const searchEngineOptions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const handleSearchEngineEnableChange = () => {
|
const handleSearchEngineEnableChange = () => {
|
||||||
|
if (!props.manageable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
api.post('/api/v1/documentCollection/update', {
|
api.post('/api/v1/documentCollection/update', {
|
||||||
id: props.documentCollectionId,
|
id: props.documentCollectionId,
|
||||||
searchEngineEnable: searchEngineEnable.value,
|
searchEngineEnable: searchEngineEnable.value,
|
||||||
@@ -100,11 +111,15 @@ const handleSearchEngineEnableChange = () => {
|
|||||||
<div class="search-config-sidebar">
|
<div class="search-config-sidebar">
|
||||||
<div class="config-header">
|
<div class="config-header">
|
||||||
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
|
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
|
||||||
|
<div v-if="!props.manageable" class="config-readonly-tip">
|
||||||
|
{{ $t('documentCollection.managePermissionHint') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElForm
|
<ElForm
|
||||||
class="config-form"
|
class="config-form"
|
||||||
:model="searchConfig"
|
:model="searchConfig"
|
||||||
|
:disabled="!props.manageable"
|
||||||
label-width="100%"
|
label-width="100%"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
@@ -227,7 +242,12 @@ const handleSearchEngineEnableChange = () => {
|
|||||||
</ElForm>
|
</ElForm>
|
||||||
|
|
||||||
<div class="config-footer">
|
<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') }}
|
{{ $t('documentCollectionSearch.button.save') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,6 +276,13 @@ const handleSearchEngineEnableChange = () => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-readonly-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.config-form {
|
.config-form {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {FormInstance} from 'element-plus';
|
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 type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
|
||||||
import CardList from '#/components/page/CardList.vue';
|
import CardList from '#/components/page/CardList.vue';
|
||||||
|
|
||||||
import {computed, markRaw, onMounted, ref} from 'vue';
|
import {computed, markRaw, onMounted, ref} from 'vue';
|
||||||
|
|
||||||
|
import {useAccess} from '@easyflow/access';
|
||||||
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
import {EasyFlowFormModal} from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Check,
|
||||||
CopyDocument,
|
CopyDocument,
|
||||||
Delete,
|
Delete,
|
||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
|
Lock,
|
||||||
|
OfficeBuilding,
|
||||||
Plus,
|
Plus,
|
||||||
|
Promotion,
|
||||||
Tickets,
|
Tickets,
|
||||||
Upload,
|
Upload,
|
||||||
VideoPlay,
|
VideoPlay,
|
||||||
@@ -47,6 +61,8 @@ interface FieldDefinition {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
|
||||||
|
|
||||||
const primaryAction: CardPrimaryAction = {
|
const primaryAction: CardPrimaryAction = {
|
||||||
icon: DesignIcon,
|
icon: DesignIcon,
|
||||||
text: $t('button.design'),
|
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[] = [
|
const actions: ActionButton[] = [
|
||||||
{
|
{
|
||||||
icon: Edit,
|
icon: Edit,
|
||||||
@@ -96,6 +145,7 @@ const actions: ActionButton[] = [
|
|||||||
{
|
{
|
||||||
icon: Download,
|
icon: Download,
|
||||||
text: $t('button.export'),
|
text: $t('button.export'),
|
||||||
|
permission: '/api/v1/workflow/save',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
exportJson(row);
|
exportJson(row);
|
||||||
@@ -104,6 +154,7 @@ const actions: ActionButton[] = [
|
|||||||
{
|
{
|
||||||
icon: CopyDocument,
|
icon: CopyDocument,
|
||||||
text: $t('button.copy'),
|
text: $t('button.copy'),
|
||||||
|
permission: '/api/v1/workflow/save',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
showDialog({
|
showDialog({
|
||||||
@@ -151,6 +202,50 @@ const headerButtons = [
|
|||||||
function initDict() {
|
function initDict() {
|
||||||
dictStore.fetchDictionary('dataStatus');
|
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) => {
|
const handleSearch = (params: string) => {
|
||||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||||
};
|
};
|
||||||
@@ -341,7 +436,7 @@ function handleSubmit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const getSideList = async () => {
|
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' },
|
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,7 +489,90 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
:data="pageList"
|
:data="pageList"
|
||||||
:primary-action="primaryAction"
|
:primary-action="primaryAction"
|
||||||
:actions="actions"
|
: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>
|
</template>
|
||||||
</PageData>
|
</PageData>
|
||||||
</div>
|
</div>
|
||||||
@@ -440,4 +618,205 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 { 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 { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
@@ -28,7 +36,7 @@ const isImport = ref(false);
|
|||||||
const jsonFile = ref<any>(null);
|
const jsonFile = ref<any>(null);
|
||||||
const uploadFileList = ref<any[]>([]);
|
const uploadFileList = ref<any[]>([]);
|
||||||
const uploadRef = ref<UploadInstance>();
|
const uploadRef = ref<UploadInstance>();
|
||||||
const entity = ref<any>({
|
const createDefaultEntity = () => ({
|
||||||
alias: '',
|
alias: '',
|
||||||
deptId: '',
|
deptId: '',
|
||||||
title: '',
|
title: '',
|
||||||
@@ -36,8 +44,24 @@ const entity = ref<any>({
|
|||||||
icon: '',
|
icon: '',
|
||||||
content: '',
|
content: '',
|
||||||
englishName: '',
|
englishName: '',
|
||||||
|
visibilityScope: 'PRIVATE',
|
||||||
});
|
});
|
||||||
|
const entity = ref<any>(createDefaultEntity());
|
||||||
const btnLoading = ref(false);
|
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({
|
const jsonFileModel = computed({
|
||||||
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
|
||||||
set: (value: any) => {
|
set: (value: any) => {
|
||||||
@@ -57,10 +81,11 @@ const rules = computed(() => ({
|
|||||||
// functions
|
// functions
|
||||||
function openDialog(row: any, importMode = false) {
|
function openDialog(row: any, importMode = false) {
|
||||||
isImport.value = importMode;
|
isImport.value = importMode;
|
||||||
if (row.id) {
|
isAdd.value = !row?.id;
|
||||||
isAdd.value = false;
|
entity.value = {
|
||||||
}
|
...createDefaultEntity(),
|
||||||
entity.value = row;
|
...(row || {}),
|
||||||
|
};
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +162,7 @@ function closeDialog() {
|
|||||||
jsonFile.value = null;
|
jsonFile.value = null;
|
||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
isImport.value = false;
|
isImport.value = false;
|
||||||
entity.value = {};
|
entity.value = createDefaultEntity();
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -198,6 +223,19 @@ function closeDialog() {
|
|||||||
dict-code="aiWorkFlowCategory"
|
dict-code="aiWorkFlowCategory"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</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')">
|
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
|
||||||
<ElInput v-model.trim="entity.alias" />
|
<ElInput v-model.trim="entity.alias" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user