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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ import tech.easyflow.ai.entity.base.DocumentCollectionBase;
import tech.easyflow.common.util.PropertiesUtil;
import tech.easyflow.common.util.StringUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.permission.resource.VisibilityResource;
import java.math.BigDecimal;
import java.util.Map;
@@ -32,7 +33,7 @@ import java.util.Map;
*/
@Table("tb_document_collection")
public class DocumentCollection extends DocumentCollectionBase {
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
public static final String TYPE_DOCUMENT = "DOCUMENT";
public static final String TYPE_FAQ = "FAQ";
@@ -71,6 +72,10 @@ public class DocumentCollection extends DocumentCollectionBase {
* 是否启用重排模型
*/
public static final String KEY_RERANK_ENABLE = "rerankEnable";
public static final String KEY_SPLITTER_DEFAULT_STRATEGY = "splitter.defaultStrategyCode";
public static final String KEY_SPLITTER_AUTO_RECOMMEND_ENABLED = "splitter.autoRecommendEnabled";
public static final String KEY_SPLITTER_FALLBACK_STRATEGY = "splitter.fallbackStrategyCode";
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
public DocumentStore toDocumentStore() {
String storeType = this.getVectorStoreType();

View File

@@ -4,6 +4,7 @@ import com.easyagents.core.model.chat.tool.Tool;
import com.mybatisflex.annotation.Table;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.entity.base.WorkflowBase;
import tech.easyflow.system.permission.resource.VisibilityResource;
/**
* 实体类。
@@ -13,7 +14,7 @@ import tech.easyflow.ai.entity.base.WorkflowBase;
*/
@Table("tb_workflow")
public class Workflow extends WorkflowBase {
public class Workflow extends WorkflowBase implements VisibilityResource {
public Tool toFunction(boolean needEnglishName) {
return new WorkflowTool(this, needEnglishName);

View File

@@ -3,8 +3,10 @@ package tech.easyflow.ai.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Map;
public class DocumentChunkBase implements Serializable {
@@ -38,6 +40,12 @@ public class DocumentChunkBase implements Serializable {
@Column(comment = "分割顺序")
private Integer sorting;
/**
* 扩展元信息
*/
@Column(typeHandler = FastjsonTypeHandler.class, comment = "扩展元信息")
private Map<String, Object> options;
public BigInteger getId() {
return id;
}
@@ -78,4 +86,12 @@ public class DocumentChunkBase implements Serializable {
this.sorting = sorting;
}
public Map<String, Object> getOptions() {
return options;
}
public void setOptions(Map<String, Object> options) {
this.options = options;
}
}

View File

@@ -160,6 +160,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
@Column(comment = "分类ID")
private BigInteger categoryId;
/**
* 可见范围
*/
@Column(comment = "可见范围")
private String visibilityScope;
public BigInteger getId() {
return id;
}
@@ -352,4 +358,12 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
this.categoryId = categoryId;
}
public String getVisibilityScope() {
return visibilityScope;
}
public void setVisibilityScope(String visibilityScope) {
this.visibilityScope = visibilityScope;
}
}

View File

@@ -103,6 +103,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
@Column(comment = "分类ID")
private BigInteger categoryId;
/**
* 可见范围
*/
@Column(comment = "可见范围")
private String visibilityScope;
public BigInteger getId() {
return id;
}
@@ -223,4 +229,12 @@ public class WorkflowBase extends DateEntity implements Serializable {
this.categoryId = categoryId;
}
public String getVisibilityScope() {
return visibilityScope;
}
public void setVisibilityScope(String visibilityScope) {
this.visibilityScope = visibilityScope;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -31,6 +31,8 @@ public interface ModelService extends IService<Model> {
List<Model> listInvokeModels();
List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType);
Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled);
List<Model> batchUpdateInvokePublishStatus(List<BigInteger> ids, Boolean publishEnabled);

View File

@@ -10,6 +10,7 @@ import com.easyagents.core.model.embedding.EmbeddingModel;
import com.easyagents.core.model.rerank.RerankModel;
import com.easyagents.core.store.VectorData;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.util.StringUtil;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +23,9 @@ import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.ai.mapper.ModelMapper;
import tech.easyflow.ai.service.ModelProviderService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.common.tree.Tree;
import tech.easyflow.common.util.SqlOperatorsUtil;
import tech.easyflow.common.util.SqlUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.annotation.Resource;
@@ -249,6 +253,18 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
.collect(Collectors.toList());
}
@Override
public List<Model> listSelectableModels(Model entity, Boolean asTree, String sortKey, String sortType) {
QueryWrapper queryWrapper = QueryWrapper.create(
entity,
entity == null ? com.mybatisflex.core.query.SqlOperators.empty() : SqlOperatorsUtil.build(entity.getClass())
);
queryWrapper.orderBy(buildOrderBy(sortKey, sortType));
List<Model> list = Tree.tryToTree(modelMapper.selectListWithRelationsByQuery(queryWrapper), asTree);
list.forEach(this::decorateModelTitle);
return list;
}
@Override
public Model updateInvokeConfig(BigInteger id, String invokeCode, Boolean publishEnabled) {
Model existing = getModelInstance(id);
@@ -281,6 +297,21 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
return updatedModels;
}
private void decorateModelTitle(Model model) {
if (model == null) {
return;
}
String providerName = Optional.ofNullable(model.getModelProvider())
.map(ModelProvider::getProviderName)
.orElse("-");
model.setTitle(providerName + "/" + model.getTitle());
}
private String buildOrderBy(String sortKey, String sortType) {
sortKey = StringUtil.camelToUnderline(sortKey);
return SqlUtil.buildOrderBy(sortKey, sortType, "id desc");
}
private String buildDefaultInvokeCode(String modelName) {
if (StrUtil.isBlank(modelName)) {
return null;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
package tech.easyflow.system.enums;
public enum ResourceAction {
READ,
USE,
MANAGE
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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 "无权限访问该资源";
}

View File

@@ -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("无权限访问该执行记录");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -3,6 +3,9 @@ package tech.easyflow.system.service;
import tech.easyflow.system.entity.SysDept;
import com.mybatisflex.core.service.IService;
import java.math.BigInteger;
import java.util.Set;
/**
* 部门表 服务层。
*
@@ -11,4 +14,7 @@ import com.mybatisflex.core.service.IService;
*/
public interface SysDeptService extends IService<SysDept> {
Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId);
boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId);
}

View File

@@ -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);
}
}
}

View File

@@ -6,6 +6,11 @@ import tech.easyflow.system.service.SysDeptService;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 部门表 服务层实现。
*
@@ -15,4 +20,39 @@ import org.springframework.stereotype.Service;
@Service
public class SysDeptServiceImpl extends ServiceImpl<SysDeptMapper, SysDept> implements SysDeptService {
@Override
public Set<BigInteger> getSelfAndAncestorDeptIds(BigInteger currentDeptId) {
if (currentDeptId == null) {
return Collections.emptySet();
}
SysDept currentDept = getById(currentDeptId);
if (currentDept == null) {
return Collections.emptySet();
}
Set<BigInteger> deptIds = new LinkedHashSet<>();
String ancestors = currentDept.getAncestors();
if (ancestors != null && !ancestors.isBlank()) {
String[] items = ancestors.split(",");
for (String item : items) {
if (item == null || item.isBlank()) {
continue;
}
BigInteger deptId = new BigInteger(item.trim());
if (BigInteger.ZERO.equals(deptId)) {
continue;
}
deptIds.add(deptId);
}
}
deptIds.add(currentDeptId);
return deptIds;
}
@Override
public boolean canUserAccessDeptScopedResource(BigInteger currentDeptId, BigInteger resourceDeptId) {
if (currentDeptId == null || resourceDeptId == null) {
return false;
}
return getSelfAndAncestorDeptIds(currentDeptId).contains(resourceDeptId);
}
}

View File

@@ -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)));
}
}

View File

@@ -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 = '';

View File

@@ -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 = '';

View File

@@ -46,6 +46,9 @@ export interface CardListProps {
titleField?: string;
descField?: string;
actions?: ActionButton[];
cornerTagField?: string;
cornerTagMap?: Record<string, string>;
cornerTagTypeMap?: Record<string, string>;
defaultIcon: any;
data: any[];
primaryAction?: CardPrimaryAction;
@@ -58,6 +61,9 @@ const props = withDefaults(defineProps<CardListProps>(), {
titleField: 'title',
descField: 'description',
actions: () => [],
cornerTagField: '',
cornerTagMap: () => ({}),
cornerTagTypeMap: () => ({}),
primaryAction: undefined,
tagField: '',
tagMap: () => ({}),
@@ -154,6 +160,23 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
{{ item[descField] }}
</ElText>
</div>
<div
v-if="$slots.corner || (cornerTagField && item[cornerTagField])"
class="card-corner-tag"
>
<slot name="corner" :item="item">
<ElTag
size="small"
effect="plain"
:type="cornerTagTypeMap[item[cornerTagField]] || 'info'"
round
>
{{
cornerTagMap[item[cornerTagField]] || item[cornerTagField]
}}
</ElTag>
</slot>
</div>
</div>
</div>
@@ -343,6 +366,26 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
color: hsl(var(--text-muted));
}
.card-corner-tag {
display: flex;
flex-shrink: 0;
align-items: flex-start;
justify-content: flex-end;
min-height: 28px;
margin-left: auto;
}
.card-corner-tag :deep(.el-tag) {
--el-tag-border-radius: 999px;
--el-tag-font-size: 12px;
--el-tag-border-color: transparent;
padding: 0 10px;
font-weight: 600;
letter-spacing: 0.01em;
backdrop-filter: blur(6px);
}
.card-footer {
display: flex;
gap: 12px;

View File

@@ -14,6 +14,13 @@
"englishName": "EnglishName",
"status": "ShowInUserCenter",
"categoryId": "Category",
"visibilityScope": "Visibility Scope",
"visibilityScopePrivate": "Personal",
"visibilityScopePrivateDesc": "Only the creator can access it",
"visibilityScopeDept": "Dept",
"visibilityScopeDeptDesc": "Available to the dept and descendants",
"visibilityScopePublic": "Public",
"visibilityScopePublicDesc": "Available to internal users matched by category",
"params": "Params",
"steps": "Steps",
"result": "Result",

View File

@@ -10,6 +10,13 @@
"collectionTypeDocument": "Document",
"collectionTypeFaq": "FAQ",
"description": "Description",
"visibilityScope": "Visibility Scope",
"visibilityScopePrivate": "Personal",
"visibilityScopePrivateDesc": "Only the creator can access it",
"visibilityScopeDept": "Dept",
"visibilityScopeDeptDesc": "Available to the dept and descendants",
"visibilityScopePublic": "Public",
"visibilityScopePublicDesc": "Available to internal users matched by category",
"slug": "Slug",
"vectorStoreEnable": "VectorStoreEnable",
"vectorStoreType": "VectorStoreType",
@@ -46,12 +53,24 @@
},
"importDoc": {
"fileUpload": "File upload",
"parameterSettings": "ParameterSettings",
"parameterSettings": "Parameter settings",
"strategyAnalysis": "Strategy analysis",
"segmentedPreview": "SegmentedPreview",
"confirmImport": "ConfirmImport",
"fileName": "File Name",
"progressUpload": "Progress of file upload",
"fileSize": "File size"
"fileSize": "File size",
"analysisTip": "The system analyzes multilingual structure first and recommends a splitting strategy. You can still adjust each file manually.",
"confidence": "Confidence",
"recommendReason": "Reasons",
"candidateStrategies": "Candidates",
"strategySelection": "Strategy",
"previewTip": "The preview result is the final import basis. Confirm it before committing.",
"previewEmpty": "No preview data",
"warningCount": "Warnings",
"chunkCount": "Chunks",
"resultEmpty": "No import result",
"importFailed": "Import failed"
},
"splitterDoc": {
"fileType": "FileType",
@@ -64,6 +83,12 @@
"simpleTokenizeSplitter": "SimpleTokenizeSplitter",
"regexDocumentSplitter": "RegexDocumentSplitter",
"markdownHeaderSplitter": "MarkdownHeaderSplitter",
"autoStrategy": "Auto recommendation",
"markdownSection": "Markdown headings",
"outlineSection": "Outline sections",
"qaPair": "Q&A pairs",
"paragraphLength": "Paragraph length",
"customRegex": "Custom regex",
"mdSplitterLevel": "MarkdownSplitterLevel",
"uploadStatus": "UploadStatus",
"pendingUpload": "PendingUpload",
@@ -139,5 +164,6 @@
"tencentCloud": "tencentCloud",
"vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model",
"dimensionOfVectorModelTips": "After successful vector data, it is not allowed to modify the dimensions of the vector model",
"dimensionOfVectorModel": "Dimension of vector model"
"dimensionOfVectorModel": "Dimension of vector model",
"managePermissionHint": "Only the creator or super admin can modify this knowledge base"
}

View File

@@ -14,6 +14,13 @@
"englishName": "英文名称",
"status": "在用户中心显示",
"categoryId": "分类",
"visibilityScope": "可见范围",
"visibilityScopePrivate": "个人",
"visibilityScopePrivateDesc": "仅创建者可访问",
"visibilityScopeDept": "部门",
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
"visibilityScopePublic": "公开",
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
"params": "执行参数",
"steps": "执行步骤",
"result": "执行结果",

View File

@@ -10,6 +10,13 @@
"collectionTypeDocument": "文档",
"collectionTypeFaq": "FAQ",
"description": "描述",
"visibilityScope": "可见范围",
"visibilityScopePrivate": "个人",
"visibilityScopePrivateDesc": "仅创建者可访问",
"visibilityScopeDept": "部门",
"visibilityScopeDeptDesc": "本部门及下级部门可访问",
"visibilityScopePublic": "公开",
"visibilityScopePublicDesc": "分类命中的内部用户可访问",
"slug": "URL 别名",
"vectorStoreEnable": "是否启用向量数据库",
"vectorStoreType": "向量数据库类型",
@@ -47,11 +54,23 @@
"importDoc": {
"fileUpload": "文件上传",
"parameterSettings": "参数设置",
"strategyAnalysis": "策略分析",
"segmentedPreview": "分段预览",
"confirmImport": "确认导入",
"fileName": "文件名称",
"progressUpload": "文件上传进度",
"fileSize": "文件大小"
"fileSize": "文件大小",
"analysisTip": "系统会先基于文档结构做中英文规则分析,再推荐拆分策略,你也可以逐个文件手动调整。",
"confidence": "置信度",
"recommendReason": "推荐理由",
"candidateStrategies": "备选策略",
"strategySelection": "拆分策略",
"previewTip": "预览结果就是最终入库依据,确认无误后再执行导入。",
"previewEmpty": "暂无可预览内容",
"warningCount": "警告数",
"chunkCount": "分块数",
"resultEmpty": "暂无导入结果",
"importFailed": "导入失败"
},
"splitterDoc": {
"fileType": "文件类型",
@@ -64,6 +83,12 @@
"simpleTokenizeSplitter": "简单分词器",
"regexDocumentSplitter": "正则文档分割器",
"markdownHeaderSplitter": "Markdown标题层级拆分器",
"autoStrategy": "自动推荐",
"markdownSection": "Markdown 标题拆分",
"outlineSection": "章节标题拆分",
"qaPair": "问答对拆分",
"paragraphLength": "自然段长度拆分",
"customRegex": "自定义正则拆分",
"mdSplitterLevel": "Markdown标题等级",
"uploadStatus": "上传状态",
"pendingUpload": "待上传",
@@ -139,5 +164,6 @@
"tencentCloud": "腾讯云",
"vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型",
"dimensionOfVectorModelTips": "成功向量数据之后不允许修改向量模型维度",
"dimensionOfVectorModel": "向量模型维度"
"dimensionOfVectorModel": "向量模型维度",
"managePermissionHint": "仅创建者或超级管理员可修改当前知识库"
}

View File

@@ -27,14 +27,26 @@ const props = defineProps({
type: String,
default: '',
},
manageable: {
type: Boolean,
default: true,
},
});
const dialogVisible = ref(false);
const pageDataRef = ref();
const handleEdit = (row: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
form.value = { id: row.id, content: row.content };
openDialog();
};
const handleDelete = (row: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('message.ok'),
cancelButtonText: $t('message.cancel'),
@@ -68,6 +80,10 @@ const queryParams = ref({
sortType: 'asc',
});
const save = () => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
btnLoading.value = true;
api.post('/api/v1/documentChunk/update', form.value).then((res: any) => {
btnLoading.value = false;
@@ -103,7 +119,12 @@ const form = ref({
:label="$t('documentCollection.content')"
min-width="240"
/>
<ElTableColumn :label="$t('common.handle')" width="100" align="right">
<ElTableColumn
v-if="props.manageable"
:label="$t('common.handle')"
width="100"
align="right"
>
<template #default="{ row }">
<div class="flex items-center gap-3">
<ElButton link type="primary" @click="handleEdit(row)">
@@ -130,6 +151,7 @@ const form = ref({
</template>
</PageData>
<EasyFlowFormModal
v-if="props.manageable"
v-model:open="dialogVisible"
:closable="!btnLoading"
:title="$t('button.edit')"

View File

@@ -2,7 +2,9 @@
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAccess } from '@easyflow/access';
import { $t } from '@easyflow/locales';
import { useUserStore } from '@easyflow/stores';
import { ArrowLeft, Plus } from '@element-plus/icons-vue';
import { ElIcon, ElImage } from 'element-plus';
@@ -20,11 +22,34 @@ import KnowledgeSearchConfig from '#/views/ai/documentCollection/KnowledgeSearch
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const { hasAccessByCodes } = useAccess();
const knowledgeId = ref<string>((route.query.id as string) || '');
const activeMenu = ref<string>((route.query.activeMenu as string) || '');
const knowledgeInfo = ref<any>({});
const selectedCategory = ref('');
const canManageKnowledgePermission = computed(() =>
hasAccessByCodes(['/api/v1/documentCollection/save']),
);
const isSuperAdmin = computed(() => {
return (
String(userStore.userInfo?.id || '') === '1' ||
(userStore.userRoles || []).includes('super_admin')
);
});
const canManageCurrentKnowledge = computed(() => {
if (!knowledgeInfo.value?.id || !canManageKnowledgePermission.value) {
return false;
}
return (
isSuperAdmin.value ||
String(userStore.userInfo?.id || '') ===
String(knowledgeInfo.value.createdBy || '')
);
});
const syncNavTitle = (title: string) => {
if (!title) {
@@ -163,6 +188,9 @@ const backDoc = () => {
<div class="description">
{{ knowledgeInfo.description || '' }}
</div>
<div v-if="!canManageCurrentKnowledge" class="permission-tip">
{{ $t('documentCollection.managePermissionHint') }}
</div>
</div>
<div class="doc-top-menu">
<button
@@ -184,7 +212,7 @@ const backDoc = () => {
<div class="doc-header" v-if="!viewDocVisible">
<HeaderSearch
v-if="!isFaqCollection"
:buttons="headerButtons"
:buttons="canManageCurrentKnowledge ? headerButtons : []"
@search="handleSearch"
@button-click="handleButtonClick"
/>
@@ -192,6 +220,7 @@ const backDoc = () => {
<DocumentTable
ref="documentTableRef"
:knowledge-id="knowledgeId"
:manageable="canManageCurrentKnowledge"
@view-doc="viewDoc"
v-if="!viewDocVisible"
/>
@@ -199,24 +228,32 @@ const backDoc = () => {
<ChunkDocumentTable
v-else
:document-id="documentId"
:manageable="canManageCurrentKnowledge"
:default-summary-prompt="knowledgeInfo.summaryPrompt"
/>
</div>
<div v-if="selectedCategory === 'faqList'" class="doc-table">
<FaqTable :knowledge-id="knowledgeId" />
<FaqTable
:knowledge-id="knowledgeId"
:manageable="canManageCurrentKnowledge"
/>
</div>
<!--知识检索-->
<div
v-if="selectedCategory === 'knowledgeSearch'"
class="doc-search-container"
>
<KnowledgeSearchConfig :document-collection-id="knowledgeId" />
<KnowledgeSearchConfig
:document-collection-id="knowledgeId"
:manageable="canManageCurrentKnowledge"
/>
<KnowledgeSearch :knowledge-id="knowledgeId" />
</div>
<!--配置-->
<div v-if="selectedCategory === 'config'">
<DocumentCollectionDataConfig
:detail-data="knowledgeInfo"
:manageable="canManageCurrentKnowledge"
@reload="getKnowledge"
/>
</div>
@@ -306,6 +343,13 @@ const backDoc = () => {
outline-offset: 1px;
}
.permission-tip {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--el-text-color-secondary);
}
.doc-table {
background-color: var(--el-bg-color);
}

View File

@@ -1,37 +1,123 @@
<script setup lang="ts">
import type {FormInstance} from 'element-plus';
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
import type { FormInstance } from 'element-plus';
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
import CardPage from '#/components/page/CardList.vue';
import type {
ActionButton,
CardPrimaryAction,
} from '#/components/page/CardList.vue';
import {computed, onMounted, ref} from 'vue';
import {useRouter} from 'vue-router';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import {EasyFlowFormModal} from '@easyflow/common-ui';
import {$t} from '@easyflow/locales';
import { useAccess } from '@easyflow/access';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { $t } from '@easyflow/locales';
import { useUserStore } from '@easyflow/stores';
import {Delete, Edit, Notebook, Plus, Search} from '@element-plus/icons-vue';
import {tryit} from 'radash';
import {
Check,
Delete,
Edit,
Lock,
Notebook,
OfficeBuilding,
Plus,
Promotion,
Search,
} from '@element-plus/icons-vue';
import {
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElPopover,
} from 'element-plus';
import { tryit } from 'radash';
import {api} from '#/api/request';
import { api } from '#/api/request';
import defaultIcon from '#/assets/ai/knowledge/book.svg';
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
import CardPage from '#/components/page/CardList.vue';
import PageData from '#/components/page/PageData.vue';
import PageSide from '#/components/page/PageSide.vue';
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
const router = useRouter();
const userStore = useUserStore();
const { hasAccessByCodes } = useAccess();
const collectionTypeLabelMap = {
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
FAQ: $t('documentCollection.collectionTypeFaq'),
};
type VisibilityScope = 'DEPT' | 'PRIVATE' | 'PUBLIC';
const canManageKnowledgePermission = computed(() =>
hasAccessByCodes(['/api/v1/documentCollection/save']),
);
const updatingScopeId = ref<null | number | string>(null);
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
const visibilityScopeMeta = computed(() => ({
PRIVATE: {
label: $t('documentCollection.visibilityScopePrivate'),
description: $t('documentCollection.visibilityScopePrivateDesc'),
icon: Lock,
tone: 'private',
},
DEPT: {
label: $t('documentCollection.visibilityScopeDept'),
description: $t('documentCollection.visibilityScopeDeptDesc'),
icon: OfficeBuilding,
tone: 'dept',
},
PUBLIC: {
label: $t('documentCollection.visibilityScopePublic'),
description: $t('documentCollection.visibilityScopePublicDesc'),
icon: Promotion,
tone: 'public',
},
}));
const visibilityScopeOptions = computed(() =>
(['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
value,
...visibilityScopeMeta.value[value],
})),
);
function isSuperAdmin() {
return (
String(userStore.userInfo?.id || '') === '1' ||
(userStore.userRoles || []).includes('super_admin')
);
}
function canManageKnowledgeItem(row: Record<string, any>) {
if (!canManageKnowledgePermission.value) {
return false;
}
const currentUserId = String(userStore.userInfo?.id || '');
return isSuperAdmin() || currentUserId === String(row?.createdBy || '');
}
function ensureManageKnowledgeItem(row: Record<string, any>) {
if (canManageKnowledgeItem(row)) {
return true;
}
ElMessage.warning($t('documentCollection.managePermissionHint'));
return false;
}
function resolveNavTitle(row: Record<string, any>) {
return row?.title || row?.name || '';
}
function openKnowledgeDetail(row: { id: string; name?: string; title?: string }) {
function openKnowledgeDetail(row: {
id: string;
name?: string;
title?: string;
}) {
router.push({
path: '/ai/documentCollection/document',
query: {
@@ -56,7 +142,7 @@ interface FieldDefinition {
const primaryAction: CardPrimaryAction = {
icon: Notebook,
text: $t('documentCollection.actions.knowledge'),
permission: '/api/v1/documentCollection/save',
permission: '/api/v1/documentCollection/query',
onClick(row) {
openKnowledgeDetail(row);
},
@@ -69,6 +155,9 @@ const actions: ActionButton[] = [
permission: '/api/v1/documentCollection/save',
placement: 'inline',
onClick(row) {
if (!ensureManageKnowledgeItem(row)) {
return;
}
aiKnowledgeModalRef.value.openDialog(row);
},
},
@@ -95,6 +184,9 @@ const actions: ActionButton[] = [
permission: '/api/v1/documentCollection/remove',
placement: 'inline',
onClick(row) {
if (!ensureManageKnowledgeItem(row)) {
return;
}
handleDelete(row);
},
},
@@ -178,6 +270,9 @@ const formRules = computed(() => {
const handleSearch = (params: any) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
};
const reloadKnowledgeList = () => {
pageDataRef.value?.reload?.();
};
const formData = ref<any>({});
const dialogVisible = ref(false);
const formRef = ref<FormInstance>();
@@ -189,7 +284,7 @@ function showControlDialog(item: any) {
const categoryList = ref<any[]>([]);
const getCategoryList = async () => {
const [, res] = await tryit(api.get)(
'/api/v1/documentCollectionCategory/list',
'/api/v1/documentCollectionCategory/visibleList',
{
params: { sortKey: 'sortNo', sortType: 'asc' },
},
@@ -258,6 +353,51 @@ const footerButton = {
},
};
const saveLoading = ref(false);
function resolveVisibilityScopeMeta(scope?: string) {
return (
visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
visibilityScopeMeta.value.PRIVATE
);
}
function setVisibilityScopePopoverRef(id: number | string, el: any) {
const cacheKey = String(id);
if (el) {
visibilityScopePopoverRefs.value[cacheKey] = el;
return;
}
delete visibilityScopePopoverRefs.value[cacheKey];
}
function closeVisibilityScopePopover(id: number | string) {
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
}
async function updateVisibilityScope(
row: any,
visibilityScope: VisibilityScope,
) {
if (
!canManageKnowledgeItem(row) ||
!row?.id ||
updatingScopeId.value === row.id ||
row.visibilityScope === visibilityScope
) {
closeVisibilityScopePopover(row.id);
return;
}
updatingScopeId.value = row.id;
try {
const res = await api.post('/api/v1/documentCollection/update', {
id: row.id,
visibilityScope,
});
if (res.errorCode === 0) {
row.visibilityScope = visibilityScope;
ElMessage.success($t('message.updateOkMessage'));
closeVisibilityScopePopover(row.id);
}
} finally {
updatingScopeId.value = null;
}
}
function handleSubmit() {
formRef.value?.validate((valid) => {
if (valid) {
@@ -318,7 +458,97 @@ function changeCategory(category: any) {
:actions="actions"
tag-field="collectionType"
:tag-map="collectionTypeLabelMap"
>
<template #corner="{ item }">
<ElPopover
v-if="canManageKnowledgeItem(item)"
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
trigger="click"
placement="bottom-end"
:width="208"
popper-class="knowledge-visibility-popover"
>
<template #reference>
<button
type="button"
class="knowledge-scope-chip"
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
:disabled="updatingScopeId === item.id"
@click.stop
>
<ElIcon class="knowledge-scope-chip__icon">
<component
:is="
resolveVisibilityScopeMeta(item.visibilityScope)
.icon
"
/>
</ElIcon>
<span class="knowledge-scope-chip__label">
{{
resolveVisibilityScopeMeta(item.visibilityScope).label
}}
</span>
</button>
</template>
<div class="knowledge-scope-panel" @click.stop>
<button
v-for="option in visibilityScopeOptions"
:key="option.value"
type="button"
class="knowledge-scope-option"
:class="[
`knowledge-scope-option--${option.tone}`,
{
'knowledge-scope-option--active':
item.visibilityScope === option.value,
},
]"
:disabled="updatingScopeId === item.id"
@click.stop="updateVisibilityScope(item, option.value)"
>
<span class="knowledge-scope-option__leading">
<span class="knowledge-scope-option__icon-wrap">
<ElIcon class="knowledge-scope-option__icon">
<component :is="option.icon" />
</ElIcon>
</span>
<span class="knowledge-scope-option__text">
<span class="knowledge-scope-option__label">
{{ option.label }}
</span>
<span class="knowledge-scope-option__desc">
{{ option.description }}
</span>
</span>
</span>
<ElIcon
v-if="item.visibilityScope === option.value"
class="knowledge-scope-option__check"
>
<Check />
</ElIcon>
</button>
</div>
</ElPopover>
<div
v-else
class="knowledge-scope-chip knowledge-scope-chip--readonly"
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
>
<ElIcon class="knowledge-scope-chip__icon">
<component
:is="
resolveVisibilityScopeMeta(item.visibilityScope).icon
"
/>
</ElIcon>
<span class="knowledge-scope-chip__label">
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
</span>
</div>
</template>
</CardPage>
</template>
</PageData>
</div>
@@ -362,7 +592,10 @@ function changeCategory(category: any) {
</EasyFlowFormModal>
<!-- 新增知识库模态框-->
<DocumentCollectionModal ref="aiKnowledgeModalRef" @reload="handleSearch" />
<DocumentCollectionModal
ref="aiKnowledgeModalRef"
@reload="reloadKnowledgeList"
/>
</div>
</template>
@@ -372,4 +605,206 @@ h1 {
margin-bottom: 30px;
color: #303133;
}
.knowledge-scope-chip {
display: inline-flex;
gap: 8px;
align-items: center;
min-height: 30px;
padding: 0 12px;
font-size: 12px;
font-weight: 600;
line-height: 1;
color: hsl(var(--text-strong));
background: hsl(var(--surface-subtle) / 0.92);
border: 1px solid hsl(var(--line-subtle));
border-radius: 999px;
transition:
border-color 0.18s ease,
background-color 0.18s ease,
color 0.18s ease,
transform 0.18s ease,
box-shadow 0.18s ease;
}
button.knowledge-scope-chip {
cursor: pointer;
}
button.knowledge-scope-chip:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
}
button.knowledge-scope-chip:focus-visible {
outline: none;
box-shadow:
0 0 0 4px hsl(var(--primary) / 0.12),
0 10px 22px -18px hsl(var(--foreground) / 0.32);
}
button.knowledge-scope-chip:disabled {
cursor: not-allowed;
opacity: 0.72;
transform: none;
}
.knowledge-scope-chip--readonly {
cursor: default;
}
.knowledge-scope-chip__icon {
font-size: 14px;
}
.knowledge-scope-chip__label {
white-space: nowrap;
}
.knowledge-scope-chip--private {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.09);
border-color: hsl(var(--primary) / 0.2);
}
.knowledge-scope-chip--dept {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
border-color: hsl(var(--warning) / 0.2);
}
.knowledge-scope-chip--public {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
border-color: hsl(var(--success) / 0.2);
}
.knowledge-scope-panel {
display: flex;
flex-direction: column;
gap: 2px;
}
.knowledge-scope-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 10px 8px;
text-align: left;
background: transparent;
border: none;
border-radius: 12px;
transition:
background-color 0.18s ease,
transform 0.18s ease,
color 0.18s ease;
}
.knowledge-scope-option:hover {
background: hsl(var(--foreground) / 0.04);
}
.knowledge-scope-option:focus-visible {
outline: none;
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
}
.knowledge-scope-option:disabled {
cursor: not-allowed;
opacity: 0.72;
}
.knowledge-scope-option__leading {
display: flex;
gap: 8px;
align-items: center;
min-width: 0;
}
.knowledge-scope-option__icon-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: transparent;
}
.knowledge-scope-option__icon {
font-size: 15px;
}
.knowledge-scope-option__text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.knowledge-scope-option__label {
font-size: 13px;
font-weight: 600;
line-height: 1.3;
color: hsl(var(--text-strong));
}
.knowledge-scope-option__desc {
font-size: 12px;
line-height: 1.4;
color: hsl(var(--text-muted));
}
.knowledge-scope-option__check {
font-size: 16px;
color: hsl(var(--primary));
}
.knowledge-scope-option--private .knowledge-scope-option__icon-wrap {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.1);
}
.knowledge-scope-option--dept .knowledge-scope-option__icon-wrap {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
}
.knowledge-scope-option--public .knowledge-scope-option__icon-wrap {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
}
.knowledge-scope-option--private.knowledge-scope-option--active {
background: hsl(var(--primary) / 0.08);
}
.knowledge-scope-option--dept.knowledge-scope-option--active {
background: hsl(var(--warning) / 0.08);
}
.knowledge-scope-option--public.knowledge-scope-option--active {
background: hsl(var(--success) / 0.08);
}
.knowledge-scope-option--private .knowledge-scope-option__check {
color: hsl(var(--primary));
}
.knowledge-scope-option--dept .knowledge-scope-option__check {
color: hsl(var(--warning));
}
.knowledge-scope-option--public .knowledge-scope-option__check {
color: hsl(var(--success));
}
:global(.knowledge-visibility-popover.el-popover.el-popper) {
padding: 8px;
border-radius: 16px;
border-color: hsl(var(--line-subtle));
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
}
</style>

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import type {FormInstance} from 'element-plus';
import type { FormInstance } from 'element-plus';
import { computed, onMounted, ref, watch } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElForm,
@@ -13,13 +17,9 @@ import {
ElTooltip,
} from 'element-plus';
import {computed, onMounted, ref, watch} from 'vue';
import {InfoFilled} from '@element-plus/icons-vue';
import {api} from '#/api/request';
import { api } from '#/api/request';
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
import {$t} from '#/locales';
import { $t } from '#/locales';
const props = defineProps({
detailData: {
@@ -45,7 +45,10 @@ const props = defineProps({
searchEngineEnable: false,
englishName: '',
}),
required: true,
},
manageable: {
type: Boolean,
default: true,
},
});
@@ -54,7 +57,7 @@ const emit = defineEmits(['reload']);
const normalizeEntity = (raw: any) => {
const options = {
canUpdateEmbeddingModel: true,
...(raw?.options || {}),
...raw?.options,
};
if (options.rerankEnable === undefined || options.rerankEnable === null) {
options.rerankEnable = !!raw?.rerankModelId;
@@ -102,7 +105,7 @@ const vectorStoreConfigPlaceholder = computed(() => {
const getEmbeddingLlmListData = async () => {
try {
const url = `/api/v1/model/list?modelType=embeddingModel`;
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
const res = await api.get(url, {});
if (res.errorCode === 0) {
embeddingLlmList.value = res.data;
@@ -115,7 +118,9 @@ const getEmbeddingLlmListData = async () => {
const getRerankerLlmListData = async () => {
try {
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
const res = await api.get(
'/api/v1/documentCollection/modelList?modelType=rerankModel',
);
rerankerLlmList.value = res.data;
} catch (error) {
ElMessage.error($t('message.apiError'));
@@ -156,6 +161,10 @@ const rules = ref({
});
async function save() {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
try {
const valid = await saveForm.value?.validate();
if (!valid) return;
@@ -190,9 +199,13 @@ async function save() {
label-width="150px"
ref="saveForm"
:model="entity"
:disabled="!props.manageable"
status-icon
:rules="rules"
>
<div v-if="!props.manageable" class="config-readonly-tip">
{{ $t('documentCollection.managePermissionHint') }}
</div>
<ElFormItem
prop="icon"
:label="$t('documentCollection.icon')"
@@ -371,7 +384,7 @@ async function save() {
type="primary"
@click="save"
:loading="btnLoading"
:disabled="btnLoading"
:disabled="btnLoading || !props.manageable"
>
{{ $t('button.save') }}
</ElButton>
@@ -385,4 +398,11 @@ async function save() {
height: 100%;
overflow: auto;
}
.config-readonly-tip {
margin-bottom: 16px;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
@@ -29,7 +29,7 @@ const rerankerLlmList = ref<any>([]);
const getEmbeddingLlmListData = async () => {
try {
const url = `/api/v1/model/list?modelType=embeddingModel`;
const url = `/api/v1/documentCollection/modelList?modelType=embeddingModel`;
const res = await api.get(url, {});
if (res.errorCode === 0) {
embeddingLlmList.value = res.data;
@@ -42,7 +42,9 @@ const getEmbeddingLlmListData = async () => {
const getRerankerLlmListData = async () => {
try {
const res = await api.get('/api/v1/model/list?modelType=rerankModel');
const res = await api.get(
'/api/v1/documentCollection/modelList?modelType=rerankModel',
);
rerankerLlmList.value = res.data;
} catch (error) {
ElMessage.error($t('message.apiError'));
@@ -87,8 +89,9 @@ const defaultEntity = {
rerankEnable: false,
},
rerankModelId: '',
searchEngineEnable: '',
searchEngineEnable: false,
englishName: '',
visibilityScope: 'PRIVATE',
};
const normalizeEntity = (raw: any = {}) => {
const options = {
@@ -135,6 +138,20 @@ const rules = ref({
{ required: true, message: $t('message.required'), trigger: 'blur' },
],
});
const visibilityScopeOptions = computed(() => [
{
label: $t('documentCollection.visibilityScopePrivate'),
value: 'PRIVATE',
},
{
label: $t('documentCollection.visibilityScopeDept'),
value: 'DEPT',
},
{
label: $t('documentCollection.visibilityScopePublic'),
value: 'PUBLIC',
},
]);
const collectionTypeList = [
{
label: $t('documentCollection.collectionTypeDocument'),
@@ -257,6 +274,19 @@ defineExpose({
dict-code="aiDocumentCollectionCategory"
/>
</ElFormItem>
<ElFormItem
prop="visibilityScope"
:label="$t('documentCollection.visibilityScope')"
>
<ElSelect v-model="entity.visibilityScope" class="w-full">
<ElOption
v-for="item in visibilityScopeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem prop="alias" :label="$t('documentCollection.alias')">
<ElInput v-model.trim="entity.alias" />
</ElFormItem>

View File

@@ -2,6 +2,7 @@
import { ref } from 'vue';
import { $t } from '@easyflow/locales';
import { downloadFileFromBlob } from '@easyflow/utils';
import { Delete, Download, MoreFilled } from '@element-plus/icons-vue';
import {
@@ -25,6 +26,10 @@ const props = defineProps({
required: true,
type: String,
},
manageable: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['viewDoc']);
defineExpose({
@@ -38,10 +43,20 @@ const pageDataRef = ref();
const handleView = (row: any) => {
emits('viewDoc', row.id);
};
const handleDownload = (row: any) => {
window.open(row.documentPath, '_blank');
const handleDownload = async (row: any) => {
const blob = await api.download(
`/api/v1/document/download?documentId=${row.id}`,
);
downloadFileFromBlob({
fileName: row.title || 'document',
source: blob,
});
};
const handleDelete = (row: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
@@ -122,7 +137,10 @@ const handleDelete = (row: any) => {
{{ $t('button.download') }}
</ElButton>
</ElDropdownItem>
<ElDropdownItem @click="handleDelete(row)">
<ElDropdownItem
v-if="props.manageable"
@click="handleDelete(row)"
>
<ElButton link :icon="Delete" type="danger">
{{ $t('button.delete') }}
</ElButton>

View File

@@ -23,9 +23,9 @@ import {
ElDropdown,
ElDropdownItem,
ElDropdownMenu,
ElInput,
ElMessage,
ElMessageBox,
ElInput,
ElTable,
ElTableColumn,
ElTree,
@@ -43,6 +43,10 @@ const props = defineProps({
type: String,
required: true,
},
manageable: {
type: Boolean,
default: true,
},
});
const pageDataRef = ref();
@@ -143,6 +147,10 @@ const handleResetSearch = () => {
};
const openAddDialog = () => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
editData.value = {
collectionId: props.knowledgeId,
categoryId:
@@ -189,12 +197,17 @@ const exportFaqExcel = async () => {
}
};
const handleImportSuccess = () => {
const handleImportSuccess = async () => {
await reloadCategoryTree();
refreshList();
};
const handleMoreActionCommand = (command: string) => {
if (command === 'import') {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
importDialogVisible.value = true;
return;
}
@@ -208,6 +221,10 @@ const handleMoreActionCommand = (command: string) => {
};
const openEditDialog = (row: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
editData.value = {
id: row.id,
collectionId: row.collectionId,
@@ -223,6 +240,10 @@ const openEditDialog = (row: any) => {
};
const saveFaq = async (payload: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
const url = payload.id ? '/api/v1/faqItem/update' : '/api/v1/faqItem/save';
const res = await api.post(url, payload);
if (res.errorCode === 0) {
@@ -237,6 +258,10 @@ const saveFaq = async (payload: any) => {
};
const removeFaq = (row: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
confirmButtonText: $t('button.confirm'),
cancelButtonText: $t('button.cancel'),
@@ -259,6 +284,10 @@ const handleCategoryClick = (data: any) => {
};
const openAddRootCategory = () => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
categoryDialogTitle.value = $t('documentCollection.faq.addCategory');
categoryDialogDisableParent.value = false;
categoryEditData.value = {
@@ -271,6 +300,10 @@ const openAddRootCategory = () => {
};
const openAddSiblingCategory = (node: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
categoryDialogTitle.value = $t('documentCollection.faq.addSiblingCategory');
categoryDialogDisableParent.value = false;
categoryEditData.value = {
@@ -286,6 +319,10 @@ const openAddSiblingCategory = (node: any) => {
};
const openAddChildCategory = (node: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
if (node.isDefault) {
ElMessage.warning(
$t('documentCollection.faq.defaultCategoryChildForbidden'),
@@ -308,6 +345,10 @@ const openAddChildCategory = (node: any) => {
};
const openEditCategory = (node: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
categoryDialogTitle.value = $t('documentCollection.faq.editCategory');
categoryDialogDisableParent.value = !!node.isDefault;
categoryEditData.value = {
@@ -324,6 +365,10 @@ const openEditCategory = (node: any) => {
};
const removeCategory = (node: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
if (node.isDefault) {
ElMessage.warning(
$t('documentCollection.faq.defaultCategoryDeleteForbidden'),
@@ -612,6 +657,10 @@ const demoteCategory = async (node: any) => {
};
const saveCategory = async (payload: any) => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
const url = payload.id
? '/api/v1/faqCategory/update'
: '/api/v1/faqCategory/save';
@@ -692,6 +741,7 @@ onMounted(() => {
<div class="faq-category-header">
<span>{{ $t('documentCollection.faq.categoryTree') }}</span>
<ElButton
v-if="props.manageable"
link
type="primary"
:icon="Plus"
@@ -719,7 +769,7 @@ onMounted(() => {
data.categoryName
}}</span>
<div
v-if="!data.isVirtual && !data.isDefault"
v-if="props.manageable && !data.isVirtual && !data.isDefault"
class="faq-category-node-actions"
@click.stop
>
@@ -770,7 +820,10 @@ onMounted(() => {
>
{{ $t('documentCollection.faq.addChildCategory') }}
</ElDropdownItem>
<ElDropdownItem :icon="Edit" @click="openEditCategory(data)">
<ElDropdownItem
:icon="Edit"
@click="openEditCategory(data)"
>
{{ $t('button.edit') }}
</ElDropdownItem>
<ElDropdownItem
@@ -791,6 +844,9 @@ onMounted(() => {
<div class="faq-content-pane">
<div class="faq-header">
<div v-if="!props.manageable" class="faq-readonly-tip">
{{ $t('documentCollection.managePermissionHint') }}
</div>
<div class="faq-toolbar">
<div class="faq-search-actions">
<ElInput
@@ -809,19 +865,25 @@ onMounted(() => {
</div>
<div class="faq-primary-actions">
<ElButton type="primary" :icon="Plus" @click="openAddDialog">
<ElButton
v-if="props.manageable"
type="primary"
:icon="Plus"
@click="openAddDialog"
>
{{ $t('button.add') }}
</ElButton>
<ElDropdown
trigger="click"
@command="handleMoreActionCommand"
>
<ElDropdown trigger="click" @command="handleMoreActionCommand">
<ElButton :icon="MoreFilled">
{{ $t('documentCollection.faq.import.moreActions') }}
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="import" :icon="Upload">
<ElDropdownItem
v-if="props.manageable"
command="import"
:icon="Upload"
>
{{ $t('button.import') }}
</ElDropdownItem>
<ElDropdownItem
@@ -871,6 +933,7 @@ onMounted(() => {
show-overflow-tooltip
/>
<ElTableColumn
v-if="props.manageable"
:label="$t('common.handle')"
width="170"
align="right"
@@ -1002,6 +1065,13 @@ onMounted(() => {
margin-bottom: 12px;
}
.faq-readonly-tip {
margin-bottom: 10px;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
.faq-toolbar {
display: flex;
align-items: center;
@@ -1062,8 +1132,11 @@ onMounted(() => {
}
:deep(
.faq-category-tree > .el-tree-node > .el-tree-node__content > .el-tree-node__expand-icon
) {
.faq-category-tree
> .el-tree-node
> .el-tree-node__content
> .el-tree-node__expand-icon
) {
display: none;
}
@@ -1077,7 +1150,9 @@ onMounted(() => {
background-color: var(--el-fill-color-light);
}
:deep(.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions) {
:deep(
.faq-category-tree .el-tree-node__content:hover .faq-category-node-actions
) {
opacity: 1;
}
@@ -1086,7 +1161,9 @@ onMounted(() => {
color: hsl(var(--primary));
}
:deep(.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions) {
:deep(
.el-tree-node.is-current > .el-tree-node__content .faq-category-node-actions
) {
opacity: 1;
}

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import {onMounted, reactive, ref} from 'vue';
import { onMounted, reactive, ref } from 'vue';
import {$t} from '@easyflow/locales';
import { $t } from '@easyflow/locales';
import {InfoFilled} from '@element-plus/icons-vue';
import { InfoFilled } from '@element-plus/icons-vue';
import {
ElButton,
ElForm,
@@ -16,13 +16,17 @@ import {
ElTooltip,
} from 'element-plus';
import {api} from '#/api/request';
import { api } from '#/api/request';
const props = defineProps({
documentCollectionId: {
type: String,
required: true,
},
manageable: {
type: Boolean,
default: true,
},
});
onMounted(() => {
@@ -55,6 +59,10 @@ const searchConfig = reactive({
});
const submitConfig = () => {
if (!props.manageable) {
ElMessage.warning($t('documentCollection.managePermissionHint'));
return;
}
const submitData = {
id: props.documentCollectionId,
options: {
@@ -89,6 +97,9 @@ const searchEngineOptions = [
},
];
const handleSearchEngineEnableChange = () => {
if (!props.manageable) {
return;
}
api.post('/api/v1/documentCollection/update', {
id: props.documentCollectionId,
searchEngineEnable: searchEngineEnable.value,
@@ -100,11 +111,15 @@ const handleSearchEngineEnableChange = () => {
<div class="search-config-sidebar">
<div class="config-header">
<h3>{{ $t('documentCollectionSearch.title') }}</h3>
<div v-if="!props.manageable" class="config-readonly-tip">
{{ $t('documentCollection.managePermissionHint') }}
</div>
</div>
<ElForm
class="config-form"
:model="searchConfig"
:disabled="!props.manageable"
label-width="100%"
size="small"
>
@@ -227,7 +242,12 @@ const handleSearchEngineEnableChange = () => {
</ElForm>
<div class="config-footer">
<ElButton type="primary" @click="submitConfig" class="submit-btn">
<ElButton
type="primary"
@click="submitConfig"
class="submit-btn"
:disabled="!props.manageable"
>
{{ $t('documentCollectionSearch.button.save') }}
</ElButton>
</div>
@@ -256,6 +276,13 @@ const handleSearchEngineEnableChange = () => {
font-weight: 600;
}
.config-readonly-tip {
margin-top: 8px;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
.config-form {
margin-bottom: 24px;
}

View File

@@ -1,20 +1,34 @@
<script setup lang="ts">
import type {FormInstance} from 'element-plus';
import {ElForm, ElFormItem, ElInput, ElInputNumber, ElMessage, ElMessageBox,} from 'element-plus';
import {
ElForm,
ElFormItem,
ElIcon,
ElInput,
ElInputNumber,
ElMessage,
ElMessageBox,
ElPopover,
} from 'element-plus';
import type {ActionButton, CardPrimaryAction,} from '#/components/page/CardList.vue';
import CardList from '#/components/page/CardList.vue';
import {computed, markRaw, onMounted, ref} from 'vue';
import {useAccess} from '@easyflow/access';
import {EasyFlowFormModal} from '@easyflow/common-ui';
import {
Check,
CopyDocument,
Delete,
Download,
Edit,
Lock,
OfficeBuilding,
Plus,
Promotion,
Tickets,
Upload,
VideoPlay,
@@ -47,6 +61,8 @@ interface FieldDefinition {
placeholder?: string;
}
type VisibilityScope = 'PRIVATE' | 'DEPT' | 'PUBLIC';
const primaryAction: CardPrimaryAction = {
icon: DesignIcon,
text: $t('button.design'),
@@ -56,6 +72,39 @@ const primaryAction: CardPrimaryAction = {
},
};
const {hasAccessByCodes} = useAccess();
const canManageWorkflow = computed(() =>
hasAccessByCodes(['/api/v1/workflow/save']),
);
const updatingScopeId = ref<string | number | null>(null);
const visibilityScopePopoverRefs = ref<Record<string, any>>({});
const visibilityScopeMeta = computed(() => ({
PRIVATE: {
label: $t('aiWorkflow.visibilityScopePrivate'),
description: $t('aiWorkflow.visibilityScopePrivateDesc'),
icon: Lock,
tone: 'private',
},
DEPT: {
label: $t('aiWorkflow.visibilityScopeDept'),
description: $t('aiWorkflow.visibilityScopeDeptDesc'),
icon: OfficeBuilding,
tone: 'dept',
},
PUBLIC: {
label: $t('aiWorkflow.visibilityScopePublic'),
description: $t('aiWorkflow.visibilityScopePublicDesc'),
icon: Promotion,
tone: 'public',
},
}));
const visibilityScopeOptions = computed(() => {
return (['PRIVATE', 'DEPT', 'PUBLIC'] as VisibilityScope[]).map((value) => ({
value,
...visibilityScopeMeta.value[value],
}));
});
const actions: ActionButton[] = [
{
icon: Edit,
@@ -96,6 +145,7 @@ const actions: ActionButton[] = [
{
icon: Download,
text: $t('button.export'),
permission: '/api/v1/workflow/save',
placement: 'menu',
onClick: (row: any) => {
exportJson(row);
@@ -104,6 +154,7 @@ const actions: ActionButton[] = [
{
icon: CopyDocument,
text: $t('button.copy'),
permission: '/api/v1/workflow/save',
placement: 'menu',
onClick: (row: any) => {
showDialog({
@@ -151,6 +202,50 @@ const headerButtons = [
function initDict() {
dictStore.fetchDictionary('dataStatus');
}
function resolveVisibilityScopeMeta(scope?: string) {
return visibilityScopeMeta.value[(scope || 'PRIVATE') as VisibilityScope] ||
visibilityScopeMeta.value.PRIVATE;
}
function setVisibilityScopePopoverRef(id: string | number, el: any) {
const cacheKey = String(id);
if (el) {
visibilityScopePopoverRefs.value[cacheKey] = el;
return;
}
delete visibilityScopePopoverRefs.value[cacheKey];
}
function closeVisibilityScopePopover(id: string | number) {
visibilityScopePopoverRefs.value[String(id)]?.hide?.();
}
async function updateVisibilityScope(
row: any,
visibilityScope: VisibilityScope,
) {
if (
!canManageWorkflow.value ||
!row?.id ||
updatingScopeId.value === row.id ||
row.visibilityScope === visibilityScope
) {
closeVisibilityScopePopover(row.id);
return;
}
updatingScopeId.value = row.id;
try {
const res = await api.post('/api/v1/workflow/update', {
id: row.id,
visibilityScope,
});
if (res.errorCode === 0) {
row.visibilityScope = visibilityScope;
ElMessage.success(res.message);
closeVisibilityScopePopover(row.id);
pageDataRef.value?.reload?.();
}
} finally {
updatingScopeId.value = null;
}
}
const handleSearch = (params: string) => {
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
};
@@ -341,7 +436,7 @@ function handleSubmit() {
});
}
const getSideList = async () => {
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/list', {
const [, res] = await tryit(api.get)('/api/v1/workflowCategory/visibleList', {
params: { sortKey: 'sortNo', sortType: 'asc' },
});
@@ -394,7 +489,90 @@ function handleHeaderButtonClick(data: any) {
:data="pageList"
:primary-action="primaryAction"
:actions="actions"
>
<template #corner="{ item }">
<ElPopover
v-if="canManageWorkflow"
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
trigger="click"
placement="bottom-end"
:width="208"
popper-class="workflow-visibility-popover"
>
<template #reference>
<button
type="button"
class="workflow-scope-chip"
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
:disabled="updatingScopeId === item.id"
@click.stop
>
<ElIcon class="workflow-scope-chip__icon">
<component
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
/>
</ElIcon>
<span class="workflow-scope-chip__label">
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
</span>
</button>
</template>
<div class="workflow-scope-panel" @click.stop>
<button
v-for="option in visibilityScopeOptions"
:key="option.value"
type="button"
class="workflow-scope-option"
:class="[
`workflow-scope-option--${option.tone}`,
{
'workflow-scope-option--active':
item.visibilityScope === option.value,
},
]"
:disabled="updatingScopeId === item.id"
@click.stop="updateVisibilityScope(item, option.value)"
>
<span class="workflow-scope-option__leading">
<span class="workflow-scope-option__icon-wrap">
<ElIcon class="workflow-scope-option__icon">
<component :is="option.icon" />
</ElIcon>
</span>
<span class="workflow-scope-option__text">
<span class="workflow-scope-option__label">
{{ option.label }}
</span>
<span class="workflow-scope-option__desc">
{{ option.description }}
</span>
</span>
</span>
<ElIcon
v-if="item.visibilityScope === option.value"
class="workflow-scope-option__check"
>
<Check />
</ElIcon>
</button>
</div>
</ElPopover>
<div
v-else
class="workflow-scope-chip workflow-scope-chip--readonly"
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
>
<ElIcon class="workflow-scope-chip__icon">
<component
:is="resolveVisibilityScopeMeta(item.visibilityScope).icon"
/>
</ElIcon>
<span class="workflow-scope-chip__label">
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
</span>
</div>
</template>
</CardList>
</template>
</PageData>
</div>
@@ -440,4 +618,205 @@ function handleHeaderButtonClick(data: any) {
</div>
</template>
<style scoped></style>
<style scoped>
.workflow-scope-chip {
display: inline-flex;
gap: 8px;
align-items: center;
min-height: 30px;
padding: 0 12px;
font-size: 12px;
font-weight: 600;
line-height: 1;
color: hsl(var(--text-strong));
background: hsl(var(--surface-subtle) / 0.92);
border: 1px solid hsl(var(--line-subtle));
border-radius: 999px;
transition:
border-color 0.18s ease,
background-color 0.18s ease,
color 0.18s ease,
transform 0.18s ease,
box-shadow 0.18s ease;
}
button.workflow-scope-chip {
cursor: pointer;
}
button.workflow-scope-chip:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 0.32);
}
button.workflow-scope-chip:focus-visible {
outline: none;
box-shadow:
0 0 0 4px hsl(var(--primary) / 0.12),
0 10px 22px -18px hsl(var(--foreground) / 0.32);
}
button.workflow-scope-chip:disabled {
cursor: not-allowed;
opacity: 0.72;
transform: none;
}
.workflow-scope-chip--readonly {
cursor: default;
}
.workflow-scope-chip__icon {
font-size: 14px;
}
.workflow-scope-chip__label {
white-space: nowrap;
}
.workflow-scope-chip--private {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.09);
border-color: hsl(var(--primary) / 0.2);
}
.workflow-scope-chip--dept {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
border-color: hsl(var(--warning) / 0.2);
}
.workflow-scope-chip--public {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
border-color: hsl(var(--success) / 0.2);
}
.workflow-scope-panel {
display: flex;
flex-direction: column;
gap: 2px;
}
.workflow-scope-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 10px 8px;
text-align: left;
background: transparent;
border: none;
border-radius: 12px;
transition:
background-color 0.18s ease,
transform 0.18s ease,
color 0.18s ease;
}
.workflow-scope-option:hover {
background: hsl(var(--foreground) / 0.04);
}
.workflow-scope-option:focus-visible {
outline: none;
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
}
.workflow-scope-option:disabled {
cursor: not-allowed;
opacity: 0.72;
}
.workflow-scope-option__leading {
display: flex;
gap: 8px;
align-items: center;
min-width: 0;
}
.workflow-scope-option__icon-wrap {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: transparent;
}
.workflow-scope-option__icon {
font-size: 15px;
}
.workflow-scope-option__text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.workflow-scope-option__label {
font-size: 13px;
font-weight: 600;
color: hsl(var(--text-strong));
}
.workflow-scope-option__desc {
font-size: 11px;
line-height: 1.35;
color: hsl(var(--text-muted));
}
.workflow-scope-option__check {
flex-shrink: 0;
font-size: 15px;
}
.workflow-scope-option--private .workflow-scope-option__icon-wrap {
color: hsl(var(--primary));
background: hsl(var(--primary) / 0.1);
}
.workflow-scope-option--dept .workflow-scope-option__icon-wrap {
color: hsl(var(--warning));
background: hsl(var(--warning) / 0.12);
}
.workflow-scope-option--public .workflow-scope-option__icon-wrap {
color: hsl(var(--success));
background: hsl(var(--success) / 0.12);
}
.workflow-scope-option--private.workflow-scope-option--active {
background: hsl(var(--primary) / 0.08);
}
.workflow-scope-option--dept.workflow-scope-option--active {
background: hsl(var(--warning) / 0.08);
}
.workflow-scope-option--public.workflow-scope-option--active {
background: hsl(var(--success) / 0.08);
}
.workflow-scope-option--private .workflow-scope-option__check {
color: hsl(var(--primary));
}
.workflow-scope-option--dept .workflow-scope-option__check {
color: hsl(var(--warning));
}
.workflow-scope-option--public .workflow-scope-option__check {
color: hsl(var(--success));
}
:global(.workflow-visibility-popover.el-popover.el-popper) {
padding: 8px;
border-radius: 16px;
border-color: hsl(var(--line-subtle));
box-shadow: 0 18px 34px -28px hsl(var(--foreground) / 0.2);
}
</style>

View File

@@ -5,7 +5,15 @@ import { computed, onMounted, ref } from 'vue';
import { EasyFlowFormModal } from '@easyflow/common-ui';
import { ElForm, ElFormItem, ElInput, ElMessage, ElUpload } from 'element-plus';
import {
ElForm,
ElFormItem,
ElInput,
ElMessage,
ElOption,
ElSelect,
ElUpload,
} from 'element-plus';
import { api } from '#/api/request';
import DictSelect from '#/components/dict/DictSelect.vue';
@@ -28,7 +36,7 @@ const isImport = ref(false);
const jsonFile = ref<any>(null);
const uploadFileList = ref<any[]>([]);
const uploadRef = ref<UploadInstance>();
const entity = ref<any>({
const createDefaultEntity = () => ({
alias: '',
deptId: '',
title: '',
@@ -36,8 +44,24 @@ const entity = ref<any>({
icon: '',
content: '',
englishName: '',
visibilityScope: 'PRIVATE',
});
const entity = ref<any>(createDefaultEntity());
const btnLoading = ref(false);
const visibilityScopeOptions = computed(() => [
{
label: $t('aiWorkflow.visibilityScopePrivate'),
value: 'PRIVATE',
},
{
label: $t('aiWorkflow.visibilityScopeDept'),
value: 'DEPT',
},
{
label: $t('aiWorkflow.visibilityScopePublic'),
value: 'PUBLIC',
},
]);
const jsonFileModel = computed({
get: () => (uploadFileList.value.length > 0 ? uploadFileList.value[0] : null),
set: (value: any) => {
@@ -57,10 +81,11 @@ const rules = computed(() => ({
// functions
function openDialog(row: any, importMode = false) {
isImport.value = importMode;
if (row.id) {
isAdd.value = false;
}
entity.value = row;
isAdd.value = !row?.id;
entity.value = {
...createDefaultEntity(),
...(row || {}),
};
dialogVisible.value = true;
}
@@ -137,7 +162,7 @@ function closeDialog() {
jsonFile.value = null;
isAdd.value = true;
isImport.value = false;
entity.value = {};
entity.value = createDefaultEntity();
dialogVisible.value = false;
}
</script>
@@ -198,6 +223,19 @@ function closeDialog() {
dict-code="aiWorkFlowCategory"
/>
</ElFormItem>
<ElFormItem
prop="visibilityScope"
:label="$t('aiWorkflow.visibilityScope')"
>
<ElSelect v-model="entity.visibilityScope" class="w-full">
<ElOption
v-for="item in visibilityScopeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem prop="alias" :label="$t('aiWorkflow.alias')">
<ElInput v-model.trim="entity.alias" />
</ElFormItem>