feat: 收敛AI资源发布审批生命周期
- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
@@ -19,6 +19,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|||||||
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||||
@@ -70,6 +71,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
@Resource
|
@Resource
|
||||||
private BotPublishAppService botPublishAppService;
|
private BotPublishAppService botPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
|
||||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||||
@@ -197,6 +200,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) {
|
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) {
|
||||||
throw new BusinessException("聊天助手尚未发布");
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
}
|
}
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
||||||
|
}
|
||||||
return Result.ok(bot);
|
return Result.ok(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,6 +227,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.getModelId() == null) {
|
if (data.getModelId() == null) {
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +238,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
|
|
||||||
if (llm == null) {
|
if (llm == null) {
|
||||||
data.setModelId(null);
|
data.setModelId(null);
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,19 +254,40 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StpUtil.isLogin()) {
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(data);
|
||||||
|
}
|
||||||
return Result.ok(data);
|
return Result.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/submitPublishApproval")
|
@PostMapping("/submitPublishApproval")
|
||||||
@SaCheckPermission("/api/v1/bot/save")
|
@SaCheckPermission("/api/v1/bot/save")
|
||||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(botPublishAppService.submitPublishApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/bot/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/submitDeleteApproval")
|
@PostMapping("/submitDeleteApproval")
|
||||||
@SaCheckPermission("/api/v1/bot/remove")
|
@SaCheckPermission("/api/v1/bot/remove")
|
||||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(botPublishAppService.submitDeleteApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
botPublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -262,13 +295,26 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
applyCategoryPermission(queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(service.list(queryWrapper));
|
List<Bot> bots = service.list(queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
||||||
|
return Result.ok(bots);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||||
applyCategoryPermission(queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
return super.queryPage(page, queryWrapper);
|
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
@@ -388,7 +434,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.fail(1, "请提交删除审批");
|
return Result.fail(1, "请使用发布状态操作删除聊天助手");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import tech.easyflow.ai.entity.DocumentCollection;
|
|||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.publish.KnowledgePublishAppService;
|
import tech.easyflow.ai.publish.KnowledgePublishAppService;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
@@ -71,6 +74,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
@Resource
|
@Resource
|
||||||
private KnowledgePublishAppService knowledgePublishAppService;
|
private KnowledgePublishAppService knowledgePublishAppService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
|
||||||
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -177,6 +182,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
)
|
)
|
||||||
public Result<DocumentCollection> detail(String id) {
|
public Result<DocumentCollection> detail(String id) {
|
||||||
DocumentCollection detail = service.getDetail(id);
|
DocumentCollection detail = service.getDetail(id);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(detail);
|
||||||
return Result.ok(detail);
|
return Result.ok(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +201,46 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
@PostMapping("/submitPublishApproval")
|
@PostMapping("/submitPublishApproval")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/save")
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(knowledgePublishAppService.submitPublishApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查知识库下线影响。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/offlineImpactCheck")
|
||||||
|
@SaCheckPermission("/api/v1/documentCollection/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.KNOWLEDGE,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.KNOWLEDGE_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理知识库"
|
||||||
|
)
|
||||||
|
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||||
|
return Result.ok(knowledgePublishAppService.checkOfflineImpact(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,7 +252,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
@PostMapping("/submitDeleteApproval")
|
@PostMapping("/submitDeleteApproval")
|
||||||
@SaCheckPermission("/api/v1/documentCollection/remove")
|
@SaCheckPermission("/api/v1/documentCollection/remove")
|
||||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(knowledgePublishAppService.submitDeleteApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
knowledgePublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("splitterProfile/save")
|
@PostMapping("splitterProfile/save")
|
||||||
@@ -250,20 +299,33 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(service.list(queryWrapper));
|
List<DocumentCollection> collections = service.list(queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(collections);
|
||||||
|
return Result.ok(collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
|
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
|
||||||
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
return super.queryPage(page, queryWrapper);
|
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.fail(1, "请提交删除审批");
|
return Result.fail(1, "请使用发布状态操作删除知识库");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
|
private void normalizeVisibilityScope(DocumentCollection entity, boolean isSave) {
|
||||||
|
|||||||
@@ -4,14 +4,19 @@ import cn.hutool.core.io.FileTypeUtil;
|
|||||||
import cn.hutool.http.HttpUtil;
|
import cn.hutool.http.HttpUtil;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.model.ai.ResourcePreviewVo;
|
||||||
import tech.easyflow.ai.entity.Resource;
|
import tech.easyflow.ai.entity.Resource;
|
||||||
import tech.easyflow.ai.service.ResourceService;
|
import tech.easyflow.ai.service.ResourceService;
|
||||||
|
import tech.easyflow.ai.utils.DocUtil;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
@@ -31,6 +36,8 @@ import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/resource")
|
@RequestMapping("/api/v1/resource")
|
||||||
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
|
||||||
|
private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000;
|
||||||
|
|
||||||
@javax.annotation.Resource
|
@javax.annotation.Resource
|
||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
@@ -79,6 +86,26 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
|||||||
return Result.ok(resource);
|
return Result.ok(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取素材预览内容。
|
||||||
|
*
|
||||||
|
* @param id 素材ID
|
||||||
|
* @return 预览文本
|
||||||
|
*/
|
||||||
|
@GetMapping("/previewContent")
|
||||||
|
public Result<ResourcePreviewVo> previewContent(String id) {
|
||||||
|
Resource resource = service.getById(id);
|
||||||
|
if (resource == null) {
|
||||||
|
throw new BusinessException("素材不存在");
|
||||||
|
}
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("RESOURCE", resource.getCreatedBy(), resource.getCategoryId(), "无权限访问素材");
|
||||||
|
|
||||||
|
byte[] bytes = DocUtil.downloadFile(resource.getResourceUrl());
|
||||||
|
String suffix = resolvePreviewSuffix(resource, bytes);
|
||||||
|
String content = DocUtil.readPreviewContent(suffix, new ByteArrayInputStream(bytes));
|
||||||
|
return Result.ok(buildPreviewVo(content));
|
||||||
|
}
|
||||||
|
|
||||||
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
private void applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
|
||||||
if (!access.isRestricted()) {
|
if (!access.isRestricted()) {
|
||||||
@@ -90,4 +117,37 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
|
|||||||
}
|
}
|
||||||
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
queryWrapper.and(RESOURCE.CREATED_BY.eq(access.getAccountId()).or(RESOURCE.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用于预览的文件后缀。
|
||||||
|
*
|
||||||
|
* @param resource 素材实体
|
||||||
|
* @param bytes 文件字节
|
||||||
|
* @return 标准化后的文件后缀
|
||||||
|
*/
|
||||||
|
private String resolvePreviewSuffix(Resource resource, byte[] bytes) {
|
||||||
|
if (StringUtils.hasText(resource.getSuffix())) {
|
||||||
|
return DocUtil.normalizeSuffix(resource.getSuffix());
|
||||||
|
}
|
||||||
|
String detectedSuffix = FileTypeUtil.getType(new ByteArrayInputStream(bytes), resource.getResourceUrl());
|
||||||
|
if (!StringUtils.hasText(detectedSuffix)) {
|
||||||
|
throw new BusinessException("无法识别当前素材文件类型");
|
||||||
|
}
|
||||||
|
return DocUtil.normalizeSuffix(detectedSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建预览返回对象,并在服务端截断超长内容。
|
||||||
|
*
|
||||||
|
* @param content 原始内容
|
||||||
|
* @return 预览内容对象
|
||||||
|
*/
|
||||||
|
private ResourcePreviewVo buildPreviewVo(String content) {
|
||||||
|
String safeContent = content == null ? "" : content;
|
||||||
|
boolean truncated = safeContent.length() > RESOURCE_PREVIEW_CONTENT_LIMIT;
|
||||||
|
ResourcePreviewVo vo = new ResourcePreviewVo();
|
||||||
|
vo.setContent(truncated ? safeContent.substring(0, RESOURCE_PREVIEW_CONTENT_LIMIT) : safeContent);
|
||||||
|
vo.setTruncated(truncated);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
|||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
import tech.easyflow.ai.publish.WorkflowPublishAppService;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.service.BotWorkflowService;
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
@@ -88,6 +91,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowPublishAppService workflowPublishAppService;
|
private WorkflowPublishAppService workflowPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
|
|
||||||
public WorkflowController(WorkflowService service, ModelService modelService) {
|
public WorkflowController(WorkflowService service, ModelService modelService) {
|
||||||
super(service);
|
super(service);
|
||||||
@@ -254,7 +259,46 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@PostMapping("/submitPublishApproval")
|
@PostMapping("/submitPublishApproval")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(workflowPublishAppService.submitPublishApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitPublishApproval(id),
|
||||||
|
"已提交发布审批",
|
||||||
|
"已直接发布"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitOfflineApproval(id),
|
||||||
|
"已提交下线审批",
|
||||||
|
"已直接下线"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查工作流下线影响。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
@GetMapping("/offlineImpactCheck")
|
||||||
|
@SaCheckPermission("/api/v1/workflow/save")
|
||||||
|
@RequireResourceAccess(
|
||||||
|
resource = CategoryResourceType.WORKFLOW,
|
||||||
|
action = ResourceAction.MANAGE,
|
||||||
|
lookup = ResourceLookup.WORKFLOW_ID,
|
||||||
|
idExpr = "#id",
|
||||||
|
denyMessage = "无权限管理工作流"
|
||||||
|
)
|
||||||
|
public Result<OfflineImpactCheckVo> offlineImpactCheck(@RequestParam BigInteger id) {
|
||||||
|
return Result.ok(workflowPublishAppService.checkOfflineImpact(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,7 +310,11 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@PostMapping("/submitDeleteApproval")
|
@PostMapping("/submitDeleteApproval")
|
||||||
@SaCheckPermission("/api/v1/workflow/remove")
|
@SaCheckPermission("/api/v1/workflow/remove")
|
||||||
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
return Result.ok(workflowPublishAppService.submitDeleteApproval(id));
|
return buildApprovalActionResult(
|
||||||
|
workflowPublishAppService.submitDeleteApproval(id),
|
||||||
|
"已提交删除审批",
|
||||||
|
"已直接删除"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/supportedCodeEngines")
|
@GetMapping("/supportedCodeEngines")
|
||||||
@@ -307,6 +355,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
)
|
)
|
||||||
public Result<Workflow> detail(String id) {
|
public Result<Workflow> detail(String id) {
|
||||||
Workflow workflow = service.getDetail(id);
|
Workflow workflow = service.getDetail(id);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(workflow);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,20 +417,33 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
return Result.ok(service.list(queryWrapper));
|
List<Workflow> workflows = service.list(queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(workflows);
|
||||||
|
return Result.ok(workflows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
|
||||||
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
|
||||||
applyPublishedOnlyFilter(queryWrapper);
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
return super.queryPage(page, queryWrapper);
|
Page<Workflow> result = super.queryPage(page, queryWrapper);
|
||||||
|
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostMapping("remove")
|
@PostMapping("remove")
|
||||||
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
|
||||||
return Result.fail(1, "请提交删除审批");
|
return Result.fail(1, "请使用发布状态操作删除工作流");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(
|
||||||
|
actionResult.isApprovalRequired() ? approvalMessage : directMessage,
|
||||||
|
actionResult.getInstanceId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package tech.easyflow.admin.model.ai;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材预览内容返回对象。
|
||||||
|
*/
|
||||||
|
public class ResourcePreviewVo {
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
private boolean truncated;
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTruncated() {
|
||||||
|
return truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTruncated(boolean truncated) {
|
||||||
|
this.truncated = truncated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.ai.entity;
|
package tech.easyflow.ai.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
import tech.easyflow.ai.entity.base.BotBase;
|
import tech.easyflow.ai.entity.base.BotBase;
|
||||||
import com.mybatisflex.annotation.Table;
|
import com.mybatisflex.annotation.Table;
|
||||||
|
|
||||||
@@ -19,6 +20,15 @@ public class Bot extends BotBase {
|
|||||||
public static final String KEY_MAX_MESSAGE_COUNT = "maxMessageCount";
|
public static final String KEY_MAX_MESSAGE_COUNT = "maxMessageCount";
|
||||||
public static final String KEY_ENABLE_DEEP_THINKING = "enableDeepThinking";
|
public static final String KEY_ENABLE_DEEP_THINKING = "enableDeepThinking";
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Boolean approvalPending;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String currentApprovalActionType;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String displayPublishStatus;
|
||||||
|
|
||||||
public boolean isAnonymousEnabled() {
|
public boolean isAnonymousEnabled() {
|
||||||
Map<String, Object> options = getOptions();
|
Map<String, Object> options = getOptions();
|
||||||
if (options == null) {
|
if (options == null) {
|
||||||
@@ -28,4 +38,28 @@ public class Bot extends BotBase {
|
|||||||
return o != null && (boolean) o;
|
return o != null && (boolean) o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getApprovalPending() {
|
||||||
|
return approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApprovalPending(Boolean approvalPending) {
|
||||||
|
this.approvalPending = approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentApprovalActionType() {
|
||||||
|
return currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||||
|
this.currentApprovalActionType = currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayPublishStatus() {
|
||||||
|
return displayPublishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||||
|
this.displayPublishStatus = displayPublishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tech.easyflow.ai.entity;
|
|||||||
import com.easyagents.core.model.chat.tool.Tool;
|
import com.easyagents.core.model.chat.tool.Tool;
|
||||||
import com.easyagents.core.store.DocumentStore;
|
import com.easyagents.core.store.DocumentStore;
|
||||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
import com.easyagents.store.milvus.MilvusVectorStore;
|
import com.easyagents.store.milvus.MilvusVectorStore;
|
||||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||||
import com.mybatisflex.annotation.Table;
|
import com.mybatisflex.annotation.Table;
|
||||||
@@ -27,6 +28,15 @@ import java.util.Map;
|
|||||||
@Table("tb_document_collection")
|
@Table("tb_document_collection")
|
||||||
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
public class DocumentCollection extends DocumentCollectionBase implements VisibilityResource {
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Boolean approvalPending;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String currentApprovalActionType;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String displayPublishStatus;
|
||||||
|
|
||||||
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
public static final String TYPE_DOCUMENT = "DOCUMENT";
|
||||||
public static final String TYPE_FAQ = "FAQ";
|
public static final String TYPE_FAQ = "FAQ";
|
||||||
|
|
||||||
@@ -135,4 +145,28 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
|||||||
}
|
}
|
||||||
return options.get(key);
|
return options.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getApprovalPending() {
|
||||||
|
return approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApprovalPending(Boolean approvalPending) {
|
||||||
|
this.approvalPending = approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentApprovalActionType() {
|
||||||
|
return currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||||
|
this.currentApprovalActionType = currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayPublishStatus() {
|
||||||
|
return displayPublishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||||
|
this.displayPublishStatus = displayPublishStatus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tech.easyflow.ai.entity;
|
package tech.easyflow.ai.entity;
|
||||||
|
|
||||||
import com.easyagents.core.model.chat.tool.Tool;
|
import com.easyagents.core.model.chat.tool.Tool;
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
import com.mybatisflex.annotation.Table;
|
import com.mybatisflex.annotation.Table;
|
||||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||||
import tech.easyflow.ai.entity.base.WorkflowBase;
|
import tech.easyflow.ai.entity.base.WorkflowBase;
|
||||||
@@ -16,6 +17,15 @@ import tech.easyflow.system.permission.resource.VisibilityResource;
|
|||||||
@Table("tb_workflow")
|
@Table("tb_workflow")
|
||||||
public class Workflow extends WorkflowBase implements VisibilityResource {
|
public class Workflow extends WorkflowBase implements VisibilityResource {
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Boolean approvalPending;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String currentApprovalActionType;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String displayPublishStatus;
|
||||||
|
|
||||||
public Tool toFunction(boolean needEnglishName) {
|
public Tool toFunction(boolean needEnglishName) {
|
||||||
return new WorkflowTool(this, needEnglishName);
|
return new WorkflowTool(this, needEnglishName);
|
||||||
}
|
}
|
||||||
@@ -23,4 +33,28 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
|
|||||||
public Tool toFunction(boolean needEnglishName, String definitionId) {
|
public Tool toFunction(boolean needEnglishName, String definitionId) {
|
||||||
return new WorkflowTool(this, needEnglishName, definitionId);
|
return new WorkflowTool(this, needEnglishName, definitionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getApprovalPending() {
|
||||||
|
return approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApprovalPending(Boolean approvalPending) {
|
||||||
|
this.approvalPending = approvalPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentApprovalActionType() {
|
||||||
|
return currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentApprovalActionType(String currentApprovalActionType) {
|
||||||
|
this.currentApprovalActionType = currentApprovalActionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayPublishStatus() {
|
||||||
|
return displayPublishStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayPublishStatus(String displayPublishStatus) {
|
||||||
|
this.displayPublishStatus = displayPublishStatus;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public enum PublishStatus {
|
|||||||
DRAFT("DRAFT"),
|
DRAFT("DRAFT"),
|
||||||
PUBLISH_PENDING("PUBLISH_PENDING"),
|
PUBLISH_PENDING("PUBLISH_PENDING"),
|
||||||
PUBLISHED("PUBLISHED"),
|
PUBLISHED("PUBLISHED"),
|
||||||
|
OFFLINE_PENDING("OFFLINE_PENDING"),
|
||||||
|
OFFLINE("OFFLINE"),
|
||||||
DELETE_PENDING("DELETE_PENDING");
|
DELETE_PENDING("DELETE_PENDING");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
@@ -34,7 +36,16 @@ public enum PublishStatus {
|
|||||||
* @return 允许外部访问或线上运行时返回 true
|
* @return 允许外部访问或线上运行时返回 true
|
||||||
*/
|
*/
|
||||||
public boolean isExternallyVisible() {
|
public boolean isExternallyVisible() {
|
||||||
return this == PUBLISHED || this == DELETE_PENDING;
|
return this == PUBLISHED || this == DELETE_PENDING || this == OFFLINE_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许作为 Bot 新绑定候选。
|
||||||
|
*
|
||||||
|
* @return 仅已发布状态返回 true
|
||||||
|
*/
|
||||||
|
public boolean isSelectableForBot() {
|
||||||
|
return this == PUBLISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源生命周期处理器抽象基类。
|
||||||
|
*
|
||||||
|
* @param <T> 资源类型
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAiResourceLifecycleHandler<T> implements ApprovalSubjectHandler, AiResourceLifecycleHandler {
|
||||||
|
|
||||||
|
protected static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||||
|
protected static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||||
|
|
||||||
|
private final ApprovalInstanceService approvalInstanceService;
|
||||||
|
protected final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
protected AbstractAiResourceLifecycleHandler(ApprovalInstanceService approvalInstanceService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.approvalInstanceService = approvalInstanceService;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载资源,不存在时抛异常。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @return 资源
|
||||||
|
*/
|
||||||
|
protected abstract T requireResource(BigInteger resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验资源管理权限。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
*/
|
||||||
|
protected abstract void assertManagePermission(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源分类 ID。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 分类 ID
|
||||||
|
*/
|
||||||
|
protected abstract BigInteger getCategoryId(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源部门 ID。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 部门 ID
|
||||||
|
*/
|
||||||
|
protected abstract BigInteger getDeptId(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源标题。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 资源标题
|
||||||
|
*/
|
||||||
|
protected abstract String getTitle(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前发布状态。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 发布状态
|
||||||
|
*/
|
||||||
|
protected abstract PublishStatus getCurrentStatus(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前已发布快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 已发布快照
|
||||||
|
*/
|
||||||
|
protected abstract Map<String, Object> getPublishedSnapshot(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建当前草稿快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @return 草稿快照
|
||||||
|
*/
|
||||||
|
protected abstract Map<String, Object> buildResourceSnapshot(T resource);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新资源待审批状态。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param publishStatus 发布状态
|
||||||
|
* @param currentApprovalInstanceId 审批实例 ID
|
||||||
|
*/
|
||||||
|
protected abstract void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布成功后写入已发布状态。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param resourceSnapshot 已发布快照
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
*/
|
||||||
|
protected abstract void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下线成功后写入下线状态。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
*/
|
||||||
|
protected abstract void markResourceOffline(BigInteger resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除资源。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
*/
|
||||||
|
protected abstract void removeResource(BigInteger resourceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回资源中文名称。
|
||||||
|
*
|
||||||
|
* @return 资源名称
|
||||||
|
*/
|
||||||
|
protected abstract String resourceLabel();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回当前动作摘要前缀。
|
||||||
|
*
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @return 摘要前缀
|
||||||
|
*/
|
||||||
|
protected String resolveActionLabel(ApprovalActionType actionType) {
|
||||||
|
return switch (actionType) {
|
||||||
|
case PUBLISH -> "发布";
|
||||||
|
case OFFLINE -> "下线";
|
||||||
|
case DELETE -> "删除";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扩展下线快照内容。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param snapshot 当前快照副本
|
||||||
|
*/
|
||||||
|
protected void enrichOfflineSnapshot(T resource, Map<String, Object> snapshot) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除前额外校验。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param currentStatus 当前状态
|
||||||
|
*/
|
||||||
|
protected void validateDelete(T resource, PublishStatus currentStatus) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下线成功后的额外副作用。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
*/
|
||||||
|
protected void afterOffline(BigInteger resourceId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除成功前的额外副作用。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
*/
|
||||||
|
protected void beforeRemove(BigInteger resourceId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||||
|
T resource = requireResource(resourceId);
|
||||||
|
assertManagePermission(resource);
|
||||||
|
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "存在未结束审批,请先处理完成");
|
||||||
|
}
|
||||||
|
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
||||||
|
PublishStatus currentStatus = getCurrentStatus(resource);
|
||||||
|
Map<String, Object> resourceSnapshot = resolveSubmitSnapshot(resource, approvalActionType, currentStatus);
|
||||||
|
|
||||||
|
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||||
|
request.setResourceType(resourceType());
|
||||||
|
request.setResourceId(resourceId);
|
||||||
|
request.setActionType(approvalActionType.getCode());
|
||||||
|
request.setApplicantId(operatorId);
|
||||||
|
request.setCategoryId(getCategoryId(resource));
|
||||||
|
request.setDeptId(getDeptId(resource));
|
||||||
|
request.setSummary(resolveActionLabel(approvalActionType) + resourceLabel() + ":" + getTitle(resource));
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||||
|
snapshot.put(SNAPSHOT_KEY, resourceSnapshot);
|
||||||
|
snapshot.put(PREVIOUS_STATUS_KEY, currentStatus.getCode());
|
||||||
|
request.setSnapshotJson(snapshot);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析动作对应的提交快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param currentStatus 当前状态
|
||||||
|
* @return 提交快照
|
||||||
|
*/
|
||||||
|
protected Map<String, Object> resolveSubmitSnapshot(T resource,
|
||||||
|
ApprovalActionType actionType,
|
||||||
|
PublishStatus currentStatus) {
|
||||||
|
return switch (actionType) {
|
||||||
|
case PUBLISH -> buildPublishSnapshot(resource, currentStatus);
|
||||||
|
case OFFLINE -> buildOfflineSnapshot(resource, currentStatus);
|
||||||
|
case DELETE -> buildDeleteSnapshot(resource, currentStatus);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建发布快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param currentStatus 当前状态
|
||||||
|
* @return 发布快照
|
||||||
|
*/
|
||||||
|
protected Map<String, Object> buildPublishSnapshot(T resource, PublishStatus currentStatus) {
|
||||||
|
if (currentStatus != PublishStatus.DRAFT
|
||||||
|
&& currentStatus != PublishStatus.OFFLINE
|
||||||
|
&& currentStatus != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "状态不允许发布");
|
||||||
|
}
|
||||||
|
Map<String, Object> snapshot = buildResourceSnapshot(resource);
|
||||||
|
if (currentStatus == PublishStatus.PUBLISHED && isSameSnapshot(snapshot, getPublishedSnapshot(resource))) {
|
||||||
|
throw new BusinessException("当前内容与已发布版本一致,无需重新发布");
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建下线快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param currentStatus 当前状态
|
||||||
|
* @return 下线快照
|
||||||
|
*/
|
||||||
|
protected Map<String, Object> buildOfflineSnapshot(T resource, PublishStatus currentStatus) {
|
||||||
|
if (currentStatus != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "尚未发布,无法下线");
|
||||||
|
}
|
||||||
|
Map<String, Object> publishedSnapshot = getPublishedSnapshot(resource);
|
||||||
|
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "缺少已发布快照,无法下线");
|
||||||
|
}
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>(publishedSnapshot);
|
||||||
|
enrichOfflineSnapshot(resource, snapshot);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建删除快照。
|
||||||
|
*
|
||||||
|
* @param resource 资源
|
||||||
|
* @param currentStatus 当前状态
|
||||||
|
* @return 删除快照
|
||||||
|
*/
|
||||||
|
protected Map<String, Object> buildDeleteSnapshot(T resource, PublishStatus currentStatus) {
|
||||||
|
if (currentStatus == PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "已发布,请先下线后再删除");
|
||||||
|
}
|
||||||
|
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||||
|
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||||
|
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||||
|
throw new BusinessException("当前" + resourceLabel() + "存在进行中的审批,请先处理完成");
|
||||||
|
}
|
||||||
|
validateDelete(resource, currentStatus);
|
||||||
|
return buildResourceSnapshot(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行审批通过后的真实动作。
|
||||||
|
*
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param resourceSnapshot 快照
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void applyApprovedAction(String actionType,
|
||||||
|
BigInteger resourceId,
|
||||||
|
Map<String, Object> resourceSnapshot,
|
||||||
|
BigInteger operatorId) {
|
||||||
|
ApprovalActionType normalizedAction = ApprovalActionType.from(actionType);
|
||||||
|
if (normalizedAction == ApprovalActionType.PUBLISH) {
|
||||||
|
publishResource(resourceId, resourceSnapshot, operatorId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (normalizedAction == ApprovalActionType.OFFLINE) {
|
||||||
|
markResourceOffline(resourceId);
|
||||||
|
afterOffline(resourceId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beforeRemove(resourceId);
|
||||||
|
removeResource(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId) {
|
||||||
|
persistResourceState(resourceId, publishStatus, instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void restoreState(BigInteger resourceId, PublishStatus previousStatus) {
|
||||||
|
persistResourceState(resourceId, previousStatus, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从请求快照读取资源内容。
|
||||||
|
*
|
||||||
|
* @param snapshotJson 请求快照
|
||||||
|
* @return 资源快照
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||||
|
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||||
|
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||||
|
throw new BusinessException("审批快照缺少" + resourceLabel() + "内容");
|
||||||
|
}
|
||||||
|
return new LinkedHashMap<>((Map<String, Object>) map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前草稿快照与已发布快照是否一致。
|
||||||
|
*
|
||||||
|
* @param currentSnapshot 当前快照
|
||||||
|
* @param publishedSnapshot 已发布快照
|
||||||
|
* @return 一致返回 true
|
||||||
|
*/
|
||||||
|
protected boolean isSameSnapshot(Map<String, Object> currentSnapshot, Map<String, Object> publishedSnapshot) {
|
||||||
|
if (publishedSnapshot == null || publishedSnapshot.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Objects.equals(
|
||||||
|
objectMapper.valueToTree(currentSnapshot),
|
||||||
|
objectMapper.valueToTree(publishedSnapshot)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源生命周期处理器。
|
||||||
|
*/
|
||||||
|
public interface AiResourceLifecycleHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前处理器支持的资源类型。
|
||||||
|
*
|
||||||
|
* @return 资源类型编码
|
||||||
|
*/
|
||||||
|
String resourceType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建动作提交请求。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @return 提交请求
|
||||||
|
*/
|
||||||
|
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入待审批状态。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param publishStatus 待写入的发布状态
|
||||||
|
* @param instanceId 审批实例 ID
|
||||||
|
*/
|
||||||
|
void updatePendingState(BigInteger resourceId, PublishStatus publishStatus, BigInteger instanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行动作真正生效后的资源更新与副作用。
|
||||||
|
*
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param resourceSnapshot 审批冻结快照
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
*/
|
||||||
|
void applyApprovedAction(String actionType, BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按提交前真实状态恢复资源状态。
|
||||||
|
*
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param previousStatus 提交前状态
|
||||||
|
*/
|
||||||
|
void restoreState(BigInteger resourceId, PublishStatus previousStatus);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源生命周期统一状态机服务。
|
||||||
|
*/
|
||||||
|
public interface AiResourceLifecycleService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交资源动作。
|
||||||
|
*
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param resourceId 资源 ID
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @return 执行结果
|
||||||
|
*/
|
||||||
|
ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源生命周期统一状态机实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AiResourceLifecycleServiceImpl implements AiResourceLifecycleService, ApprovalResultHandler {
|
||||||
|
|
||||||
|
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
||||||
|
private static final String PREVIOUS_STATUS_KEY = "previousPublishStatus";
|
||||||
|
|
||||||
|
private final List<AiResourceLifecycleHandler> handlers;
|
||||||
|
private final ApprovalMatchService approvalMatchService;
|
||||||
|
private final ApprovalInstanceService approvalInstanceService;
|
||||||
|
|
||||||
|
public AiResourceLifecycleServiceImpl(List<AiResourceLifecycleHandler> handlers,
|
||||||
|
ApprovalMatchService approvalMatchService,
|
||||||
|
ApprovalInstanceService approvalInstanceService) {
|
||||||
|
this.handlers = handlers;
|
||||||
|
this.approvalMatchService = approvalMatchService;
|
||||||
|
this.approvalInstanceService = approvalInstanceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public ApprovalActionResult submitAction(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
||||||
|
AiResourceLifecycleHandler handler = getHandler(resourceType);
|
||||||
|
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
||||||
|
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||||
|
if (flow == null) {
|
||||||
|
handler.applyApprovedAction(actionType, resourceId, readResourceSnapshot(request.getSnapshotJson()), operatorId);
|
||||||
|
return ApprovalActionResult.direct();
|
||||||
|
}
|
||||||
|
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||||
|
handler.updatePendingState(
|
||||||
|
resourceId,
|
||||||
|
resolveSubmittedStatus(actionType, resolvePreviousStatus(request.getSnapshotJson())),
|
||||||
|
instanceId
|
||||||
|
);
|
||||||
|
return ApprovalActionResult.required(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
|
getHandler(instance.getResourceType()).applyApprovedAction(
|
||||||
|
instance.getActionType(),
|
||||||
|
instance.getResourceId(),
|
||||||
|
readResourceSnapshot(instance.getSnapshotJson()),
|
||||||
|
operatorId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
|
getHandler(instance.getResourceType()).restoreState(
|
||||||
|
instance.getResourceId(),
|
||||||
|
resolvePreviousStatus(instance.getSnapshotJson())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
|
getHandler(instance.getResourceType()).restoreState(
|
||||||
|
instance.getResourceId(),
|
||||||
|
resolvePreviousStatus(instance.getSnapshotJson())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiResourceLifecycleHandler getHandler(String resourceType) {
|
||||||
|
return handlers.stream()
|
||||||
|
.filter(item -> item.resourceType().equals(resourceType))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new BusinessException("未找到资源生命周期处理器: " + resourceType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析提交动作后的真实持久化状态。
|
||||||
|
*
|
||||||
|
* @param actionType 动作类型
|
||||||
|
* @param previousStatus 提交前真实状态
|
||||||
|
* @return 提交后应写入的状态
|
||||||
|
*/
|
||||||
|
private PublishStatus resolveSubmittedStatus(String actionType, PublishStatus previousStatus) {
|
||||||
|
if (ApprovalActionType.PUBLISH.getCode().equals(actionType) && previousStatus == PublishStatus.PUBLISHED) {
|
||||||
|
return PublishStatus.PUBLISHED;
|
||||||
|
}
|
||||||
|
if (ApprovalActionType.DELETE.getCode().equals(actionType)) {
|
||||||
|
return PublishStatus.DELETE_PENDING;
|
||||||
|
}
|
||||||
|
if (ApprovalActionType.OFFLINE.getCode().equals(actionType)) {
|
||||||
|
return PublishStatus.OFFLINE_PENDING;
|
||||||
|
}
|
||||||
|
return PublishStatus.PUBLISH_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从审批快照解析提交前真实状态。
|
||||||
|
*
|
||||||
|
* @param snapshotJson 审批冻结快照
|
||||||
|
* @return 提交前真实状态
|
||||||
|
*/
|
||||||
|
private PublishStatus resolvePreviousStatus(Map<String, Object> snapshotJson) {
|
||||||
|
Object status = snapshotJson == null ? null : snapshotJson.get(PREVIOUS_STATUS_KEY);
|
||||||
|
if (status instanceof String value && !value.isBlank()) {
|
||||||
|
return PublishStatus.from(value);
|
||||||
|
}
|
||||||
|
return PublishStatus.DRAFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从审批快照中提取冻结资源内容。
|
||||||
|
*
|
||||||
|
* @param snapshotJson 审批冻结快照
|
||||||
|
* @return 冻结资源内容
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> readResourceSnapshot(Map<String, Object> snapshotJson) {
|
||||||
|
Object snapshot = snapshotJson == null ? null : snapshotJson.get(SNAPSHOT_KEY);
|
||||||
|
if (!(snapshot instanceof Map<?, ?> map)) {
|
||||||
|
throw new BusinessException("审批快照缺少资源内容");
|
||||||
|
}
|
||||||
|
return (Map<String, Object>) map;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.ai.entity.Bot;
|
import tech.easyflow.ai.entity.Bot;
|
||||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
|
||||||
import tech.easyflow.ai.entity.BotCategory;
|
import tech.easyflow.ai.entity.BotCategory;
|
||||||
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
import tech.easyflow.ai.entity.BotMcp;
|
import tech.easyflow.ai.entity.BotMcp;
|
||||||
import tech.easyflow.ai.entity.BotPlugin;
|
import tech.easyflow.ai.entity.BotPlugin;
|
||||||
import tech.easyflow.ai.entity.BotWorkflow;
|
import tech.easyflow.ai.entity.BotWorkflow;
|
||||||
@@ -26,35 +26,27 @@ import tech.easyflow.ai.service.McpService;
|
|||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
import tech.easyflow.ai.service.PluginItemService;
|
import tech.easyflow.ai.service.PluginItemService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
|
||||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
|
||||||
import tech.easyflow.system.entity.SysDept;
|
import tech.easyflow.system.entity.SysDept;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
import tech.easyflow.system.service.SysDeptService;
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 聊天助手审批处理器。
|
* 聊天助手生命周期处理器。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
public class BotApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Bot> {
|
||||||
|
|
||||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
|
||||||
|
|
||||||
private final BotService botService;
|
private final BotService botService;
|
||||||
private final BotWorkflowService botWorkflowService;
|
private final BotWorkflowService botWorkflowService;
|
||||||
@@ -63,7 +55,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
private final BotMcpService botMcpService;
|
private final BotMcpService botMcpService;
|
||||||
private final WorkflowService workflowService;
|
private final WorkflowService workflowService;
|
||||||
private final DocumentCollectionService documentCollectionService;
|
private final DocumentCollectionService documentCollectionService;
|
||||||
private final ApprovalInstanceService approvalInstanceService;
|
|
||||||
private final CategoryPermissionService categoryPermissionService;
|
private final CategoryPermissionService categoryPermissionService;
|
||||||
private final ModelService modelService;
|
private final ModelService modelService;
|
||||||
private final BotCategoryService botCategoryService;
|
private final BotCategoryService botCategoryService;
|
||||||
@@ -84,7 +75,9 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
BotCategoryService botCategoryService,
|
BotCategoryService botCategoryService,
|
||||||
SysDeptService sysDeptService,
|
SysDeptService sysDeptService,
|
||||||
PluginItemService pluginItemService,
|
PluginItemService pluginItemService,
|
||||||
McpService mcpService) {
|
McpService mcpService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
super(approvalInstanceService, objectMapper);
|
||||||
this.botService = botService;
|
this.botService = botService;
|
||||||
this.botWorkflowService = botWorkflowService;
|
this.botWorkflowService = botWorkflowService;
|
||||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||||
@@ -92,7 +85,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
this.botMcpService = botMcpService;
|
this.botMcpService = botMcpService;
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.documentCollectionService = documentCollectionService;
|
this.documentCollectionService = documentCollectionService;
|
||||||
this.approvalInstanceService = approvalInstanceService;
|
|
||||||
this.categoryPermissionService = categoryPermissionService;
|
this.categoryPermissionService = categoryPermissionService;
|
||||||
this.modelService = modelService;
|
this.modelService = modelService;
|
||||||
this.botCategoryService = botCategoryService;
|
this.botCategoryService = botCategoryService;
|
||||||
@@ -106,69 +98,6 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return ApprovalResourceType.BOT.getCode();
|
return ApprovalResourceType.BOT.getCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
|
||||||
Bot bot = requireBot(resourceId);
|
|
||||||
assertManagePermission(bot);
|
|
||||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
|
||||||
throw new BusinessException("当前聊天助手存在未结束审批,请先处理完成");
|
|
||||||
}
|
|
||||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
|
||||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(bot);
|
|
||||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
|
||||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(bot.getPublishedSnapshotJson()))) {
|
|
||||||
throw new BusinessException("当前聊天助手没有变更,无需重复发布");
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
|
||||||
request.setResourceType(resourceType());
|
|
||||||
request.setResourceId(resourceId);
|
|
||||||
request.setActionType(approvalActionType.getCode());
|
|
||||||
request.setApplicantId(operatorId);
|
|
||||||
request.setCategoryId(bot.getCategoryId());
|
|
||||||
request.setDeptId(bot.getDeptId());
|
|
||||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "聊天助手:" + bot.getTitle());
|
|
||||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
|
||||||
Bot update = new Bot();
|
|
||||||
update.setId(context.getResourceId());
|
|
||||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
|
||||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
|
||||||
botService.updateById(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApproved(ApprovalCallbackContext context) {
|
|
||||||
ApprovalInstance instance = context.getInstance();
|
|
||||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
|
||||||
Bot update = new Bot();
|
|
||||||
update.setId(instance.getResourceId());
|
|
||||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
|
||||||
update.setPublishedAt(new Date());
|
|
||||||
update.setPublishedBy(context.getOperatorId());
|
|
||||||
botService.updateById(update);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeBotRelations(instance.getResourceId());
|
|
||||||
botService.removeById(instance.getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRejected(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoked(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||||
Bot bot = botService.getDetail(String.valueOf(identifier));
|
Bot bot = botService.getDetail(String.valueOf(identifier));
|
||||||
@@ -178,24 +107,52 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bot requireBot(BigInteger id) {
|
@Override
|
||||||
Bot bot = botService.getById(id);
|
protected Bot requireResource(BigInteger resourceId) {
|
||||||
|
Bot bot = botService.getById(resourceId);
|
||||||
if (bot == null) {
|
if (bot == null) {
|
||||||
throw new BusinessException("聊天助手不存在");
|
throw new BusinessException("聊天助手不存在");
|
||||||
}
|
}
|
||||||
return bot;
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertManagePermission(Bot bot) {
|
@Override
|
||||||
|
protected void assertManagePermission(Bot resource) {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin();
|
boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin();
|
||||||
boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy());
|
boolean creator = account != null && account.getId() != null && account.getId().equals(resource.getCreatedBy());
|
||||||
if (!superAdmin && !creator) {
|
if (!superAdmin && !creator) {
|
||||||
throw new BusinessException("仅创建者或超级管理员可管理聊天助手");
|
throw new BusinessException("仅创建者或超级管理员可管理聊天助手");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildResourceSnapshot(Bot bot) {
|
@Override
|
||||||
|
protected BigInteger getCategoryId(Bot resource) {
|
||||||
|
return resource.getCategoryId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BigInteger getDeptId(Bot resource) {
|
||||||
|
return resource.getDeptId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTitle(Bot resource) {
|
||||||
|
return resource.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PublishStatus getCurrentStatus(Bot resource) {
|
||||||
|
return PublishStatus.from(resource.getPublishStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> getPublishedSnapshot(Bot resource) {
|
||||||
|
return resource.getPublishedSnapshotJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> buildResourceSnapshot(Bot bot) {
|
||||||
Model model = resolveModel(bot.getModelId());
|
Model model = resolveModel(bot.getModelId());
|
||||||
BotCategory category = resolveCategory(bot.getCategoryId());
|
BotCategory category = resolveCategory(bot.getCategoryId());
|
||||||
SysDept dept = resolveDept(bot.getDeptId());
|
SysDept dept = resolveDept(bot.getDeptId());
|
||||||
@@ -228,13 +185,59 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||||
|
Bot update = new Bot();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(publishStatus.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||||
|
botService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||||
|
Bot update = new Bot();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||||
|
update.setPublishedAt(new java.util.Date());
|
||||||
|
update.setPublishedBy(operatorId);
|
||||||
|
botService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void markResourceOffline(BigInteger resourceId) {
|
||||||
|
Bot update = new Bot();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
botService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeResource(BigInteger resourceId) {
|
||||||
|
botService.removeById(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String resourceLabel() {
|
||||||
|
return "聊天助手";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeRemove(BigInteger resourceId) {
|
||||||
|
removeBotRelations(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
private List<Map<String, Object>> buildWorkflowBindings(BigInteger botId) {
|
private List<Map<String, Object>> buildWorkflowBindings(BigInteger botId) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId);
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId);
|
||||||
List<BotWorkflow> relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
List<BotWorkflow> relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
|
relations.sort(Comparator.comparing(BotWorkflow::getWorkflowId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
for (BotWorkflow relation : relations) {
|
for (BotWorkflow relation : relations) {
|
||||||
Workflow workflow = relation.getWorkflow();
|
Workflow workflow = relation.getWorkflow();
|
||||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
|
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isSelectableForBot()) {
|
||||||
throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手");
|
throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手");
|
||||||
}
|
}
|
||||||
Map<String, Object> item = new LinkedHashMap<>();
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
@@ -248,10 +251,11 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
private List<Map<String, Object>> buildKnowledgeBindings(BigInteger botId) {
|
private List<Map<String, Object>> buildKnowledgeBindings(BigInteger botId) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId);
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId);
|
||||||
List<BotDocumentCollection> relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
List<BotDocumentCollection> relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper);
|
||||||
|
relations.sort(Comparator.comparing(BotDocumentCollection::getDocumentCollectionId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
for (BotDocumentCollection relation : relations) {
|
for (BotDocumentCollection relation : relations) {
|
||||||
DocumentCollection knowledge = relation.getKnowledge();
|
DocumentCollection knowledge = relation.getKnowledge();
|
||||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
|
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isSelectableForBot()) {
|
||||||
throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手");
|
throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手");
|
||||||
}
|
}
|
||||||
Map<String, Object> item = new LinkedHashMap<>();
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
@@ -266,6 +270,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
private List<Map<String, Object>> buildPluginBindings(BigInteger botId) {
|
private List<Map<String, Object>> buildPluginBindings(BigInteger botId) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId);
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId);
|
||||||
List<BotPlugin> relations = botPluginService.list(queryWrapper);
|
List<BotPlugin> relations = botPluginService.list(queryWrapper);
|
||||||
|
relations.sort(Comparator.comparing(BotPlugin::getPluginItemId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
for (BotPlugin relation : relations) {
|
for (BotPlugin relation : relations) {
|
||||||
PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId());
|
PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId());
|
||||||
@@ -274,7 +279,7 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
}
|
}
|
||||||
Map<String, Object> item = new LinkedHashMap<>();
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
item.put("pluginItemId", relation.getPluginItemId());
|
item.put("pluginItemId", relation.getPluginItemId());
|
||||||
item.put("pluginItemName", resolvePluginName(pluginItem));
|
item.put("pluginItemName", pluginItem.getName());
|
||||||
result.add(item);
|
result.add(item);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -283,156 +288,85 @@ public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
private List<Map<String, Object>> buildMcpBindings(BigInteger botId) {
|
private List<Map<String, Object>> buildMcpBindings(BigInteger botId) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId);
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId);
|
||||||
List<BotMcp> relations = botMcpService.list(queryWrapper);
|
List<BotMcp> relations = botMcpService.list(queryWrapper);
|
||||||
|
relations.sort(Comparator.comparing(BotMcp::getMcpId, Comparator.nullsLast(BigInteger::compareTo)));
|
||||||
List<Map<String, Object>> result = new ArrayList<>();
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
for (BotMcp relation : relations) {
|
for (BotMcp relation : relations) {
|
||||||
Mcp mcp = mcpService.getById(relation.getMcpId());
|
Mcp mcp = mcpService.getById(relation.getMcpId());
|
||||||
if (mcp == null) {
|
if (mcp == null) {
|
||||||
throw new BusinessException("聊天助手绑定的MCP不存在,无法发布聊天助手");
|
throw new BusinessException("聊天助手绑定的 MCP 不存在,无法发布聊天助手");
|
||||||
}
|
}
|
||||||
Map<String, Object> item = new LinkedHashMap<>();
|
Map<String, Object> item = new LinkedHashMap<>();
|
||||||
item.put("mcpId", relation.getMcpId());
|
item.put("mcpId", relation.getMcpId());
|
||||||
item.put("mcpName", mcp.getTitle());
|
item.put("mcpName", mcp.getTitle());
|
||||||
item.put("mcpToolName", relation.getMcpToolName());
|
item.put("mcpToolName", relation.getMcpToolName());
|
||||||
item.put("mcpToolDescription", relation.getMcpToolDescription());
|
|
||||||
result.add(item);
|
result.add(item);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析聊天助手分类。
|
|
||||||
*
|
|
||||||
* @param categoryId 分类 ID
|
|
||||||
* @return 分类实体,不存在时返回 {@code null}
|
|
||||||
*/
|
|
||||||
private BotCategory resolveCategory(BigInteger categoryId) {
|
|
||||||
if (categoryId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return botCategoryService.getById(categoryId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析部门信息。
|
|
||||||
*
|
|
||||||
* @param deptId 部门 ID
|
|
||||||
* @return 部门实体,不存在时返回 {@code null}
|
|
||||||
*/
|
|
||||||
private SysDept resolveDept(BigInteger deptId) {
|
|
||||||
if (deptId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return sysDeptService.getById(deptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析聊天模型。
|
|
||||||
*
|
|
||||||
* @param modelId 模型 ID
|
|
||||||
* @return 模型实体
|
|
||||||
*/
|
|
||||||
private Model resolveModel(BigInteger modelId) {
|
|
||||||
if (modelId == null) {
|
|
||||||
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
|
||||||
}
|
|
||||||
Model model = modelService.getById(modelId);
|
|
||||||
if (model == null) {
|
|
||||||
throw new BusinessException("聊天助手绑定的模型不存在,无法提交审批");
|
|
||||||
}
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成模型展示名称。
|
|
||||||
*
|
|
||||||
* @param model 模型实体
|
|
||||||
* @return 模型名称
|
|
||||||
*/
|
|
||||||
private String resolveModelName(Model model) {
|
|
||||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
|
||||||
return model.getTitle();
|
|
||||||
}
|
|
||||||
return model.getModelName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取系统提示词。
|
|
||||||
*
|
|
||||||
* @param bot 聊天助手
|
|
||||||
* @return 系统提示词
|
|
||||||
*/
|
|
||||||
private String resolveSystemPrompt(Bot bot) {
|
|
||||||
if (bot.getModelOptions() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Object prompt = bot.getModelOptions().get(Bot.KEY_SYSTEM_PROMPT);
|
|
||||||
return prompt == null ? null : String.valueOf(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取数值配置项。
|
|
||||||
*
|
|
||||||
* @param options 配置 map
|
|
||||||
* @param key 配置键
|
|
||||||
* @return 数值配置,不存在时返回 {@code null}
|
|
||||||
*/
|
|
||||||
private Number readNumberOption(Map<String, Object> options, String key) {
|
|
||||||
if (options == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Object value = options.get(key);
|
|
||||||
if (value instanceof Number number) {
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成插件工具展示名称。
|
|
||||||
*
|
|
||||||
* @param pluginItem 插件工具实体
|
|
||||||
* @return 展示名称
|
|
||||||
*/
|
|
||||||
private String resolvePluginName(PluginItem pluginItem) {
|
|
||||||
if (pluginItem.getName() != null && !pluginItem.getName().isBlank()) {
|
|
||||||
return pluginItem.getName();
|
|
||||||
}
|
|
||||||
return pluginItem.getEnglishName();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
|
||||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
|
||||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
|
||||||
throw new BusinessException("审批快照缺少聊天助手发布内容");
|
|
||||||
}
|
|
||||||
return (Map<String, Object>) map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublishStatus resolvePendingStatus(String actionType) {
|
|
||||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
|
||||||
? PublishStatus.DELETE_PENDING
|
|
||||||
: PublishStatus.PUBLISH_PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearPendingStatus(BigInteger botId) {
|
|
||||||
Bot bot = botService.getById(botId);
|
|
||||||
if (bot == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Bot update = new Bot();
|
|
||||||
update.setId(botId);
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishStatus(bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()
|
|
||||||
? PublishStatus.DRAFT.getCode()
|
|
||||||
: PublishStatus.PUBLISHED.getCode());
|
|
||||||
botService.updateById(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeBotRelations(BigInteger botId) {
|
private void removeBotRelations(BigInteger botId) {
|
||||||
botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
|
botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
|
||||||
botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
|
botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
|
||||||
botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
|
botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
|
||||||
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
|
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BotCategory resolveCategory(BigInteger categoryId) {
|
||||||
|
if (categoryId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return botCategoryService.getById(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysDept resolveDept(BigInteger deptId) {
|
||||||
|
if (deptId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sysDeptService.getById(deptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Model resolveModel(BigInteger modelId) {
|
||||||
|
if (modelId == null) {
|
||||||
|
throw new BusinessException("聊天助手未配置模型,无法提交审批");
|
||||||
|
}
|
||||||
|
Model model = modelService.getById(modelId);
|
||||||
|
if (model == null) {
|
||||||
|
throw new BusinessException("聊天助手关联模型不存在,无法提交审批");
|
||||||
|
}
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveModelName(Model model) {
|
||||||
|
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||||
|
if (providerName == null || providerName.isBlank()) {
|
||||||
|
return model.getModelName();
|
||||||
|
}
|
||||||
|
return providerName + " / " + model.getModelName();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSystemPrompt(Bot bot) {
|
||||||
|
if (bot.getModelOptions() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object value = bot.getModelOptions().get("systemPrompt");
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Number readNumberOption(Map<String, Object> options, String key) {
|
||||||
|
if (options == null || !options.containsKey(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object value = options.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
if (value instanceof String string && !string.isBlank()) {
|
||||||
|
try {
|
||||||
|
return Double.valueOf(string);
|
||||||
|
} catch (NumberFormatException ignore) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
@@ -12,36 +15,58 @@ import java.math.BigInteger;
|
|||||||
@Service
|
@Service
|
||||||
public class BotPublishAppService {
|
public class BotPublishAppService {
|
||||||
|
|
||||||
|
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||||
|
|
||||||
|
public BotPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||||
|
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交聊天助手发布审批。
|
* 提交聊天助手发布审批。
|
||||||
*
|
*
|
||||||
* @param id 助手 ID
|
* @param id 助手 ID
|
||||||
* @return 助手 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||||
resourceType = "BOT",
|
|
||||||
actionType = "PUBLISH",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitPublishApproval(BigInteger id) {
|
|
||||||
assertId(id);
|
assertId(id);
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.BOT.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.PUBLISH.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交聊天助手下线审批。
|
||||||
|
*
|
||||||
|
* @param id 助手 ID
|
||||||
|
* @return 动作执行结果
|
||||||
|
*/
|
||||||
|
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||||
|
assertId(id);
|
||||||
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.BOT.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.OFFLINE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交聊天助手删除审批。
|
* 提交聊天助手删除审批。
|
||||||
*
|
*
|
||||||
* @param id 助手 ID
|
* @param id 助手 ID
|
||||||
* @return 助手 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||||
resourceType = "BOT",
|
|
||||||
actionType = "DELETE",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
|
||||||
assertId(id);
|
assertId(id);
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.BOT.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.DELETE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertId(BigInteger id) {
|
private void assertId(BigInteger id) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
@@ -12,43 +12,36 @@ import tech.easyflow.ai.service.BotDocumentCollectionService;
|
|||||||
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
|
||||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
|
||||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.system.entity.SysDept;
|
import tech.easyflow.system.entity.SysDept;
|
||||||
import tech.easyflow.system.enums.CategoryResourceType;
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
import tech.easyflow.system.enums.ResourceAction;
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
import tech.easyflow.system.enums.VisibilityScope;
|
import tech.easyflow.system.enums.VisibilityScope;
|
||||||
import tech.easyflow.system.service.SysDeptService;
|
|
||||||
import tech.easyflow.system.service.ResourceAccessService;
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
import tech.easyflow.system.service.SysDeptService;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库审批处理器。
|
* 知识库生命周期处理器。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
public class KnowledgeApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<DocumentCollection> {
|
||||||
|
|
||||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
|
||||||
|
|
||||||
private final DocumentCollectionService documentCollectionService;
|
private final DocumentCollectionService documentCollectionService;
|
||||||
private final ResourceAccessService resourceAccessService;
|
private final ResourceAccessService resourceAccessService;
|
||||||
private final ApprovalInstanceService approvalInstanceService;
|
|
||||||
private final BotDocumentCollectionService botDocumentCollectionService;
|
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||||
private final ModelService modelService;
|
private final ModelService modelService;
|
||||||
private final DocumentCollectionCategoryService documentCollectionCategoryService;
|
private final DocumentCollectionCategoryService documentCollectionCategoryService;
|
||||||
private final SysDeptService sysDeptService;
|
private final SysDeptService sysDeptService;
|
||||||
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
|
||||||
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
|
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
|
||||||
ResourceAccessService resourceAccessService,
|
ResourceAccessService resourceAccessService,
|
||||||
@@ -56,14 +49,17 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
BotDocumentCollectionService botDocumentCollectionService,
|
BotDocumentCollectionService botDocumentCollectionService,
|
||||||
ModelService modelService,
|
ModelService modelService,
|
||||||
DocumentCollectionCategoryService documentCollectionCategoryService,
|
DocumentCollectionCategoryService documentCollectionCategoryService,
|
||||||
SysDeptService sysDeptService) {
|
SysDeptService sysDeptService,
|
||||||
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
super(approvalInstanceService, objectMapper);
|
||||||
this.documentCollectionService = documentCollectionService;
|
this.documentCollectionService = documentCollectionService;
|
||||||
this.resourceAccessService = resourceAccessService;
|
this.resourceAccessService = resourceAccessService;
|
||||||
this.approvalInstanceService = approvalInstanceService;
|
|
||||||
this.botDocumentCollectionService = botDocumentCollectionService;
|
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||||
this.modelService = modelService;
|
this.modelService = modelService;
|
||||||
this.documentCollectionCategoryService = documentCollectionCategoryService;
|
this.documentCollectionCategoryService = documentCollectionCategoryService;
|
||||||
this.sysDeptService = sysDeptService;
|
this.sysDeptService = sysDeptService;
|
||||||
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -71,72 +67,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return ApprovalResourceType.KNOWLEDGE.getCode();
|
return ApprovalResourceType.KNOWLEDGE.getCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
|
||||||
DocumentCollection knowledge = requireKnowledge(resourceId);
|
|
||||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.MANAGE, "无权限管理知识库");
|
|
||||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
|
||||||
throw new BusinessException("当前知识库存在未结束审批,请先处理完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
|
||||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(knowledge);
|
|
||||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
|
||||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(knowledge.getPublishedSnapshotJson()))) {
|
|
||||||
throw new BusinessException("当前知识库没有变更,无需重复发布");
|
|
||||||
}
|
|
||||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
|
||||||
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
|
||||||
request.setResourceType(resourceType());
|
|
||||||
request.setResourceId(resourceId);
|
|
||||||
request.setActionType(approvalActionType.getCode());
|
|
||||||
request.setApplicantId(operatorId);
|
|
||||||
request.setCategoryId(knowledge.getCategoryId());
|
|
||||||
request.setDeptId(knowledge.getDeptId());
|
|
||||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "知识库:" + knowledge.getTitle());
|
|
||||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
|
||||||
DocumentCollection update = new DocumentCollection();
|
|
||||||
update.setId(context.getResourceId());
|
|
||||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
|
||||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
|
||||||
documentCollectionService.updateById(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApproved(ApprovalCallbackContext context) {
|
|
||||||
ApprovalInstance instance = context.getInstance();
|
|
||||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
|
||||||
DocumentCollection update = new DocumentCollection();
|
|
||||||
update.setId(instance.getResourceId());
|
|
||||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
|
||||||
update.setPublishedAt(new Date());
|
|
||||||
update.setPublishedBy(context.getOperatorId());
|
|
||||||
documentCollectionService.updateById(update);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
documentCollectionService.removeById(instance.getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRejected(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoked(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||||
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
|
||||||
@@ -146,20 +76,47 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DocumentCollection requireKnowledge(BigInteger id) {
|
@Override
|
||||||
DocumentCollection knowledge = documentCollectionService.getById(id);
|
protected DocumentCollection requireResource(BigInteger resourceId) {
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(resourceId);
|
||||||
if (knowledge == null) {
|
if (knowledge == null) {
|
||||||
throw new BusinessException("知识库不存在");
|
throw new BusinessException("知识库不存在");
|
||||||
}
|
}
|
||||||
return knowledge;
|
return knowledge;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasBotBinding(BigInteger knowledgeId) {
|
@Override
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
protected void assertManagePermission(DocumentCollection resource) {
|
||||||
return botDocumentCollectionService.exists(queryWrapper);
|
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, resource, ResourceAction.MANAGE, "无权限管理知识库");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
@Override
|
||||||
|
protected BigInteger getCategoryId(DocumentCollection resource) {
|
||||||
|
return resource.getCategoryId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BigInteger getDeptId(DocumentCollection resource) {
|
||||||
|
return resource.getDeptId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTitle(DocumentCollection resource) {
|
||||||
|
return resource.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PublishStatus getCurrentStatus(DocumentCollection resource) {
|
||||||
|
return PublishStatus.from(resource.getPublishStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> getPublishedSnapshot(DocumentCollection resource) {
|
||||||
|
return resource.getPublishedSnapshotJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
|
||||||
Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批");
|
Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批");
|
||||||
Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批");
|
Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批");
|
||||||
DocumentCollectionCategory category = resolveCategory(collection.getCategoryId());
|
DocumentCollectionCategory category = resolveCategory(collection.getCategoryId());
|
||||||
@@ -197,12 +154,82 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 解析知识库分类信息。
|
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||||
*
|
DocumentCollection update = new DocumentCollection();
|
||||||
* @param categoryId 分类 ID
|
update.setId(resourceId);
|
||||||
* @return 分类实体,不存在时返回 {@code null}
|
update.setPublishStatus(publishStatus.getCode());
|
||||||
*/
|
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||||
|
documentCollectionService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||||
|
DocumentCollection update = new DocumentCollection();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||||
|
update.setPublishedAt(new java.util.Date());
|
||||||
|
update.setPublishedBy(operatorId);
|
||||||
|
documentCollectionService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void markResourceOffline(BigInteger resourceId) {
|
||||||
|
DocumentCollection update = new DocumentCollection();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
documentCollectionService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeResource(BigInteger resourceId) {
|
||||||
|
documentCollectionService.removeById(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String resourceLabel() {
|
||||||
|
return "知识库";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void enrichOfflineSnapshot(DocumentCollection resource, Map<String, Object> snapshot) {
|
||||||
|
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkKnowledgeImpact(resource.getId());
|
||||||
|
if (!impact.isCanProceed()) {
|
||||||
|
throw new BusinessException(buildWorkflowUsageBlockMessage(impact));
|
||||||
|
}
|
||||||
|
if (impact.isHasBotBindings()) {
|
||||||
|
snapshot.put("botBindings", impact.getBotBindings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validateDelete(DocumentCollection resource, PublishStatus currentStatus) {
|
||||||
|
if (hasBotBinding(resource.getId())) {
|
||||||
|
throw new BusinessException("此知识库还关联着bot,请先取消关联!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterOffline(BigInteger resourceId) {
|
||||||
|
resourceOfflineImpactService.unbindKnowledgeFromBots(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasBotBinding(BigInteger knowledgeId) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
|
||||||
|
return botDocumentCollectionService.exists(queryWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildWorkflowUsageBlockMessage(OfflineImpactCheckVo impact) {
|
||||||
|
String names = impact.getWorkflowUsages().stream()
|
||||||
|
.map(item -> item.getTitle() == null ? String.valueOf(item.getId()) : item.getTitle())
|
||||||
|
.reduce((left, right) -> left + "、" + right)
|
||||||
|
.orElse("未知工作流");
|
||||||
|
return "当前知识库被以下工作流使用:" + names + ",请先在工作流中调整解绑后再下线";
|
||||||
|
}
|
||||||
|
|
||||||
private DocumentCollectionCategory resolveCategory(BigInteger categoryId) {
|
private DocumentCollectionCategory resolveCategory(BigInteger categoryId) {
|
||||||
if (categoryId == null) {
|
if (categoryId == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -210,12 +237,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return documentCollectionCategoryService.getById(categoryId);
|
return documentCollectionCategoryService.getById(categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析部门信息。
|
|
||||||
*
|
|
||||||
* @param deptId 部门 ID
|
|
||||||
* @return 部门实体,不存在时返回 {@code null}
|
|
||||||
*/
|
|
||||||
private SysDept resolveDept(BigInteger deptId) {
|
private SysDept resolveDept(BigInteger deptId) {
|
||||||
if (deptId == null) {
|
if (deptId == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -223,13 +244,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return sysDeptService.getById(deptId);
|
return sysDeptService.getById(deptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析必填模型。
|
|
||||||
*
|
|
||||||
* @param modelId 模型 ID
|
|
||||||
* @param errorMessage 模型不存在时抛出的提示
|
|
||||||
* @return 模型实体
|
|
||||||
*/
|
|
||||||
private Model resolveModel(BigInteger modelId, String errorMessage) {
|
private Model resolveModel(BigInteger modelId, String errorMessage) {
|
||||||
if (modelId == null) {
|
if (modelId == null) {
|
||||||
throw new BusinessException(errorMessage);
|
throw new BusinessException(errorMessage);
|
||||||
@@ -241,13 +255,6 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析可选模型。
|
|
||||||
*
|
|
||||||
* @param modelId 模型 ID
|
|
||||||
* @param errorMessage 模型不存在时抛出的提示
|
|
||||||
* @return 模型实体,不存在时返回 {@code null}
|
|
||||||
*/
|
|
||||||
private Model resolveOptionalModel(BigInteger modelId, String errorMessage) {
|
private Model resolveOptionalModel(BigInteger modelId, String errorMessage) {
|
||||||
if (modelId == null) {
|
if (modelId == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -259,75 +266,30 @@ public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成模型展示名称。
|
|
||||||
*
|
|
||||||
* @param model 模型实体
|
|
||||||
* @return 模型名称
|
|
||||||
*/
|
|
||||||
private String resolveModelName(Model model) {
|
private String resolveModelName(Model model) {
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (model.getTitle() != null && !model.getTitle().isBlank()) {
|
String providerName = model.getModelProvider() == null ? null : model.getModelProvider().getProviderName();
|
||||||
return model.getTitle();
|
if (providerName == null || providerName.isBlank()) {
|
||||||
}
|
|
||||||
return model.getModelName();
|
return model.getModelName();
|
||||||
}
|
}
|
||||||
|
return providerName + " / " + model.getModelName();
|
||||||
/**
|
|
||||||
* 解析知识库可见范围文案。
|
|
||||||
*
|
|
||||||
* @param visibilityScope 可见范围编码
|
|
||||||
* @return 展示文案
|
|
||||||
*/
|
|
||||||
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
|
||||||
return switch (VisibilityScope.fromOrDefault(visibilityScope, VisibilityScope.PRIVATE)) {
|
|
||||||
case DEPT -> "部门";
|
|
||||||
case PUBLIC -> "公开";
|
|
||||||
default -> "个人";
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析知识库类型文案。
|
|
||||||
*
|
|
||||||
* @param collectionType 知识库类型
|
|
||||||
* @return 展示文案
|
|
||||||
*/
|
|
||||||
private String resolveCollectionTypeLabel(String collectionType) {
|
private String resolveCollectionTypeLabel(String collectionType) {
|
||||||
if (DocumentCollection.TYPE_FAQ.equalsIgnoreCase(collectionType)) {
|
if ("FAQ".equalsIgnoreCase(collectionType)) {
|
||||||
return "FAQ";
|
return "FAQ";
|
||||||
}
|
}
|
||||||
return "文档";
|
return "文档";
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
private String resolveVisibilityScopeLabel(String visibilityScope) {
|
||||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
VisibilityScope scope = VisibilityScope.from(visibilityScope);
|
||||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
return switch (scope) {
|
||||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
case PRIVATE -> "仅自己";
|
||||||
throw new BusinessException("审批快照缺少知识库发布内容");
|
case DEPT -> "本部门";
|
||||||
}
|
case PUBLIC -> "公开";
|
||||||
return (Map<String, Object>) map;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private PublishStatus resolvePendingStatus(String actionType) {
|
|
||||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
|
||||||
? PublishStatus.DELETE_PENDING
|
|
||||||
: PublishStatus.PUBLISH_PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearPendingStatus(BigInteger knowledgeId) {
|
|
||||||
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
|
|
||||||
if (collection == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DocumentCollection update = new DocumentCollection();
|
|
||||||
update.setId(knowledgeId);
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishStatus(collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty()
|
|
||||||
? PublishStatus.DRAFT.getCode()
|
|
||||||
: PublishStatus.PUBLISHED.getCode());
|
|
||||||
documentCollectionService.updateById(update);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
@@ -12,36 +20,82 @@ import java.math.BigInteger;
|
|||||||
@Service
|
@Service
|
||||||
public class KnowledgePublishAppService {
|
public class KnowledgePublishAppService {
|
||||||
|
|
||||||
|
private final DocumentCollectionService documentCollectionService;
|
||||||
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||||
|
|
||||||
|
public KnowledgePublishAppService(DocumentCollectionService documentCollectionService,
|
||||||
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||||
|
this.documentCollectionService = documentCollectionService;
|
||||||
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
|
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交知识库发布审批。
|
* 提交知识库发布审批。
|
||||||
*
|
*
|
||||||
* @param id 知识库 ID
|
* @param id 知识库 ID
|
||||||
* @return 知识库 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||||
resourceType = "KNOWLEDGE",
|
|
||||||
actionType = "PUBLISH",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitPublishApproval(BigInteger id) {
|
|
||||||
assertId(id);
|
assertId(id);
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.PUBLISH.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交知识库下线审批。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 动作执行结果
|
||||||
|
*/
|
||||||
|
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||||
|
assertId(id);
|
||||||
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.OFFLINE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查知识库下线影响。
|
||||||
|
*
|
||||||
|
* @param id 知识库 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||||
|
assertId(id);
|
||||||
|
DocumentCollection knowledge = documentCollectionService.getById(id);
|
||||||
|
if (knowledge == null) {
|
||||||
|
throw new BusinessException("知识库不存在");
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("当前知识库尚未发布,无法下线");
|
||||||
|
}
|
||||||
|
return resourceOfflineImpactService.checkKnowledgeImpact(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交知识库删除审批。
|
* 提交知识库删除审批。
|
||||||
*
|
*
|
||||||
* @param id 知识库 ID
|
* @param id 知识库 ID
|
||||||
* @return 知识库 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||||
resourceType = "KNOWLEDGE",
|
|
||||||
actionType = "DELETE",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
|
||||||
assertId(id);
|
assertId(id);
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.DELETE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertId(BigInteger id) {
|
private void assertId(BigInteger id) {
|
||||||
|
|||||||
@@ -1,52 +1,48 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.ai.entity.BotWorkflow;
|
import tech.easyflow.ai.entity.BotWorkflow;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.enums.PublishStatus;
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.service.BotWorkflowService;
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
|
||||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.system.enums.CategoryResourceType;
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
import tech.easyflow.system.enums.ResourceAction;
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
import tech.easyflow.system.service.ResourceAccessService;
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作流审批处理器。
|
* 工作流生命周期处理器。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Workflow> {
|
||||||
|
|
||||||
private static final String SNAPSHOT_KEY = "resourceSnapshot";
|
|
||||||
|
|
||||||
private final WorkflowService workflowService;
|
private final WorkflowService workflowService;
|
||||||
private final ResourceAccessService resourceAccessService;
|
private final ResourceAccessService resourceAccessService;
|
||||||
private final ApprovalInstanceService approvalInstanceService;
|
|
||||||
private final BotWorkflowService botWorkflowService;
|
private final BotWorkflowService botWorkflowService;
|
||||||
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
|
||||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||||
ResourceAccessService resourceAccessService,
|
ResourceAccessService resourceAccessService,
|
||||||
ApprovalInstanceService approvalInstanceService,
|
ApprovalInstanceService approvalInstanceService,
|
||||||
BotWorkflowService botWorkflowService) {
|
BotWorkflowService botWorkflowService,
|
||||||
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
super(approvalInstanceService, objectMapper);
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.resourceAccessService = resourceAccessService;
|
this.resourceAccessService = resourceAccessService;
|
||||||
this.approvalInstanceService = approvalInstanceService;
|
|
||||||
this.botWorkflowService = botWorkflowService;
|
this.botWorkflowService = botWorkflowService;
|
||||||
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -54,72 +50,6 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
return ApprovalResourceType.WORKFLOW.getCode();
|
return ApprovalResourceType.WORKFLOW.getCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId) {
|
|
||||||
Workflow workflow = requireWorkflow(resourceId);
|
|
||||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.MANAGE, "无权限管理工作流");
|
|
||||||
if (approvalInstanceService.existsActiveInstance(resourceType(), resourceId)) {
|
|
||||||
throw new BusinessException("当前工作流存在未结束审批,请先处理完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalActionType approvalActionType = ApprovalActionType.from(actionType);
|
|
||||||
Map<String, Object> resourceSnapshot = buildResourceSnapshot(workflow);
|
|
||||||
if (approvalActionType == ApprovalActionType.PUBLISH
|
|
||||||
&& JSON.toJSONString(resourceSnapshot).equals(JSON.toJSONString(workflow.getPublishedSnapshotJson()))) {
|
|
||||||
throw new BusinessException("当前工作流没有变更,无需重复发布");
|
|
||||||
}
|
|
||||||
if (approvalActionType == ApprovalActionType.DELETE && hasBotBinding(resourceId)) {
|
|
||||||
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
|
||||||
}
|
|
||||||
|
|
||||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
|
||||||
request.setResourceType(resourceType());
|
|
||||||
request.setResourceId(resourceId);
|
|
||||||
request.setActionType(approvalActionType.getCode());
|
|
||||||
request.setApplicantId(operatorId);
|
|
||||||
request.setCategoryId(workflow.getCategoryId());
|
|
||||||
request.setDeptId(workflow.getDeptId());
|
|
||||||
request.setSummary((approvalActionType == ApprovalActionType.PUBLISH ? "发布" : "删除") + "工作流:" + workflow.getTitle());
|
|
||||||
request.setSnapshotJson(Map.of(SNAPSHOT_KEY, resourceSnapshot));
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSubmitted(ApprovalSubmitCallbackContext context) {
|
|
||||||
Workflow update = new Workflow();
|
|
||||||
update.setId(context.getResourceId());
|
|
||||||
update.setCurrentApprovalInstanceId(context.getInstanceId());
|
|
||||||
update.setPublishStatus(resolvePendingStatus(context.getActionType()).getCode());
|
|
||||||
workflowService.updateById(update);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onApproved(ApprovalCallbackContext context) {
|
|
||||||
ApprovalInstance instance = context.getInstance();
|
|
||||||
if (ApprovalActionType.PUBLISH.getCode().equals(instance.getActionType())) {
|
|
||||||
Workflow update = new Workflow();
|
|
||||||
update.setId(instance.getResourceId());
|
|
||||||
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishedSnapshotJson(readResourceSnapshot(instance));
|
|
||||||
update.setPublishedAt(new Date());
|
|
||||||
update.setPublishedBy(context.getOperatorId());
|
|
||||||
workflowService.updateById(update);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workflowService.removeById(instance.getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRejected(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoked(ApprovalCallbackContext context) {
|
|
||||||
clearPendingStatus(context.getInstance().getResourceId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||||
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
|
||||||
@@ -129,62 +59,125 @@ public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Workflow requireWorkflow(BigInteger id) {
|
@Override
|
||||||
Workflow workflow = workflowService.getById(id);
|
protected Workflow requireResource(BigInteger resourceId) {
|
||||||
|
Workflow workflow = workflowService.getById(resourceId);
|
||||||
if (workflow == null) {
|
if (workflow == null) {
|
||||||
throw new BusinessException("工作流不存在");
|
throw new BusinessException("工作流不存在");
|
||||||
}
|
}
|
||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void assertManagePermission(Workflow resource) {
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, resource, ResourceAction.MANAGE, "无权限管理工作流");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BigInteger getCategoryId(Workflow resource) {
|
||||||
|
return resource.getCategoryId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BigInteger getDeptId(Workflow resource) {
|
||||||
|
return resource.getDeptId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getTitle(Workflow resource) {
|
||||||
|
return resource.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PublishStatus getCurrentStatus(Workflow resource) {
|
||||||
|
return PublishStatus.from(resource.getPublishStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> getPublishedSnapshot(Workflow resource) {
|
||||||
|
return resource.getPublishedSnapshotJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> buildResourceSnapshot(Workflow resource) {
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||||
|
snapshot.put("id", resource.getId());
|
||||||
|
snapshot.put("alias", resource.getAlias());
|
||||||
|
snapshot.put("deptId", resource.getDeptId());
|
||||||
|
snapshot.put("tenantId", resource.getTenantId());
|
||||||
|
snapshot.put("title", resource.getTitle());
|
||||||
|
snapshot.put("description", resource.getDescription());
|
||||||
|
snapshot.put("icon", resource.getIcon());
|
||||||
|
snapshot.put("content", resource.getContent());
|
||||||
|
snapshot.put("englishName", resource.getEnglishName());
|
||||||
|
snapshot.put("status", resource.getStatus());
|
||||||
|
snapshot.put("categoryId", resource.getCategoryId());
|
||||||
|
snapshot.put("visibilityScope", resource.getVisibilityScope());
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||||
|
Workflow update = new Workflow();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(publishStatus.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||||
|
workflowService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||||
|
Workflow update = new Workflow();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
update.setPublishedSnapshotJson(resourceSnapshot);
|
||||||
|
update.setPublishedAt(new java.util.Date());
|
||||||
|
update.setPublishedBy(operatorId);
|
||||||
|
workflowService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void markResourceOffline(BigInteger resourceId) {
|
||||||
|
Workflow update = new Workflow();
|
||||||
|
update.setId(resourceId);
|
||||||
|
update.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||||
|
update.setCurrentApprovalInstanceId(null);
|
||||||
|
workflowService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void removeResource(BigInteger resourceId) {
|
||||||
|
workflowService.removeById(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String resourceLabel() {
|
||||||
|
return "工作流";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void enrichOfflineSnapshot(Workflow resource, Map<String, Object> snapshot) {
|
||||||
|
OfflineImpactCheckVo impact = resourceOfflineImpactService.checkWorkflowImpact(resource.getId());
|
||||||
|
if (impact.isHasBotBindings()) {
|
||||||
|
snapshot.put("botBindings", impact.getBotBindings());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void validateDelete(Workflow resource, PublishStatus currentStatus) {
|
||||||
|
if (hasBotBinding(resource.getId())) {
|
||||||
|
throw new BusinessException("此工作流还关联有bot,请先取消关联后再删除!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterOffline(BigInteger resourceId) {
|
||||||
|
resourceOfflineImpactService.unbindWorkflowFromBots(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasBotBinding(BigInteger workflowId) {
|
private boolean hasBotBinding(BigInteger workflowId) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
|
||||||
return botWorkflowService.exists(queryWrapper);
|
return botWorkflowService.exists(queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> buildResourceSnapshot(Workflow workflow) {
|
|
||||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
|
||||||
snapshot.put("id", workflow.getId());
|
|
||||||
snapshot.put("alias", workflow.getAlias());
|
|
||||||
snapshot.put("deptId", workflow.getDeptId());
|
|
||||||
snapshot.put("tenantId", workflow.getTenantId());
|
|
||||||
snapshot.put("title", workflow.getTitle());
|
|
||||||
snapshot.put("description", workflow.getDescription());
|
|
||||||
snapshot.put("icon", workflow.getIcon());
|
|
||||||
snapshot.put("content", workflow.getContent());
|
|
||||||
snapshot.put("englishName", workflow.getEnglishName());
|
|
||||||
snapshot.put("status", workflow.getStatus());
|
|
||||||
snapshot.put("categoryId", workflow.getCategoryId());
|
|
||||||
snapshot.put("visibilityScope", workflow.getVisibilityScope());
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, Object> readResourceSnapshot(ApprovalInstance instance) {
|
|
||||||
Object snapshot = instance.getSnapshotJson() == null ? null : instance.getSnapshotJson().get(SNAPSHOT_KEY);
|
|
||||||
if (!(snapshot instanceof Map<?, ?> map)) {
|
|
||||||
throw new BusinessException("审批快照缺少工作流发布内容");
|
|
||||||
}
|
|
||||||
return (Map<String, Object>) map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublishStatus resolvePendingStatus(String actionType) {
|
|
||||||
return ApprovalActionType.DELETE.getCode().equals(actionType)
|
|
||||||
? PublishStatus.DELETE_PENDING
|
|
||||||
: PublishStatus.PUBLISH_PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearPendingStatus(BigInteger workflowId) {
|
|
||||||
Workflow workflow = workflowService.getById(workflowId);
|
|
||||||
if (workflow == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Workflow update = new Workflow();
|
|
||||||
update.setId(workflowId);
|
|
||||||
update.setCurrentApprovalInstanceId(null);
|
|
||||||
update.setPublishStatus(workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()
|
|
||||||
? PublishStatus.DRAFT.getCode()
|
|
||||||
: PublishStatus.PUBLISHED.getCode());
|
|
||||||
workflowService.updateById(update);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package tech.easyflow.ai.publish;
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
@@ -14,36 +20,82 @@ import java.math.BigInteger;
|
|||||||
@Service
|
@Service
|
||||||
public class WorkflowPublishAppService {
|
public class WorkflowPublishAppService {
|
||||||
|
|
||||||
|
private final WorkflowService workflowService;
|
||||||
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
|
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||||
|
|
||||||
|
public WorkflowPublishAppService(WorkflowService workflowService,
|
||||||
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
|
AiResourceLifecycleService aiResourceLifecycleService) {
|
||||||
|
this.workflowService = workflowService;
|
||||||
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
|
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交工作流发布审批。
|
* 提交工作流发布审批。
|
||||||
*
|
*
|
||||||
* @param id 工作流 ID
|
* @param id 工作流 ID
|
||||||
* @return 工作流 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||||
resourceType = "WORKFLOW",
|
|
||||||
actionType = "PUBLISH",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitPublishApproval(BigInteger id) {
|
|
||||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.WORKFLOW.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.PUBLISH.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交工作流下线审批。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 动作执行结果
|
||||||
|
*/
|
||||||
|
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||||
|
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||||
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.WORKFLOW.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.OFFLINE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查工作流下线影响。
|
||||||
|
*
|
||||||
|
* @param id 工作流 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
public OfflineImpactCheckVo checkOfflineImpact(BigInteger id) {
|
||||||
|
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.OFFLINE.getCode());
|
||||||
|
Workflow workflow = workflowService.getById(id);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在");
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("当前工作流尚未发布,无法下线");
|
||||||
|
}
|
||||||
|
return resourceOfflineImpactService.checkWorkflowImpact(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交工作流删除审批。
|
* 提交工作流删除审批。
|
||||||
*
|
*
|
||||||
* @param id 工作流 ID
|
* @param id 工作流 ID
|
||||||
* @return 工作流 ID
|
* @return 动作执行结果
|
||||||
*/
|
*/
|
||||||
@ApprovalAction(
|
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||||
resourceType = "WORKFLOW",
|
|
||||||
actionType = "DELETE",
|
|
||||||
idExpr = "#id"
|
|
||||||
)
|
|
||||||
public BigInteger submitDeleteApproval(BigInteger id) {
|
|
||||||
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode());
|
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode());
|
||||||
return id;
|
return aiResourceLifecycleService.submitAction(
|
||||||
|
ApprovalResourceType.WORKFLOW.getCode(),
|
||||||
|
id,
|
||||||
|
ApprovalActionType.DELETE.getCode(),
|
||||||
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertId(BigInteger id, String resourceType, String actionType) {
|
private void assertId(BigInteger id, String resourceType, String actionType) {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package tech.easyflow.ai.service;
|
||||||
|
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源审批状态派生服务。
|
||||||
|
* <p>
|
||||||
|
* 该服务仅负责根据当前审批实例与资源真实状态派生只读展示字段,
|
||||||
|
* 不参与资源状态回写,也不在读链路中执行任何数据库修复操作。
|
||||||
|
*/
|
||||||
|
public interface AiResourceApprovalStateService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充工作流审批展示状态。
|
||||||
|
*
|
||||||
|
* @param workflow 工作流
|
||||||
|
*/
|
||||||
|
void fillWorkflowApprovalState(Workflow workflow);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充工作流审批展示状态。
|
||||||
|
*
|
||||||
|
* @param workflows 工作流集合
|
||||||
|
*/
|
||||||
|
void fillWorkflowApprovalState(Collection<Workflow> workflows);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充知识库审批展示状态。
|
||||||
|
*
|
||||||
|
* @param collection 知识库
|
||||||
|
*/
|
||||||
|
void fillKnowledgeApprovalState(DocumentCollection collection);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充知识库审批展示状态。
|
||||||
|
*
|
||||||
|
* @param collections 知识库集合
|
||||||
|
*/
|
||||||
|
void fillKnowledgeApprovalState(Collection<DocumentCollection> collections);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充聊天助手审批展示状态。
|
||||||
|
*
|
||||||
|
* @param bot 聊天助手
|
||||||
|
*/
|
||||||
|
void fillBotApprovalState(Bot bot);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充聊天助手审批展示状态。
|
||||||
|
*
|
||||||
|
* @param bots 聊天助手集合
|
||||||
|
*/
|
||||||
|
void fillBotApprovalState(Collection<Bot> bots);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package tech.easyflow.ai.service;
|
||||||
|
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源下线影响检查与解绑服务。
|
||||||
|
*/
|
||||||
|
public interface ResourceOfflineImpactService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查工作流下线影响。
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查知识库下线影响。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return 下线影响结果
|
||||||
|
*/
|
||||||
|
OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流下线后,静默解绑所有关联 Bot。
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流 ID
|
||||||
|
*/
|
||||||
|
void unbindWorkflowFromBots(BigInteger workflowId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库下线后,静默解绑所有关联 Bot。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
*/
|
||||||
|
void unbindKnowledgeFromBots(BigInteger knowledgeId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package tech.easyflow.ai.service.impl;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.AiResourceApprovalStateService;
|
||||||
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
|
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 资源审批状态派生服务实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AiResourceApprovalStateServiceImpl implements AiResourceApprovalStateService {
|
||||||
|
|
||||||
|
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||||
|
|
||||||
|
public AiResourceApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||||
|
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillWorkflowApprovalState(Workflow workflow) {
|
||||||
|
fillWorkflowApprovalState(workflow == null ? List.of() : List.of(workflow));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillWorkflowApprovalState(Collection<Workflow> workflows) {
|
||||||
|
fillApprovalState(
|
||||||
|
workflows,
|
||||||
|
ApprovalResourceType.WORKFLOW.getCode(),
|
||||||
|
Workflow::getCurrentApprovalInstanceId,
|
||||||
|
workflow -> PublishStatus.from(workflow.getPublishStatus()),
|
||||||
|
Workflow::getPublishedSnapshotJson,
|
||||||
|
Workflow::setApprovalPending,
|
||||||
|
Workflow::setCurrentApprovalActionType,
|
||||||
|
Workflow::setDisplayPublishStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillKnowledgeApprovalState(DocumentCollection collection) {
|
||||||
|
fillKnowledgeApprovalState(collection == null ? List.of() : List.of(collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillKnowledgeApprovalState(Collection<DocumentCollection> collections) {
|
||||||
|
fillApprovalState(
|
||||||
|
collections,
|
||||||
|
ApprovalResourceType.KNOWLEDGE.getCode(),
|
||||||
|
DocumentCollection::getCurrentApprovalInstanceId,
|
||||||
|
collection -> PublishStatus.from(collection.getPublishStatus()),
|
||||||
|
DocumentCollection::getPublishedSnapshotJson,
|
||||||
|
DocumentCollection::setApprovalPending,
|
||||||
|
DocumentCollection::setCurrentApprovalActionType,
|
||||||
|
DocumentCollection::setDisplayPublishStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillBotApprovalState(Bot bot) {
|
||||||
|
fillBotApprovalState(bot == null ? List.of() : List.of(bot));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void fillBotApprovalState(Collection<Bot> bots) {
|
||||||
|
fillApprovalState(
|
||||||
|
bots,
|
||||||
|
ApprovalResourceType.BOT.getCode(),
|
||||||
|
Bot::getCurrentApprovalInstanceId,
|
||||||
|
bot -> PublishStatus.from(bot.getPublishStatus()),
|
||||||
|
Bot::getPublishedSnapshotJson,
|
||||||
|
Bot::setApprovalPending,
|
||||||
|
Bot::setCurrentApprovalActionType,
|
||||||
|
Bot::setDisplayPublishStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一派生审批展示状态。
|
||||||
|
*
|
||||||
|
* @param resources 资源集合
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||||
|
* @param statusGetter 发布状态获取器
|
||||||
|
* @param snapshotGetter 已发布快照获取器
|
||||||
|
* @param pendingSetter 审批中标记写入器
|
||||||
|
* @param actionSetter 当前审批动作写入器
|
||||||
|
* @param displaySetter 展示状态写入器
|
||||||
|
* @param <T> 资源类型
|
||||||
|
*/
|
||||||
|
private <T> void fillApprovalState(Collection<T> resources,
|
||||||
|
String resourceType,
|
||||||
|
Function<T, BigInteger> instanceIdGetter,
|
||||||
|
Function<T, PublishStatus> statusGetter,
|
||||||
|
Function<T, Map<String, Object>> snapshotGetter,
|
||||||
|
BiConsumer<T, Boolean> pendingSetter,
|
||||||
|
BiConsumer<T, String> actionSetter,
|
||||||
|
BiConsumer<T, String> displaySetter) {
|
||||||
|
if (CollectionUtils.isEmpty(resources)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<T> validResources = resources.stream().filter(Objects::nonNull).toList();
|
||||||
|
if (validResources.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validResources, instanceIdGetter);
|
||||||
|
for (T resource : validResources) {
|
||||||
|
PublishStatus currentStatus = statusGetter.apply(resource);
|
||||||
|
BigInteger instanceId = instanceIdGetter.apply(resource);
|
||||||
|
ApprovalInstance instance = instanceId == null ? null : instanceMap.get(instanceId);
|
||||||
|
if (!isValidCurrentInstance(resourceType, instance)) {
|
||||||
|
pendingSetter.accept(resource, false);
|
||||||
|
actionSetter.accept(resource, null);
|
||||||
|
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||||
|
if (instanceStatus.isFinished()) {
|
||||||
|
pendingSetter.accept(resource, false);
|
||||||
|
actionSetter.accept(resource, null);
|
||||||
|
displaySetter.accept(resource, resolveDisplayStatusWithoutActiveInstance(currentStatus, snapshotGetter.apply(resource)).getCode());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||||
|
pendingSetter.accept(resource, true);
|
||||||
|
actionSetter.accept(resource, actionType.getCode());
|
||||||
|
displaySetter.accept(resource, resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量加载当前审批实例。
|
||||||
|
*
|
||||||
|
* @param resources 资源集合
|
||||||
|
* @param instanceIdGetter 当前审批实例 ID 获取器
|
||||||
|
* @param <T> 资源类型
|
||||||
|
* @return 审批实例映射
|
||||||
|
*/
|
||||||
|
private <T> Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<T> resources,
|
||||||
|
Function<T, BigInteger> instanceIdGetter) {
|
||||||
|
Set<BigInteger> instanceIds = resources.stream()
|
||||||
|
.map(instanceIdGetter)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||||
|
if (instanceIds.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||||
|
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||||
|
);
|
||||||
|
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断审批实例是否仍是当前资源的有效实例。
|
||||||
|
*
|
||||||
|
* @param resourceType 资源类型
|
||||||
|
* @param instance 审批实例
|
||||||
|
* @return 有效返回 true
|
||||||
|
*/
|
||||||
|
private boolean isValidCurrentInstance(String resourceType, ApprovalInstance instance) {
|
||||||
|
return instance != null && resourceType.equals(instance.getResourceType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在没有活动审批实例时派生展示状态。
|
||||||
|
*
|
||||||
|
* @param currentStatus 当前真实状态
|
||||||
|
* @param publishedSnapshot 已发布快照
|
||||||
|
* @return 展示状态
|
||||||
|
*/
|
||||||
|
private PublishStatus resolveDisplayStatusWithoutActiveInstance(PublishStatus currentStatus,
|
||||||
|
Map<String, Object> publishedSnapshot) {
|
||||||
|
// 读接口不再猜测或掩盖真实状态。若资源表已经落成 pending 但当前找不到有效审批实例,
|
||||||
|
// 页面应直接看到真实状态,后续再由独立修复流程处理脏数据,而不是在展示层伪装成已发布。
|
||||||
|
return currentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在存在活动审批实例时派生展示状态。
|
||||||
|
*
|
||||||
|
* @param currentStatus 当前真实状态
|
||||||
|
* @param actionType 当前审批动作
|
||||||
|
* @return 展示状态
|
||||||
|
*/
|
||||||
|
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||||
|
ApprovalActionType actionType) {
|
||||||
|
if (currentStatus == PublishStatus.PUBLISHED && actionType == ApprovalActionType.PUBLISH) {
|
||||||
|
return PublishStatus.PUBLISH_PENDING;
|
||||||
|
}
|
||||||
|
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||||
|
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||||
|
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||||
|
return currentStatus;
|
||||||
|
}
|
||||||
|
return switch (actionType) {
|
||||||
|
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||||
|
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||||
|
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
package tech.easyflow.ai.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||||
|
import tech.easyflow.ai.entity.BotWorkflow;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.ai.service.BotWorkflowService;
|
||||||
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 资源下线影响检查与 Bot 静默解绑实现。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ResourceOfflineImpactServiceImpl implements ResourceOfflineImpactService {
|
||||||
|
|
||||||
|
private static final String KNOWLEDGE_NODE_TYPE = "knowledgeNode";
|
||||||
|
private static final String BOT_BINDING_LOCK_KEY_PREFIX = "easyflow:lock:bot:binding:";
|
||||||
|
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||||
|
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
|
||||||
|
private final BotWorkflowService botWorkflowService;
|
||||||
|
private final BotDocumentCollectionService botDocumentCollectionService;
|
||||||
|
private final BotService botService;
|
||||||
|
private final WorkflowService workflowService;
|
||||||
|
private final RedisLockExecutor redisLockExecutor;
|
||||||
|
|
||||||
|
public ResourceOfflineImpactServiceImpl(BotWorkflowService botWorkflowService,
|
||||||
|
BotDocumentCollectionService botDocumentCollectionService,
|
||||||
|
BotService botService,
|
||||||
|
WorkflowService workflowService,
|
||||||
|
RedisLockExecutor redisLockExecutor) {
|
||||||
|
this.botWorkflowService = botWorkflowService;
|
||||||
|
this.botDocumentCollectionService = botDocumentCollectionService;
|
||||||
|
this.botService = botService;
|
||||||
|
this.workflowService = workflowService;
|
||||||
|
this.redisLockExecutor = redisLockExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OfflineImpactCheckVo checkWorkflowImpact(BigInteger workflowId) {
|
||||||
|
List<OfflineImpactBindingVo> botBindings = listBotsByWorkflowId(workflowId);
|
||||||
|
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||||
|
result.setCanProceed(true);
|
||||||
|
result.setBotBindings(botBindings);
|
||||||
|
result.setHasBotBindings(!botBindings.isEmpty());
|
||||||
|
result.setWorkflowUsages(Collections.emptyList());
|
||||||
|
result.setHasWorkflowUsages(false);
|
||||||
|
result.setMessage(botBindings.isEmpty()
|
||||||
|
? "当前工作流下线后不会影响已有绑定"
|
||||||
|
: "当前工作流下线成功后,将自动从相关聊天助手中解绑");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public OfflineImpactCheckVo checkKnowledgeImpact(BigInteger knowledgeId) {
|
||||||
|
List<OfflineImpactBindingVo> botBindings = listBotsByKnowledgeId(knowledgeId);
|
||||||
|
List<OfflineImpactBindingVo> workflowUsages = listWorkflowsUsingKnowledge(knowledgeId);
|
||||||
|
OfflineImpactCheckVo result = new OfflineImpactCheckVo();
|
||||||
|
result.setBotBindings(botBindings);
|
||||||
|
result.setHasBotBindings(!botBindings.isEmpty());
|
||||||
|
result.setWorkflowUsages(workflowUsages);
|
||||||
|
result.setHasWorkflowUsages(!workflowUsages.isEmpty());
|
||||||
|
result.setCanProceed(workflowUsages.isEmpty());
|
||||||
|
result.setMessage(workflowUsages.isEmpty()
|
||||||
|
? (botBindings.isEmpty()
|
||||||
|
? "当前知识库下线后不会影响已有绑定"
|
||||||
|
: "当前知识库下线成功后,将自动从相关聊天助手中解绑")
|
||||||
|
: "当前知识库仍被工作流使用,请先调整工作流后再下线");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void unbindWorkflowFromBots(BigInteger workflowId) {
|
||||||
|
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||||
|
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||||
|
Set<BigInteger> botIds = collectBotIds(relations, BotWorkflow::getBotId);
|
||||||
|
for (BigInteger botId : botIds) {
|
||||||
|
redisLockExecutor.executeWithLock(
|
||||||
|
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||||
|
LOCK_WAIT_TIMEOUT,
|
||||||
|
LOCK_LEASE_TIMEOUT,
|
||||||
|
() -> {
|
||||||
|
botWorkflowService.remove(QueryWrapper.create()
|
||||||
|
.eq(BotWorkflow::getBotId, botId)
|
||||||
|
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||||
|
trimPublishedSnapshotBindings(botId, "workflowBindings", "workflowId", workflowId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void unbindKnowledgeFromBots(BigInteger knowledgeId) {
|
||||||
|
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||||
|
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||||
|
Set<BigInteger> botIds = collectBotIds(relations, BotDocumentCollection::getBotId);
|
||||||
|
for (BigInteger botId : botIds) {
|
||||||
|
redisLockExecutor.executeWithLock(
|
||||||
|
BOT_BINDING_LOCK_KEY_PREFIX + botId,
|
||||||
|
LOCK_WAIT_TIMEOUT,
|
||||||
|
LOCK_LEASE_TIMEOUT,
|
||||||
|
() -> {
|
||||||
|
botDocumentCollectionService.remove(QueryWrapper.create()
|
||||||
|
.eq(BotDocumentCollection::getBotId, botId)
|
||||||
|
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||||
|
trimPublishedSnapshotBindings(botId, "knowledgeBindings", "knowledgeId", knowledgeId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> listBotsByWorkflowId(BigInteger workflowId) {
|
||||||
|
List<BotWorkflow> relations = botWorkflowService.list(QueryWrapper.create()
|
||||||
|
.eq(BotWorkflow::getWorkflowId, workflowId));
|
||||||
|
return listBotsByIds(collectBotIds(relations, BotWorkflow::getBotId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> listBotsByKnowledgeId(BigInteger knowledgeId) {
|
||||||
|
List<BotDocumentCollection> relations = botDocumentCollectionService.list(QueryWrapper.create()
|
||||||
|
.eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId));
|
||||||
|
return listBotsByIds(collectBotIds(relations, BotDocumentCollection::getBotId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> listBotsByIds(Set<BigInteger> botIds) {
|
||||||
|
if (botIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Bot> bots = botService.listByIds(botIds);
|
||||||
|
Map<BigInteger, Bot> botMap = new HashMap<>();
|
||||||
|
for (Bot bot : bots) {
|
||||||
|
botMap.put(bot.getId(), bot);
|
||||||
|
}
|
||||||
|
List<OfflineImpactBindingVo> result = new ArrayList<>(botIds.size());
|
||||||
|
for (BigInteger botId : botIds) {
|
||||||
|
Bot bot = botMap.get(botId);
|
||||||
|
if (bot == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.add(toBindingVo(bot.getId(), bot.getTitle()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> listWorkflowsUsingKnowledge(BigInteger knowledgeId) {
|
||||||
|
List<Workflow> workflows = workflowService.list();
|
||||||
|
if (workflows == null || workflows.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<OfflineImpactBindingVo> result = new ArrayList<>();
|
||||||
|
for (Workflow workflow : workflows) {
|
||||||
|
if (workflow == null || workflow.getId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (containsKnowledgeReference(workflow.getContent(), knowledgeId)) {
|
||||||
|
result.add(toBindingVo(workflow.getId(), workflow.getTitle()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsKnowledgeReference(String content, BigInteger knowledgeId) {
|
||||||
|
if (!StringUtils.hasText(content) || knowledgeId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Object parsed = JSON.parse(content);
|
||||||
|
if (!(parsed instanceof JSONObject root)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
JSONArray nodes = root.getJSONArray("nodes");
|
||||||
|
if (nodes == null || nodes.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String expected = knowledgeId.toString();
|
||||||
|
for (int i = 0; i < nodes.size(); i++) {
|
||||||
|
JSONObject node = nodes.getJSONObject(i);
|
||||||
|
if (node == null || !KNOWLEDGE_NODE_TYPE.equals(node.getString("type"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONObject data = node.getJSONObject("data");
|
||||||
|
if (data == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object rawKnowledgeId = data.get("knowledgeId");
|
||||||
|
if (rawKnowledgeId != null && expected.equals(String.valueOf(rawKnowledgeId))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void trimPublishedSnapshotBindings(BigInteger botId,
|
||||||
|
String bindingsKey,
|
||||||
|
String idKey,
|
||||||
|
BigInteger resourceId) {
|
||||||
|
Bot bot = botService.getById(botId);
|
||||||
|
if (bot == null || bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> snapshot = new LinkedHashMap<>(bot.getPublishedSnapshotJson());
|
||||||
|
Object rawBindings = snapshot.get(bindingsKey);
|
||||||
|
if (!(rawBindings instanceof List<?> bindings)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> filtered = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
String expectedId = resourceId == null ? null : resourceId.toString();
|
||||||
|
for (Object item : bindings) {
|
||||||
|
if (!(item instanceof Map<?, ?> bindingMap)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object currentId = bindingMap.get(idKey);
|
||||||
|
if (expectedId != null && currentId != null && expectedId.equals(String.valueOf(currentId))) {
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.add(new LinkedHashMap<>((Map<String, Object>) bindingMap));
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snapshot.put(bindingsKey, filtered);
|
||||||
|
Bot update = new Bot();
|
||||||
|
update.setId(botId);
|
||||||
|
update.setPublishedSnapshotJson(snapshot);
|
||||||
|
botService.updateById(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> Set<BigInteger> collectBotIds(Collection<T> relations, BotIdGetter<T> getter) {
|
||||||
|
if (relations == null || relations.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
Set<BigInteger> result = new LinkedHashSet<>();
|
||||||
|
for (T relation : relations) {
|
||||||
|
BigInteger botId = getter.getBotId(relation);
|
||||||
|
if (botId != null) {
|
||||||
|
result.add(botId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OfflineImpactBindingVo toBindingVo(BigInteger id, String title) {
|
||||||
|
OfflineImpactBindingVo vo = new OfflineImpactBindingVo();
|
||||||
|
vo.setId(id);
|
||||||
|
vo.setTitle(title);
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface BotIdGetter<T> {
|
||||||
|
|
||||||
|
BigInteger getBotId(T relation);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import okhttp3.Call;
|
|||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
import org.apache.poi.extractor.ExtractorFactory;
|
||||||
|
import org.apache.poi.extractor.POITextExtractor;
|
||||||
import org.apache.pdfbox.multipdf.Splitter;
|
import org.apache.pdfbox.multipdf.Splitter;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||||
@@ -22,8 +24,10 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class DocUtil {
|
public class DocUtil {
|
||||||
@@ -85,6 +89,27 @@ public class DocUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取素材预览文本。
|
||||||
|
*
|
||||||
|
* @param suffix 文件后缀
|
||||||
|
* @param is 文件输入流
|
||||||
|
* @return 预览文本
|
||||||
|
*/
|
||||||
|
public static String readPreviewContent(String suffix, InputStream is) {
|
||||||
|
String normalizedSuffix = normalizeSuffix(suffix);
|
||||||
|
if (isPlainTextSuffix(normalizedSuffix)) {
|
||||||
|
return readPlainTextFile(is);
|
||||||
|
}
|
||||||
|
if ("pdf".equals(normalizedSuffix)) {
|
||||||
|
return readPdfFile(is);
|
||||||
|
}
|
||||||
|
if (isOfficeSuffix(normalizedSuffix)) {
|
||||||
|
return readOfficeFile(is);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("不支持的文件类型: " + suffix);
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<Integer, byte[]> splitPdf(byte[] bytes, int splitSize) {
|
public static Map<Integer, byte[]> splitPdf(byte[] bytes, int splitSize) {
|
||||||
|
|
||||||
Map<Integer, byte[]> map = new HashMap<>();
|
Map<Integer, byte[]> map = new HashMap<>();
|
||||||
@@ -174,6 +199,16 @@ public class DocUtil {
|
|||||||
return name.substring(name.lastIndexOf(".") + 1);
|
return name.substring(name.lastIndexOf(".") + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化文件后缀,统一使用小写。
|
||||||
|
*
|
||||||
|
* @param suffix 原始文件后缀
|
||||||
|
* @return 规范化后的后缀
|
||||||
|
*/
|
||||||
|
public static String normalizeSuffix(String suffix) {
|
||||||
|
return suffix == null ? "" : suffix.trim().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] readBytes(InputStream inputStream) {
|
public static byte[] readBytes(InputStream inputStream) {
|
||||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||||
try {
|
try {
|
||||||
@@ -194,4 +229,59 @@ public class DocUtil {
|
|||||||
public static String getFileNameByUrl(String url) {
|
public static String getFileNameByUrl(String url) {
|
||||||
return url.substring(url.lastIndexOf("/") + 1);
|
return url.substring(url.lastIndexOf("/") + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取纯文本类型文件。
|
||||||
|
*
|
||||||
|
* @param is 文件输入流
|
||||||
|
* @return 文本内容
|
||||||
|
*/
|
||||||
|
private static String readPlainTextFile(InputStream is) {
|
||||||
|
try {
|
||||||
|
return new String(readBytes(is), StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("读取文本文件失败:", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Apache POI 提取 Office 文档中的纯文本。
|
||||||
|
*
|
||||||
|
* @param is 文件输入流
|
||||||
|
* @return 文本内容
|
||||||
|
*/
|
||||||
|
private static String readOfficeFile(InputStream is) {
|
||||||
|
try (POITextExtractor extractor = ExtractorFactory.createExtractor(is)) {
|
||||||
|
return extractor.getText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("读取 Office 文件失败:", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为纯文本类文件。
|
||||||
|
*
|
||||||
|
* @param suffix 文件后缀
|
||||||
|
* @return 是否纯文本类文件
|
||||||
|
*/
|
||||||
|
private static boolean isPlainTextSuffix(String suffix) {
|
||||||
|
return "txt".equals(suffix) || "md".equals(suffix) || "csv".equals(suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为 Office 文件。
|
||||||
|
*
|
||||||
|
* @param suffix 文件后缀
|
||||||
|
* @return 是否 Office 文件
|
||||||
|
*/
|
||||||
|
private static boolean isOfficeSuffix(String suffix) {
|
||||||
|
return "doc".equals(suffix)
|
||||||
|
|| "docx".equals(suffix)
|
||||||
|
|| "xls".equals(suffix)
|
||||||
|
|| "xlsx".equals(suffix)
|
||||||
|
|| "ppt".equals(suffix)
|
||||||
|
|| "pptx".equals(suffix);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package tech.easyflow.ai.vo;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下线影响项视图对象。
|
||||||
|
*/
|
||||||
|
public class OfflineImpactBindingVo {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源 ID。
|
||||||
|
*
|
||||||
|
* @return 资源 ID
|
||||||
|
*/
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置资源 ID。
|
||||||
|
*
|
||||||
|
* @param id 资源 ID
|
||||||
|
*/
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取资源名称。
|
||||||
|
*
|
||||||
|
* @return 资源名称
|
||||||
|
*/
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置资源名称。
|
||||||
|
*
|
||||||
|
* @param title 资源名称
|
||||||
|
*/
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package tech.easyflow.ai.vo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下线影响检查结果。
|
||||||
|
*/
|
||||||
|
public class OfflineImpactCheckVo {
|
||||||
|
|
||||||
|
private boolean canProceed;
|
||||||
|
|
||||||
|
private boolean hasBotBindings;
|
||||||
|
|
||||||
|
private boolean hasWorkflowUsages;
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> botBindings = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<OfflineImpactBindingVo> workflowUsages = new ArrayList<>();
|
||||||
|
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许继续下线。
|
||||||
|
*
|
||||||
|
* @return 是否允许继续
|
||||||
|
*/
|
||||||
|
public boolean isCanProceed() {
|
||||||
|
return canProceed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否允许继续下线。
|
||||||
|
*
|
||||||
|
* @param canProceed 是否允许继续
|
||||||
|
*/
|
||||||
|
public void setCanProceed(boolean canProceed) {
|
||||||
|
this.canProceed = canProceed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否存在 Bot 绑定。
|
||||||
|
*
|
||||||
|
* @return 是否存在 Bot 绑定
|
||||||
|
*/
|
||||||
|
public boolean isHasBotBindings() {
|
||||||
|
return hasBotBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否存在 Bot 绑定。
|
||||||
|
*
|
||||||
|
* @param hasBotBindings 是否存在 Bot 绑定
|
||||||
|
*/
|
||||||
|
public void setHasBotBindings(boolean hasBotBindings) {
|
||||||
|
this.hasBotBindings = hasBotBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否存在工作流引用。
|
||||||
|
*
|
||||||
|
* @return 是否存在工作流引用
|
||||||
|
*/
|
||||||
|
public boolean isHasWorkflowUsages() {
|
||||||
|
return hasWorkflowUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否存在工作流引用。
|
||||||
|
*
|
||||||
|
* @param hasWorkflowUsages 是否存在工作流引用
|
||||||
|
*/
|
||||||
|
public void setHasWorkflowUsages(boolean hasWorkflowUsages) {
|
||||||
|
this.hasWorkflowUsages = hasWorkflowUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Bot 绑定列表。
|
||||||
|
*
|
||||||
|
* @return Bot 绑定列表
|
||||||
|
*/
|
||||||
|
public List<OfflineImpactBindingVo> getBotBindings() {
|
||||||
|
return botBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Bot 绑定列表。
|
||||||
|
*
|
||||||
|
* @param botBindings Bot 绑定列表
|
||||||
|
*/
|
||||||
|
public void setBotBindings(List<OfflineImpactBindingVo> botBindings) {
|
||||||
|
this.botBindings = botBindings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作流引用列表。
|
||||||
|
*
|
||||||
|
* @return 工作流引用列表
|
||||||
|
*/
|
||||||
|
public List<OfflineImpactBindingVo> getWorkflowUsages() {
|
||||||
|
return workflowUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工作流引用列表。
|
||||||
|
*
|
||||||
|
* @param workflowUsages 工作流引用列表
|
||||||
|
*/
|
||||||
|
public void setWorkflowUsages(List<OfflineImpactBindingVo> workflowUsages) {
|
||||||
|
this.workflowUsages = workflowUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提示信息。
|
||||||
|
*
|
||||||
|
* @return 提示信息
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提示信息。
|
||||||
|
*
|
||||||
|
* @param message 提示信息
|
||||||
|
*/
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,15 @@ import org.springframework.expression.ExpressionParser;
|
|||||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import tech.easyflow.approval.annotation.ApprovalAction;
|
import tech.easyflow.approval.annotation.ApprovalAction;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提审动作切面。
|
* 提审动作切面。
|
||||||
@@ -25,11 +29,14 @@ import java.math.BigInteger;
|
|||||||
public class ApprovalActionAspect {
|
public class ApprovalActionAspect {
|
||||||
|
|
||||||
private final ApprovalActionFacade approvalActionFacade;
|
private final ApprovalActionFacade approvalActionFacade;
|
||||||
|
private final List<ApprovalSubjectHandler> handlers;
|
||||||
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
private final ExpressionParser expressionParser = new SpelExpressionParser();
|
||||||
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||||
|
|
||||||
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade) {
|
public ApprovalActionAspect(ApprovalActionFacade approvalActionFacade,
|
||||||
|
List<ApprovalSubjectHandler> handlers) {
|
||||||
this.approvalActionFacade = approvalActionFacade;
|
this.approvalActionFacade = approvalActionFacade;
|
||||||
|
this.handlers = handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +44,7 @@ public class ApprovalActionAspect {
|
|||||||
*
|
*
|
||||||
* @param joinPoint 切点
|
* @param joinPoint 切点
|
||||||
* @param approvalAction 注解
|
* @param approvalAction 注解
|
||||||
* @return 审批实例 ID
|
* @return 动作执行结果
|
||||||
* @throws Throwable 执行异常
|
* @throws Throwable 执行异常
|
||||||
*/
|
*/
|
||||||
@Around("@annotation(approvalAction)")
|
@Around("@annotation(approvalAction)")
|
||||||
@@ -45,12 +52,14 @@ public class ApprovalActionAspect {
|
|||||||
Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr());
|
Object identifier = resolveIdentifier(joinPoint, approvalAction.idExpr());
|
||||||
BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier));
|
BigInteger resourceId = identifier == null ? null : new BigInteger(String.valueOf(identifier));
|
||||||
joinPoint.proceed();
|
joinPoint.proceed();
|
||||||
return approvalActionFacade.submit(
|
ApprovalSubjectHandler handler = getHandler(approvalAction.resourceType());
|
||||||
approvalAction.resourceType(),
|
ApprovalSubmitRequest request = handler.buildSubmitRequest(
|
||||||
resourceId,
|
resourceId,
|
||||||
approvalAction.actionType(),
|
approvalAction.actionType(),
|
||||||
SaTokenUtil.getLoginAccount().getId()
|
SaTokenUtil.getLoginAccount().getId()
|
||||||
);
|
);
|
||||||
|
ApprovalActionResult result = approvalActionFacade.submit(request);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) {
|
private Object resolveIdentifier(ProceedingJoinPoint joinPoint, String idExpr) {
|
||||||
@@ -63,4 +72,11 @@ public class ApprovalActionAspect {
|
|||||||
);
|
);
|
||||||
return expressionParser.parseExpression(idExpr).getValue(context);
|
return expressionParser.parseExpression(idExpr).getValue(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||||
|
return handlers.stream()
|
||||||
|
.filter(item -> item.resourceType().equals(resourceType))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalStateException("未找到审批处理器: " + resourceType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package tech.easyflow.approval.entity.vo;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批动作执行结果。
|
||||||
|
*/
|
||||||
|
public class ApprovalActionResult {
|
||||||
|
|
||||||
|
private boolean approvalRequired;
|
||||||
|
|
||||||
|
private BigInteger instanceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造需要审批的结果。
|
||||||
|
*
|
||||||
|
* @param instanceId 审批实例 ID
|
||||||
|
* @return 审批结果
|
||||||
|
*/
|
||||||
|
public static ApprovalActionResult required(BigInteger instanceId) {
|
||||||
|
ApprovalActionResult result = new ApprovalActionResult();
|
||||||
|
result.setApprovalRequired(true);
|
||||||
|
result.setInstanceId(instanceId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造直接执行完成的结果。
|
||||||
|
*
|
||||||
|
* @return 审批结果
|
||||||
|
*/
|
||||||
|
public static ApprovalActionResult direct() {
|
||||||
|
ApprovalActionResult result = new ApprovalActionResult();
|
||||||
|
result.setApprovalRequired(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isApprovalRequired() {
|
||||||
|
return approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApprovalRequired(boolean approvalRequired) {
|
||||||
|
this.approvalRequired = approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstanceId(BigInteger instanceId) {
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ public class ApprovalInstanceDetailVo {
|
|||||||
|
|
||||||
private String applicantName;
|
private String applicantName;
|
||||||
|
|
||||||
|
private String applicantAccount;
|
||||||
|
|
||||||
private Date submittedAt;
|
private Date submittedAt;
|
||||||
|
|
||||||
private Date finishedAt;
|
private Date finishedAt;
|
||||||
@@ -137,6 +139,14 @@ public class ApprovalInstanceDetailVo {
|
|||||||
this.applicantName = applicantName;
|
this.applicantName = applicantName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getApplicantAccount() {
|
||||||
|
return applicantAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setApplicantAccount(String applicantAccount) {
|
||||||
|
this.applicantAccount = applicantAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public Date getSubmittedAt() {
|
public Date getSubmittedAt() {
|
||||||
return submittedAt;
|
return submittedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public class ApprovalLogVo {
|
|||||||
|
|
||||||
private BigInteger operatorId;
|
private BigInteger operatorId;
|
||||||
|
|
||||||
|
private String operatorAccount;
|
||||||
|
|
||||||
private String operatorName;
|
private String operatorName;
|
||||||
|
|
||||||
private Date created;
|
private Date created;
|
||||||
@@ -45,6 +47,14 @@ public class ApprovalLogVo {
|
|||||||
this.operatorId = operatorId;
|
this.operatorId = operatorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOperatorAccount() {
|
||||||
|
return operatorAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOperatorAccount(String operatorAccount) {
|
||||||
|
this.operatorAccount = operatorAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOperatorName() {
|
public String getOperatorName() {
|
||||||
return operatorName;
|
return operatorName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.util.Locale;
|
|||||||
public enum ApprovalActionType {
|
public enum ApprovalActionType {
|
||||||
|
|
||||||
PUBLISH("PUBLISH"),
|
PUBLISH("PUBLISH"),
|
||||||
|
OFFLINE("OFFLINE"),
|
||||||
DELETE("DELETE");
|
DELETE("DELETE");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package tech.easyflow.approval.service;
|
package tech.easyflow.approval.service;
|
||||||
|
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
@@ -12,13 +14,10 @@ public interface ApprovalActionFacade {
|
|||||||
/**
|
/**
|
||||||
* 提交审批。
|
* 提交审批。
|
||||||
*
|
*
|
||||||
* @param resourceType 资源类型
|
* @param request 审批提交请求
|
||||||
* @param resourceId 资源 ID
|
* @return 动作执行结果
|
||||||
* @param actionType 动作类型
|
|
||||||
* @param operatorId 操作人 ID
|
|
||||||
* @return 审批实例 ID
|
|
||||||
*/
|
*/
|
||||||
BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId);
|
ApprovalActionResult submit(ApprovalSubmitRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理审批通过后的业务回调。
|
* 处理审批通过后的业务回调。
|
||||||
|
|||||||
@@ -15,4 +15,12 @@ public interface ApprovalMatchService {
|
|||||||
* @return 命中的流程详情
|
* @return 命中的流程详情
|
||||||
*/
|
*/
|
||||||
ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request);
|
ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据资源上下文匹配审批流程,未命中时返回 {@code null}。
|
||||||
|
*
|
||||||
|
* @param request 审批提交请求
|
||||||
|
* @return 命中的流程详情,未命中时返回 {@code null}
|
||||||
|
*/
|
||||||
|
ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package tech.easyflow.approval.service;
|
||||||
|
|
||||||
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批结果业务处理器。
|
||||||
|
*/
|
||||||
|
public interface ApprovalResultHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理审批通过后的业务回调。
|
||||||
|
*
|
||||||
|
* @param instance 审批实例
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param comment 审批意见
|
||||||
|
*/
|
||||||
|
void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理审批驳回后的业务回调。
|
||||||
|
*
|
||||||
|
* @param instance 审批实例
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param comment 审批意见
|
||||||
|
*/
|
||||||
|
void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理审批撤回后的业务回调。
|
||||||
|
*
|
||||||
|
* @param instance 审批实例
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param comment 审批意见
|
||||||
|
*/
|
||||||
|
void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment);
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package tech.easyflow.approval.service;
|
package tech.easyflow.approval.service;
|
||||||
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -28,34 +26,6 @@ public interface ApprovalSubjectHandler {
|
|||||||
*/
|
*/
|
||||||
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
ApprovalSubmitRequest buildSubmitRequest(BigInteger resourceId, String actionType, BigInteger operatorId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批提交完成后的回调。
|
|
||||||
*
|
|
||||||
* @param context 回调上下文
|
|
||||||
*/
|
|
||||||
void onSubmitted(ApprovalSubmitCallbackContext context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批通过后的回调。
|
|
||||||
*
|
|
||||||
* @param context 回调上下文
|
|
||||||
*/
|
|
||||||
void onApproved(ApprovalCallbackContext context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批驳回后的回调。
|
|
||||||
*
|
|
||||||
* @param context 回调上下文
|
|
||||||
*/
|
|
||||||
void onRejected(ApprovalCallbackContext context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批撤回后的回调。
|
|
||||||
*
|
|
||||||
* @param context 回调上下文
|
|
||||||
*/
|
|
||||||
void onRevoked(ApprovalCallbackContext context);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验资源是否已发布。
|
* 校验资源是否已发布。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package tech.easyflow.approval.service.impl;
|
package tech.easyflow.approval.service.impl;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalCallbackContext;
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitCallbackContext;
|
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalMatchService;
|
||||||
|
import tech.easyflow.approval.service.ApprovalResultHandler;
|
||||||
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
import tech.easyflow.approval.service.ApprovalSubjectHandler;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -22,31 +23,30 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
|||||||
|
|
||||||
private final List<ApprovalSubjectHandler> handlers;
|
private final List<ApprovalSubjectHandler> handlers;
|
||||||
private final ApprovalInstanceService approvalInstanceService;
|
private final ApprovalInstanceService approvalInstanceService;
|
||||||
|
private final ApprovalMatchService approvalMatchService;
|
||||||
|
private final ApprovalResultHandler approvalResultHandler;
|
||||||
|
|
||||||
public ApprovalActionFacadeImpl(List<ApprovalSubjectHandler> handlers,
|
public ApprovalActionFacadeImpl(List<ApprovalSubjectHandler> handlers,
|
||||||
ApprovalInstanceService approvalInstanceService) {
|
ApprovalInstanceService approvalInstanceService,
|
||||||
|
ApprovalMatchService approvalMatchService,
|
||||||
|
ApprovalResultHandler approvalResultHandler) {
|
||||||
this.handlers = handlers;
|
this.handlers = handlers;
|
||||||
this.approvalInstanceService = approvalInstanceService;
|
this.approvalInstanceService = approvalInstanceService;
|
||||||
|
this.approvalMatchService = approvalMatchService;
|
||||||
|
this.approvalResultHandler = approvalResultHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
public ApprovalActionResult submit(ApprovalSubmitRequest request) {
|
||||||
public BigInteger submit(String resourceType, BigInteger resourceId, String actionType, BigInteger operatorId) {
|
ApprovalFlowDetailVo flow = approvalMatchService.matchFlowOrNull(request);
|
||||||
ApprovalSubjectHandler handler = getHandler(resourceType);
|
if (flow == null) {
|
||||||
ApprovalSubmitRequest request = handler.buildSubmitRequest(resourceId, actionType, operatorId);
|
return ApprovalActionResult.direct();
|
||||||
|
}
|
||||||
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
BigInteger instanceId = approvalInstanceService.submitApproval(request);
|
||||||
|
return ApprovalActionResult.required(instanceId);
|
||||||
ApprovalSubmitCallbackContext context = new ApprovalSubmitCallbackContext();
|
|
||||||
context.setInstanceId(instanceId);
|
|
||||||
context.setResourceType(request.getResourceType());
|
|
||||||
context.setResourceId(request.getResourceId());
|
|
||||||
context.setActionType(request.getActionType());
|
|
||||||
context.setOperatorId(operatorId);
|
|
||||||
handler.onSubmitted(context);
|
|
||||||
return instanceId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,8 +54,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
public void handleApproved(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
approvalResultHandler.handleApproved(instance, operatorId, comment);
|
||||||
handler.onApproved(buildCallbackContext(instance, operatorId, comment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,8 +62,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
public void handleRejected(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
approvalResultHandler.handleRejected(instance, operatorId, comment);
|
||||||
handler.onRejected(buildCallbackContext(instance, operatorId, comment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,8 +70,7 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
public void handleRevoked(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
||||||
ApprovalSubjectHandler handler = getHandler(instance.getResourceType());
|
approvalResultHandler.handleRevoked(instance, operatorId, comment);
|
||||||
handler.onRevoked(buildCallbackContext(instance, operatorId, comment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,14 +82,6 @@ public class ApprovalActionFacadeImpl implements ApprovalActionFacade {
|
|||||||
handler.assertPublishedAccess(identifier, denyMessage);
|
handler.assertPublishedAccess(identifier, denyMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApprovalCallbackContext buildCallbackContext(ApprovalInstance instance, BigInteger operatorId, String comment) {
|
|
||||||
ApprovalCallbackContext context = new ApprovalCallbackContext();
|
|
||||||
context.setInstance(instance);
|
|
||||||
context.setOperatorId(operatorId);
|
|
||||||
context.setComment(comment);
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ApprovalSubjectHandler getHandler(String resourceType) {
|
private ApprovalSubjectHandler getHandler(String resourceType) {
|
||||||
String normalized = ApprovalResourceType.from(resourceType).getCode();
|
String normalized = ApprovalResourceType.from(resourceType).getCode();
|
||||||
return handlers.stream()
|
return handlers.stream()
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) {
|
public ApprovalFlowDetailVo matchFlow(ApprovalSubmitRequest request) {
|
||||||
|
ApprovalFlowDetailVo matchedFlow = matchFlowOrNull(request);
|
||||||
|
if (matchedFlow == null) {
|
||||||
|
throw new BusinessException("当前资源上下文未命中审批流程");
|
||||||
|
}
|
||||||
|
return matchedFlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ApprovalFlowDetailVo matchFlowOrNull(ApprovalSubmitRequest request) {
|
||||||
ApprovalSubmitRequest normalized = normalizeRequest(request);
|
ApprovalSubmitRequest normalized = normalizeRequest(request);
|
||||||
QueryWrapper flowWrapper = QueryWrapper.create()
|
QueryWrapper flowWrapper = QueryWrapper.create()
|
||||||
.eq(ApprovalFlow::getResourceType, normalized.getResourceType())
|
.eq(ApprovalFlow::getResourceType, normalized.getResourceType())
|
||||||
@@ -60,7 +72,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
|||||||
.eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode());
|
.eq(ApprovalFlow::getStatus, ApprovalFlowStatus.ENABLED.getCode());
|
||||||
List<ApprovalFlow> flows = approvalFlowMapper.selectListByQuery(flowWrapper);
|
List<ApprovalFlow> flows = approvalFlowMapper.selectListByQuery(flowWrapper);
|
||||||
if (CollectionUtil.isEmpty(flows)) {
|
if (CollectionUtil.isEmpty(flows)) {
|
||||||
throw new BusinessException("未找到可用的审批流程");
|
return null;
|
||||||
}
|
}
|
||||||
List<BigInteger> flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList());
|
List<BigInteger> flowIds = flows.stream().map(ApprovalFlow::getId).collect(Collectors.toList());
|
||||||
Map<BigInteger, List<ApprovalFlowScope>> scopeMap = approvalFlowScopeMapper.selectListByQuery(
|
Map<BigInteger, List<ApprovalFlowScope>> scopeMap = approvalFlowScopeMapper.selectListByQuery(
|
||||||
@@ -76,7 +88,7 @@ public class ApprovalMatchServiceImpl implements ApprovalMatchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchedFlows.isEmpty()) {
|
if (matchedFlows.isEmpty()) {
|
||||||
throw new BusinessException("当前资源上下文未命中审批流程");
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
matchedFlows.sort(Comparator
|
matchedFlows.sort(Comparator
|
||||||
|
|||||||
@@ -154,8 +154,9 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
List<ApprovalLog> logs = approvalLogMapper.selectListByQuery(
|
||||||
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
QueryWrapper.create().eq(ApprovalLog::getInstanceId, instanceId));
|
||||||
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
Map<Integer, ApprovalFlowStepVo> frozenStepMap = resolveFrozenStepMap(instance);
|
||||||
Map<BigInteger, String> accountNameMap = loadAccountNameMap(instance, tasks, logs);
|
Map<BigInteger, SysAccount> accountMap = loadAccountMap(instance, tasks, logs);
|
||||||
detail.setApplicantName(accountNameMap.get(instance.getApplicantId()));
|
detail.setApplicantName(resolveAccountName(accountMap.get(instance.getApplicantId())));
|
||||||
|
detail.setApplicantAccount(resolveAccountLoginName(accountMap.get(instance.getApplicantId())));
|
||||||
|
|
||||||
detail.setTasks(tasks.stream()
|
detail.setTasks(tasks.stream()
|
||||||
.sorted(Comparator.comparing(ApprovalTask::getStepNo))
|
.sorted(Comparator.comparing(ApprovalTask::getStepNo))
|
||||||
@@ -171,7 +172,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
taskVo.setAssigneeTargetCode(item.getAssigneeTargetCode());
|
||||||
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
taskVo.setAssigneeTargetName(item.getAssigneeTargetName());
|
||||||
taskVo.setActedBy(item.getActedBy());
|
taskVo.setActedBy(item.getActedBy());
|
||||||
taskVo.setActedByName(accountNameMap.get(item.getActedBy()));
|
taskVo.setActedByName(resolveAccountName(accountMap.get(item.getActedBy())));
|
||||||
taskVo.setActedAt(item.getActedAt());
|
taskVo.setActedAt(item.getActedAt());
|
||||||
taskVo.setComment(item.getComment());
|
taskVo.setComment(item.getComment());
|
||||||
return taskVo;
|
return taskVo;
|
||||||
@@ -185,7 +186,8 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
logVo.setId(item.getId());
|
logVo.setId(item.getId());
|
||||||
logVo.setEventType(item.getEventType());
|
logVo.setEventType(item.getEventType());
|
||||||
logVo.setOperatorId(item.getOperatorId());
|
logVo.setOperatorId(item.getOperatorId());
|
||||||
logVo.setOperatorName(accountNameMap.get(item.getOperatorId()));
|
logVo.setOperatorAccount(resolveAccountLoginName(accountMap.get(item.getOperatorId())));
|
||||||
|
logVo.setOperatorName(resolveAccountName(accountMap.get(item.getOperatorId())));
|
||||||
logVo.setCreated(item.getCreated());
|
logVo.setCreated(item.getCreated());
|
||||||
logVo.setPayloadJson(item.getPayloadJson());
|
logVo.setPayloadJson(item.getPayloadJson());
|
||||||
return logVo;
|
return logVo;
|
||||||
@@ -205,14 +207,14 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量加载审批详情里涉及到的账号显示名称。
|
* 批量加载审批详情里涉及到的账号信息。
|
||||||
*
|
*
|
||||||
* @param instance 审批实例
|
* @param instance 审批实例
|
||||||
* @param tasks 审批任务列表
|
* @param tasks 审批任务列表
|
||||||
* @param logs 审批日志列表
|
* @param logs 审批日志列表
|
||||||
* @return 账号 ID 到展示名称的映射
|
* @return 账号 ID 到账号实体的映射
|
||||||
*/
|
*/
|
||||||
private Map<BigInteger, String> loadAccountNameMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
private Map<BigInteger, SysAccount> loadAccountMap(ApprovalInstance instance, List<ApprovalTask> tasks,
|
||||||
List<ApprovalLog> logs) {
|
List<ApprovalLog> logs) {
|
||||||
Set<BigInteger> accountIds = new HashSet<>();
|
Set<BigInteger> accountIds = new HashSet<>();
|
||||||
if (instance.getApplicantId() != null) {
|
if (instance.getApplicantId() != null) {
|
||||||
@@ -232,7 +234,7 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
return sysAccountService.listByIds(accountIds).stream()
|
return sysAccountService.listByIds(accountIds).stream()
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
SysAccount::getId,
|
SysAccount::getId,
|
||||||
this::resolveAccountName,
|
account -> account,
|
||||||
(left, right) -> left,
|
(left, right) -> left,
|
||||||
LinkedHashMap::new));
|
LinkedHashMap::new));
|
||||||
}
|
}
|
||||||
@@ -256,6 +258,19 @@ public class ApprovalQueryServiceImpl implements ApprovalQueryService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析账号登录名。
|
||||||
|
*
|
||||||
|
* @param account 账号实体
|
||||||
|
* @return 登录账号
|
||||||
|
*/
|
||||||
|
private String resolveAccountLoginName(SysAccount account) {
|
||||||
|
if (account == null || !StringUtils.hasText(account.getLoginName())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return account.getLoginName().trim();
|
||||||
|
}
|
||||||
|
|
||||||
private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) {
|
private QueryWrapper buildBaseQuery(String resourceType, String actionType, String keyword) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||||
if (StringUtils.hasText(resourceType)) {
|
if (StringUtils.hasText(resourceType)) {
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import org.springframework.stereotype.Service;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +36,16 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
|||||||
@Resource
|
@Resource
|
||||||
private SysAccountRoleService sysAccountRoleService;
|
private SysAccountRoleService sysAccountRoleService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据账号查询菜单,并自动补齐已授权节点的父级菜单链。
|
||||||
|
* <p>
|
||||||
|
* 这样当角色只勾选了某个页面下的按钮权限或子能力时,
|
||||||
|
* 其所属的页面菜单仍能正常出现在侧边栏中,避免出现“有子权限但无入口”的问题。
|
||||||
|
*
|
||||||
|
* @param entity 菜单过滤条件
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @return 当前账号可访问的菜单集合
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<SysMenu> getMenusByAccountId(SysMenu entity, BigInteger accountId) {
|
public List<SysMenu> getMenusByAccountId(SysMenu entity, BigInteger accountId) {
|
||||||
// 查询用户对应角色id集合
|
// 查询用户对应角色id集合
|
||||||
@@ -48,11 +62,53 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
|
|||||||
if (CollectionUtil.isEmpty(menuIds)) {
|
if (CollectionUtil.isEmpty(menuIds)) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
List<BigInteger> fullMenuIds = collectMenuIdsWithParents(menuIds);
|
||||||
// 查询当前用户拥有的菜单
|
// 查询当前用户拥有的菜单
|
||||||
SqlOperators ops = SqlOperatorsUtil.build(SysMenu.class);
|
SqlOperators ops = SqlOperatorsUtil.build(SysMenu.class);
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, ops);
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, ops);
|
||||||
queryWrapper.in(SysMenu::getId, menuIds);
|
queryWrapper.in(SysMenu::getId, fullMenuIds);
|
||||||
queryWrapper.orderBy("sort_no asc");
|
queryWrapper.orderBy("sort_no asc");
|
||||||
return list(queryWrapper);
|
return list(queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收集菜单自身及其所有父级菜单 ID。
|
||||||
|
*
|
||||||
|
* @param menuIds 已授权的菜单 ID 列表
|
||||||
|
* @return 包含父级链路的完整菜单 ID 列表
|
||||||
|
*/
|
||||||
|
private List<BigInteger> collectMenuIdsWithParents(List<BigInteger> menuIds) {
|
||||||
|
List<SysMenu> allMenus = list();
|
||||||
|
if (CollectionUtil.isEmpty(allMenus)) {
|
||||||
|
return menuIds;
|
||||||
|
}
|
||||||
|
Map<BigInteger, SysMenu> menuMap = new HashMap<>();
|
||||||
|
for (SysMenu menu : allMenus) {
|
||||||
|
menuMap.put(menu.getId(), menu);
|
||||||
|
}
|
||||||
|
Set<BigInteger> result = new HashSet<>(menuIds);
|
||||||
|
for (BigInteger menuId : menuIds) {
|
||||||
|
appendParentMenuIds(menuId, menuMap, result);
|
||||||
|
}
|
||||||
|
return new ArrayList<>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归追加父级菜单 ID,直到根节点或无父节点为止。
|
||||||
|
*
|
||||||
|
* @param menuId 当前菜单 ID
|
||||||
|
* @param menuMap 全量菜单映射
|
||||||
|
* @param result 结果集合
|
||||||
|
*/
|
||||||
|
private void appendParentMenuIds(BigInteger menuId, Map<BigInteger, SysMenu> menuMap, Set<BigInteger> result) {
|
||||||
|
SysMenu current = menuMap.get(menuId);
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigInteger parentId = current.getParentId();
|
||||||
|
if (parentId == null || BigInteger.ZERO.equals(parentId) || !result.add(parentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appendParentMenuIds(parentId, menuMap, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
UPDATE `tb_sys_menu`
|
||||||
|
SET
|
||||||
|
`menu_url` = '/sys/approval',
|
||||||
|
`component` = '/system/approval/ApprovalManage',
|
||||||
|
`menu_icon` = 'svg:approval',
|
||||||
|
`is_show` = 1,
|
||||||
|
`modified` = NOW(),
|
||||||
|
`modified_by` = 1,
|
||||||
|
`remark` = '审批管理菜单'
|
||||||
|
WHERE `id` = 367100000000000001;
|
||||||
|
|
||||||
|
UPDATE `tb_sys_menu`
|
||||||
|
SET
|
||||||
|
`menu_type` = 1,
|
||||||
|
`menu_url` = '',
|
||||||
|
`component` = '',
|
||||||
|
`menu_icon` = '',
|
||||||
|
`is_show` = 0,
|
||||||
|
`permission_tag` = '/page/approval/flow',
|
||||||
|
`modified` = NOW(),
|
||||||
|
`modified_by` = 1,
|
||||||
|
`remark` = '审批管理-流程配置页签权限'
|
||||||
|
WHERE `id` = 367100000000000002;
|
||||||
|
|
||||||
|
UPDATE `tb_sys_menu`
|
||||||
|
SET
|
||||||
|
`menu_type` = 1,
|
||||||
|
`menu_url` = '',
|
||||||
|
`component` = '',
|
||||||
|
`menu_icon` = '',
|
||||||
|
`is_show` = 0,
|
||||||
|
`permission_tag` = '/page/approval/pending',
|
||||||
|
`modified` = NOW(),
|
||||||
|
`modified_by` = 1,
|
||||||
|
`remark` = '审批管理-待审批页签权限'
|
||||||
|
WHERE `id` = 367100000000000003;
|
||||||
|
|
||||||
|
UPDATE `tb_sys_menu`
|
||||||
|
SET
|
||||||
|
`menu_type` = 1,
|
||||||
|
`menu_url` = '',
|
||||||
|
`component` = '',
|
||||||
|
`menu_icon` = '',
|
||||||
|
`is_show` = 0,
|
||||||
|
`permission_tag` = '/page/approval/processed',
|
||||||
|
`modified` = NOW(),
|
||||||
|
`modified_by` = 1,
|
||||||
|
`remark` = '审批管理-已审批页签权限'
|
||||||
|
WHERE `id` = 367100000000000004;
|
||||||
|
|
||||||
|
UPDATE `tb_sys_menu`
|
||||||
|
SET
|
||||||
|
`menu_type` = 1,
|
||||||
|
`menu_url` = '',
|
||||||
|
`component` = '',
|
||||||
|
`menu_icon` = '',
|
||||||
|
`is_show` = 0,
|
||||||
|
`permission_tag` = '/page/approval/initiated',
|
||||||
|
`modified` = NOW(),
|
||||||
|
`modified_by` = 1,
|
||||||
|
`remark` = '审批管理-我发起页签权限'
|
||||||
|
WHERE `id` = 367100000000000005;
|
||||||
@@ -61,6 +61,14 @@ export const submitBotPublishApproval = (id: string) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 提交 Bot 下线审批 */
|
||||||
|
export const submitBotOfflineApproval = (id: string) => {
|
||||||
|
return api.post<RequestResult<number | string>>(
|
||||||
|
'/api/v1/bot/submitOfflineApproval',
|
||||||
|
{ id },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/** 提交 Bot 删除审批 */
|
/** 提交 Bot 删除审批 */
|
||||||
export const submitBotDeleteApproval = (id: string) => {
|
export const submitBotDeleteApproval = (id: string) => {
|
||||||
return api.post<RequestResult<number | string>>(
|
return api.post<RequestResult<number | string>>(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface ActionButton {
|
|||||||
permission?: string;
|
permission?: string;
|
||||||
placement?: ActionPlacement;
|
placement?: ActionPlacement;
|
||||||
tone?: ActionTone;
|
tone?: ActionTone;
|
||||||
|
visible?: ((row: any) => boolean) | boolean;
|
||||||
onClick: (row: any) => void;
|
onClick: (row: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,13 @@ function hasPermission(permission?: string) {
|
|||||||
return !permission || hasAccessByCodes([permission]);
|
return !permission || hasAccessByCodes([permission]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isActionVisible(action: ActionButton, row: any) {
|
||||||
|
if (typeof action.visible === 'function') {
|
||||||
|
return action.visible(row);
|
||||||
|
}
|
||||||
|
return action.visible !== false;
|
||||||
|
}
|
||||||
|
|
||||||
const resolvedPrimaryAction = computed(() => {
|
const resolvedPrimaryAction = computed(() => {
|
||||||
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
|
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -109,24 +117,6 @@ const resolvedActions = computed<ResolvedActionButton[]>(() => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const inlineActions = computed(() => {
|
|
||||||
return resolvedActions.value.filter(
|
|
||||||
(action) => action.placement === 'inline',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const menuActions = computed(() => {
|
|
||||||
return resolvedActions.value.filter((action) => action.placement === 'menu');
|
|
||||||
});
|
|
||||||
|
|
||||||
const showFooter = computed(() => {
|
|
||||||
return Boolean(
|
|
||||||
resolvedPrimaryAction.value ||
|
|
||||||
inlineActions.value.length > 0 ||
|
|
||||||
menuActions.value.length > 0,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function handlePrimaryAction(item: any) {
|
function handlePrimaryAction(item: any) {
|
||||||
resolvedPrimaryAction.value?.onClick(item);
|
resolvedPrimaryAction.value?.onClick(item);
|
||||||
}
|
}
|
||||||
@@ -139,6 +129,24 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
|||||||
function resolveActionText(action: ActionButton, item: any) {
|
function resolveActionText(action: ActionButton, item: any) {
|
||||||
return typeof action.text === 'function' ? action.text(item) : action.text;
|
return typeof action.text === 'function' ? action.text(item) : action.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveInlineActions(item: any) {
|
||||||
|
return resolvedActions.value.filter(
|
||||||
|
(action) => action.placement === 'inline' && isActionVisible(action, item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMenuActions(item: any) {
|
||||||
|
return resolvedActions.value.filter(
|
||||||
|
(action) => action.placement === 'menu' && isActionVisible(action, item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasVisibleActions(item: any) {
|
||||||
|
return (
|
||||||
|
resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -198,7 +206,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="showFooter" #footer>
|
<template v-if="resolvedPrimaryAction || hasVisibleActions(item)" #footer>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
|
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
|
||||||
<div class="primary-label">
|
<div class="primary-label">
|
||||||
@@ -214,12 +222,12 @@ function resolveActionText(action: ActionButton, item: any) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="inlineActions.length > 0 || menuActions.length > 0"
|
v-if="hasVisibleActions(item)"
|
||||||
class="card-actions"
|
class="card-actions"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-for="(action, actionIndex) in inlineActions"
|
v-for="(action, actionIndex) in resolveInlineActions(item)"
|
||||||
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
||||||
:icon="typeof action.icon === 'string' ? undefined : action.icon"
|
:icon="typeof action.icon === 'string' ? undefined : action.icon"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -235,7 +243,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
|||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
||||||
<ElDropdown
|
<ElDropdown
|
||||||
v-if="menuActions.length > 0"
|
v-if="resolveMenuActions(item).length > 0"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
@@ -248,7 +256,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<ElDropdownMenu>
|
<ElDropdownMenu>
|
||||||
<ElDropdownItem
|
<ElDropdownItem
|
||||||
v-for="(action, actionIndex) in menuActions"
|
v-for="(action, actionIndex) in resolveMenuActions(item)"
|
||||||
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
||||||
:class="{
|
:class="{
|
||||||
'card-menu-item--danger': action.tone === 'danger',
|
'card-menu-item--danger': action.tone === 'danger',
|
||||||
|
|||||||
@@ -65,10 +65,16 @@
|
|||||||
"publishStatusDraft": "Draft",
|
"publishStatusDraft": "Draft",
|
||||||
"publishStatusPublishPending": "Publish Pending",
|
"publishStatusPublishPending": "Publish Pending",
|
||||||
"publishStatusPublished": "Published",
|
"publishStatusPublished": "Published",
|
||||||
|
"publishStatusOfflinePending": "Offline Pending",
|
||||||
|
"publishStatusOffline": "Offline",
|
||||||
"publishStatusDeletePending": "Delete Pending",
|
"publishStatusDeletePending": "Delete Pending",
|
||||||
"publishStatusLabel": "Release",
|
"publishStatusLabel": "Release",
|
||||||
"submitPublishApprovalConfirm": "The current draft will enter the publish approval flow. It becomes externally available only after approval.",
|
"submitPublishApprovalConfirm": "Publish the current workflow now?",
|
||||||
"submitDeleteApprovalConfirm": "The workflow will enter the delete approval flow. It will be physically deleted only after approval.",
|
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
|
||||||
|
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
|
||||||
|
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
||||||
|
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
||||||
|
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
||||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||||
"check": "Check",
|
"check": "Check",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
|
"offline": "Offline",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"addFlow": "New Flow",
|
"addFlow": "New Flow",
|
||||||
"editFlow": "Edit Flow",
|
"editFlow": "Edit Flow",
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
"actedAt": "Acted At",
|
"actedAt": "Acted At",
|
||||||
"comment": "Comment",
|
"comment": "Comment",
|
||||||
"eventType": "Event Type",
|
"eventType": "Event Type",
|
||||||
"operatorId": "Operator ID",
|
"operatorId": "Operator Account",
|
||||||
"operatorName": "Operator Name",
|
"operatorName": "Operator Name",
|
||||||
"createdAt": "Created At",
|
"createdAt": "Created At",
|
||||||
"eventInfo": "Event Info",
|
"eventInfo": "Event Info",
|
||||||
|
|||||||
@@ -18,10 +18,16 @@
|
|||||||
"publishStatusPublishPendingDesc": "The assistant will switch to the new release after approval.",
|
"publishStatusPublishPendingDesc": "The assistant will switch to the new release after approval.",
|
||||||
"publishStatusPublished": "Published",
|
"publishStatusPublished": "Published",
|
||||||
"publishStatusPublishedDesc": "The current release is externally available. Ongoing edits still stay in draft.",
|
"publishStatusPublishedDesc": "The current release is externally available. Ongoing edits still stay in draft.",
|
||||||
|
"publishStatusOfflinePending": "Offline Pending",
|
||||||
|
"publishStatusOfflinePendingDesc": "The current release stays available until approval completes, but it is hidden from new binding candidates.",
|
||||||
|
"publishStatusOffline": "Offline",
|
||||||
|
"publishStatusOfflineDesc": "The current release is offline. External chat, Public API, and new bindings are unavailable.",
|
||||||
"publishStatusDeletePending": "Delete Pending",
|
"publishStatusDeletePending": "Delete Pending",
|
||||||
"publishStatusDeletePendingDesc": "The current release remains available, but it is no longer offered as a new binding candidate.",
|
"publishStatusDeletePendingDesc": "The current release remains available, but it is no longer offered as a new binding candidate.",
|
||||||
"submitPublishApprovalConfirm": "Submit the current draft to publish approval. It becomes externally available only after approval.",
|
"submitPublishApprovalConfirm": "Publish the current assistant now?",
|
||||||
"submitDeleteApprovalConfirm": "Submit the bot to delete approval. It will be physically deleted only after approval.",
|
"submitRepublishApprovalConfirm": "Republish the current assistant now?",
|
||||||
|
"submitOfflineApprovalConfirm": "Take the current assistant offline?",
|
||||||
|
"submitDeleteApprovalConfirm": "Delete the current assistant?",
|
||||||
"publishPendingHint": "There is already an approval in progress for this bot.",
|
"publishPendingHint": "There is already an approval in progress for this bot.",
|
||||||
"deletePendingHint": "There is already an approval in progress for this bot.",
|
"deletePendingHint": "There is already an approval in progress for this bot.",
|
||||||
"publishRequiredHint": "There is no released version yet. Submit publish approval first.",
|
"publishRequiredHint": "There is no released version yet. Submit publish approval first.",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@
|
|||||||
"markAsResolved": "MarkAsResolved",
|
"markAsResolved": "MarkAsResolved",
|
||||||
"optimizing": "Optimizing",
|
"optimizing": "Optimizing",
|
||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
|
"publish": "Publish",
|
||||||
"republish": "Republish",
|
"republish": "Republish",
|
||||||
|
"offline": "Offline",
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"more": "Mode",
|
"more": "Mode",
|
||||||
"submitDeleteApproval": "Submit Delete Approval",
|
"submitDeleteApproval": "Submit Delete Approval",
|
||||||
|
|||||||
@@ -38,10 +38,18 @@
|
|||||||
"publishStatusDraft": "Draft",
|
"publishStatusDraft": "Draft",
|
||||||
"publishStatusPublishPending": "Publish Pending",
|
"publishStatusPublishPending": "Publish Pending",
|
||||||
"publishStatusPublished": "Published",
|
"publishStatusPublished": "Published",
|
||||||
|
"publishStatusOfflinePending": "Offline Pending",
|
||||||
|
"publishStatusOffline": "Offline",
|
||||||
"publishStatusDeletePending": "Delete Pending",
|
"publishStatusDeletePending": "Delete Pending",
|
||||||
"publishStatusLabel": "Release",
|
"publishStatusLabel": "Release",
|
||||||
"submitPublishApprovalConfirm": "The knowledge base will enter the publish approval flow. It can be referenced by bots only after approval.",
|
"submitPublishApprovalConfirm": "Publish the current knowledge base now?",
|
||||||
"submitDeleteApprovalConfirm": "The knowledge base will enter the delete approval flow. It will be physically deleted only after approval.",
|
"submitRepublishApprovalConfirm": "Republish the current knowledge base now?",
|
||||||
|
"submitOfflineApprovalConfirm": "Take the current knowledge base offline?",
|
||||||
|
"submitDeleteApprovalConfirm": "Delete the current knowledge base?",
|
||||||
|
"offlineImpactBoundBotsIntro": "This knowledge base is currently bound to the following bots:",
|
||||||
|
"offlineImpactBoundBotsFooter": "After the knowledge base goes offline, the system will automatically remove it from these bots.",
|
||||||
|
"offlineImpactWorkflowBlockedIntro": "This knowledge base is still used by the following workflows:",
|
||||||
|
"offlineImpactWorkflowBlockedFooter": "Please update those workflow nodes before taking the knowledge base offline.",
|
||||||
"publishPendingHint": "There is already an approval in progress for this knowledge base.",
|
"publishPendingHint": "There is already an approval in progress for this knowledge base.",
|
||||||
"deletePendingHint": "There is already an approval in progress for this knowledge base.",
|
"deletePendingHint": "There is already an approval in progress for this knowledge base.",
|
||||||
"createdModifyTime": "Creation/update time",
|
"createdModifyTime": "Creation/update time",
|
||||||
|
|||||||
@@ -65,10 +65,16 @@
|
|||||||
"publishStatusDraft": "草稿",
|
"publishStatusDraft": "草稿",
|
||||||
"publishStatusPublishPending": "发布审批中",
|
"publishStatusPublishPending": "发布审批中",
|
||||||
"publishStatusPublished": "已发布",
|
"publishStatusPublished": "已发布",
|
||||||
|
"publishStatusOfflinePending": "下线审批中",
|
||||||
|
"publishStatusOffline": "已下线",
|
||||||
"publishStatusDeletePending": "删除审批中",
|
"publishStatusDeletePending": "删除审批中",
|
||||||
"publishStatusLabel": "发布状态",
|
"publishStatusLabel": "发布状态",
|
||||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后新版本才会正式对外可用。",
|
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
|
||||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
|
||||||
|
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
|
||||||
|
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
||||||
|
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
||||||
|
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
||||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||||
"check": "检查",
|
"check": "检查",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"publish": "发布",
|
"publish": "发布",
|
||||||
|
"offline": "下线",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"addFlow": "新建流程",
|
"addFlow": "新建流程",
|
||||||
"editFlow": "编辑流程",
|
"editFlow": "编辑流程",
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
"actedAt": "处理时间",
|
"actedAt": "处理时间",
|
||||||
"comment": "处理意见",
|
"comment": "处理意见",
|
||||||
"eventType": "事件类型",
|
"eventType": "事件类型",
|
||||||
"operatorId": "操作人ID",
|
"operatorId": "操作人账号",
|
||||||
"operatorName": "操作人名称",
|
"operatorName": "操作人名称",
|
||||||
"createdAt": "创建时间",
|
"createdAt": "创建时间",
|
||||||
"eventInfo": "事件信息",
|
"eventInfo": "事件信息",
|
||||||
|
|||||||
@@ -18,10 +18,16 @@
|
|||||||
"publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。",
|
"publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。",
|
||||||
"publishStatusPublished": "已发布",
|
"publishStatusPublished": "已发布",
|
||||||
"publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。",
|
"publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。",
|
||||||
|
"publishStatusOfflinePending": "下线审批中",
|
||||||
|
"publishStatusOfflinePendingDesc": "审批完成前当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||||
|
"publishStatusOffline": "已下线",
|
||||||
|
"publishStatusOfflineDesc": "当前正式版本已下线,外链聊天、Public API 和新的资源绑定都不可用。",
|
||||||
"publishStatusDeletePending": "删除审批中",
|
"publishStatusDeletePending": "删除审批中",
|
||||||
"publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
"publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后聊天助手才会正式对外可用。",
|
"submitPublishApprovalConfirm": "确认发布当前聊天助手吗?",
|
||||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
"submitRepublishApprovalConfirm": "确认重新发布当前聊天助手吗?",
|
||||||
|
"submitOfflineApprovalConfirm": "确认下线当前聊天助手吗?",
|
||||||
|
"submitDeleteApprovalConfirm": "确认删除当前聊天助手吗?",
|
||||||
"publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
"publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||||
"deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
"deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||||
"publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。",
|
"publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。",
|
||||||
|
|||||||
@@ -41,7 +41,9 @@
|
|||||||
"markAsResolved": "标记已处理",
|
"markAsResolved": "标记已处理",
|
||||||
"optimizing": "正在优化中...",
|
"optimizing": "正在优化中...",
|
||||||
"regenerate": "重新生成",
|
"regenerate": "重新生成",
|
||||||
|
"publish": "发布",
|
||||||
"republish": "重新发布",
|
"republish": "重新发布",
|
||||||
|
"offline": "下线",
|
||||||
"hide": "隐藏",
|
"hide": "隐藏",
|
||||||
"more": "更多",
|
"more": "更多",
|
||||||
"submitDeleteApproval": "提交删除审批",
|
"submitDeleteApproval": "提交删除审批",
|
||||||
|
|||||||
@@ -38,10 +38,18 @@
|
|||||||
"publishStatusDraft": "草稿",
|
"publishStatusDraft": "草稿",
|
||||||
"publishStatusPublishPending": "发布审批中",
|
"publishStatusPublishPending": "发布审批中",
|
||||||
"publishStatusPublished": "已发布",
|
"publishStatusPublished": "已发布",
|
||||||
|
"publishStatusOfflinePending": "下线审批中",
|
||||||
|
"publishStatusOffline": "已下线",
|
||||||
"publishStatusDeletePending": "删除审批中",
|
"publishStatusDeletePending": "删除审批中",
|
||||||
"publishStatusLabel": "发布状态",
|
"publishStatusLabel": "发布状态",
|
||||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后该知识库才可作为正式版本被聊天助手引用。",
|
"submitPublishApprovalConfirm": "确认发布当前知识库吗?",
|
||||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
"submitRepublishApprovalConfirm": "确认重新发布当前知识库吗?",
|
||||||
|
"submitOfflineApprovalConfirm": "确认下线当前知识库吗?",
|
||||||
|
"submitDeleteApprovalConfirm": "确认删除当前知识库吗?",
|
||||||
|
"offlineImpactBoundBotsIntro": "当前知识库被以下聊天助手绑定:",
|
||||||
|
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该知识库。",
|
||||||
|
"offlineImpactWorkflowBlockedIntro": "当前知识库仍被以下工作流使用:",
|
||||||
|
"offlineImpactWorkflowBlockedFooter": "请先在工作流中调整相关知识库节点后再下线。",
|
||||||
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||||
"deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
"deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||||
"createdModifyTime": "创建/更新时间",
|
"createdModifyTime": "创建/更新时间",
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ function setupAccessGuard(router: Router) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 页面菜单与按钮权限码是两套数据源。每次重新构建动态菜单时,
|
||||||
|
// 同步刷新一次 accessCodes,避免后端权限模型调整后页面仍持有旧按钮权限。
|
||||||
|
await authStore.fetchAccessCodes();
|
||||||
|
|
||||||
// 生成路由表
|
// 生成路由表
|
||||||
// 当前登录用户拥有的角色标识列表
|
// 当前登录用户拥有的角色标识列表
|
||||||
const userRoles = userInfo.roles ?? [];
|
const userRoles = userInfo.roles ?? [];
|
||||||
|
|||||||
@@ -3,6 +3,74 @@ import type { RouteRecordRaw } from 'vue-router';
|
|||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
name: 'ApprovalFlowPage',
|
||||||
|
path: '/sys/approval/flow',
|
||||||
|
redirect: {
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: {
|
||||||
|
tab: 'flow',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: $t('approval.tab.flow'),
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInTab: true,
|
||||||
|
activePath: '/sys/approval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ApprovalPendingPage',
|
||||||
|
path: '/sys/approval/pending',
|
||||||
|
redirect: {
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: {
|
||||||
|
tab: 'pending',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: $t('approval.tab.pending'),
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInTab: true,
|
||||||
|
activePath: '/sys/approval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ApprovalProcessedPage',
|
||||||
|
path: '/sys/approval/processed',
|
||||||
|
redirect: {
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: {
|
||||||
|
tab: 'processed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: $t('approval.tab.processed'),
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInTab: true,
|
||||||
|
activePath: '/sys/approval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ApprovalInitiatedPage',
|
||||||
|
path: '/sys/approval/initiated',
|
||||||
|
redirect: {
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: {
|
||||||
|
tab: 'initiated',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: $t('approval.tab.initiated'),
|
||||||
|
hideInMenu: true,
|
||||||
|
hideInBreadcrumb: true,
|
||||||
|
hideInTab: true,
|
||||||
|
activePath: '/sys/approval',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'ApprovalDetail',
|
name: 'ApprovalDetail',
|
||||||
path: '/sys/approval/detail/:id',
|
path: '/sys/approval/detail/:id',
|
||||||
|
|||||||
@@ -154,6 +154,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
return userInfo;
|
return userInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAccessCodes() {
|
||||||
|
const accessCodes = await getAccessCodesApi();
|
||||||
|
accessStore.setAccessCodes(accessCodes);
|
||||||
|
return accessCodes;
|
||||||
|
}
|
||||||
|
|
||||||
function $reset() {
|
function $reset() {
|
||||||
loginLoading.value = false;
|
loginLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
$reset,
|
$reset,
|
||||||
authDevLogin,
|
authDevLogin,
|
||||||
authLogin,
|
authLogin,
|
||||||
|
fetchAccessCodes,
|
||||||
fetchUserInfo,
|
fetchUserInfo,
|
||||||
loginLoading,
|
loginLoading,
|
||||||
logout,
|
logout,
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api';
|
import {
|
||||||
|
submitBotDeleteApproval,
|
||||||
|
submitBotOfflineApproval,
|
||||||
|
submitBotPublishApproval,
|
||||||
|
} from '#/api';
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||||
@@ -34,9 +38,12 @@ import CardList from '#/components/page/CardList.vue';
|
|||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import PageSide from '#/components/page/PageSide.vue';
|
import PageSide from '#/components/page/PageSide.vue';
|
||||||
import {
|
import {
|
||||||
|
canAiResourceDelete,
|
||||||
|
canAiResourceOffline,
|
||||||
|
canAiResourcePublish,
|
||||||
|
canAiResourceRepublish,
|
||||||
isAiResourceApprovalPending,
|
isAiResourceApprovalPending,
|
||||||
isAiResourcePublished,
|
resolveAiResourceDisplayStatus,
|
||||||
normalizeAiPublishStatus,
|
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
|
|
||||||
@@ -105,35 +112,57 @@ const actions: ActionButton[] = [
|
|||||||
{
|
{
|
||||||
icon: Promotion,
|
icon: Promotion,
|
||||||
text: (row: BotInfo) =>
|
text: (row: BotInfo) =>
|
||||||
isAiResourcePublished(row.publishStatus)
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||||
? $t('button.republish')
|
? $t('button.republish')
|
||||||
: $t('button.submitPublishApproval'),
|
: $t('button.publish'),
|
||||||
permission: '/api/v1/bot/save',
|
permission: '/api/v1/bot/save',
|
||||||
placement: 'inline',
|
placement: 'inline',
|
||||||
|
visible: (row: BotInfo) =>
|
||||||
|
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||||
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick(row: BotInfo) {
|
onClick(row: BotInfo) {
|
||||||
handleSubmitPublishApproval(row);
|
handlePublishAction(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Promotion,
|
||||||
|
text: $t('button.offline'),
|
||||||
|
permission: '/api/v1/bot/save',
|
||||||
|
placement: 'menu',
|
||||||
|
visible: (row: BotInfo) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||||
|
onClick(row: BotInfo) {
|
||||||
|
handleOfflineAction(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Delete,
|
icon: Delete,
|
||||||
text: $t('button.submitDeleteApproval'),
|
text: $t('button.delete'),
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
permission: '/api/v1/bot/remove',
|
permission: '/api/v1/bot/remove',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
|
visible: (row: BotInfo) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick(row: BotInfo) {
|
onClick(row: BotInfo) {
|
||||||
handleSubmitDeleteApproval(row);
|
handleSubmitDeleteApproval(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
function isRepublishAction(bot: BotInfo) {
|
||||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
return canAiResourceRepublish(bot.displayPublishStatus, bot.publishStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePublishAction = async (bot: BotInfo) => {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('bot.publishPendingHint'));
|
ElMessage.warning($t('bot.publishPendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('bot.submitPublishApprovalConfirm'),
|
isRepublishAction(bot)
|
||||||
|
? $t('bot.submitRepublishApprovalConfirm')
|
||||||
|
: $t('bot.submitPublishApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -150,8 +179,36 @@ const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
|||||||
pageDataRef.value?.reload?.();
|
pageDataRef.value?.reload?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const handleOfflineAction = async (bot: BotInfo) => {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||||
|
) {
|
||||||
|
ElMessage.warning($t('bot.publishPendingHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
$t('bot.submitOfflineApprovalConfirm'),
|
||||||
|
$t('message.noticeTitle'),
|
||||||
|
{
|
||||||
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
cancelButtonText: $t('button.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await submitBotOfflineApproval(String(bot.id));
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
|
pageDataRef.value?.reload?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
||||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
if (
|
||||||
|
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('bot.deletePendingHint'));
|
ElMessage.warning($t('bot.deletePendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,8 +231,11 @@ const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
|||||||
pageDataRef.value?.reload?.();
|
pageDataRef.value?.reload?.();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function resolvePublishStatusMeta(status?: string) {
|
function resolvePublishStatusMetaByInstance(
|
||||||
switch (normalizeAiPublishStatus(status)) {
|
displayPublishStatus?: string,
|
||||||
|
publishStatus?: string,
|
||||||
|
) {
|
||||||
|
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||||
case 'PUBLISHED':
|
case 'PUBLISHED':
|
||||||
return {
|
return {
|
||||||
label: $t('bot.publishStatusPublished'),
|
label: $t('bot.publishStatusPublished'),
|
||||||
@@ -186,6 +246,16 @@ function resolvePublishStatusMeta(status?: string) {
|
|||||||
label: $t('bot.publishStatusPublishPending'),
|
label: $t('bot.publishStatusPublishPending'),
|
||||||
type: 'warning' as const,
|
type: 'warning' as const,
|
||||||
};
|
};
|
||||||
|
case 'OFFLINE_PENDING':
|
||||||
|
return {
|
||||||
|
label: $t('bot.publishStatusOfflinePending'),
|
||||||
|
type: 'warning' as const,
|
||||||
|
};
|
||||||
|
case 'OFFLINE':
|
||||||
|
return {
|
||||||
|
label: $t('bot.publishStatusOffline'),
|
||||||
|
type: 'info' as const,
|
||||||
|
};
|
||||||
case 'DELETE_PENDING':
|
case 'DELETE_PENDING':
|
||||||
return {
|
return {
|
||||||
label: $t('bot.publishStatusDeletePending'),
|
label: $t('bot.publishStatusDeletePending'),
|
||||||
@@ -378,9 +448,9 @@ const getSideList = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
effect="plain"
|
effect="plain"
|
||||||
round
|
round
|
||||||
:type="resolvePublishStatusMeta(item.publishStatus).type"
|
:type="resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).type"
|
||||||
>
|
>
|
||||||
{{ resolvePublishStatusMeta(item.publishStatus).label }}
|
{{ resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label }}
|
||||||
</ElTag>
|
</ElTag>
|
||||||
</template>
|
</template>
|
||||||
</CardList>
|
</CardList>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { tryit } from 'radash';
|
|||||||
import {
|
import {
|
||||||
getPerQuestions,
|
getPerQuestions,
|
||||||
submitBotDeleteApproval,
|
submitBotDeleteApproval,
|
||||||
|
submitBotOfflineApproval,
|
||||||
submitBotPublishApproval,
|
submitBotPublishApproval,
|
||||||
updateBotApi,
|
updateBotApi,
|
||||||
updateBotOptions,
|
updateBotOptions,
|
||||||
@@ -56,10 +57,13 @@ import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDa
|
|||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||||
import {
|
import {
|
||||||
|
canAiResourceDelete,
|
||||||
|
canAiResourceOffline,
|
||||||
|
canAiResourcePublish,
|
||||||
|
canAiResourceRepublish,
|
||||||
isAiResourceApprovalPending,
|
isAiResourceApprovalPending,
|
||||||
isAiResourceExternallyVisible,
|
isAiResourceExternallyVisible,
|
||||||
isAiResourcePublished,
|
resolveAiResourceDisplayStatus,
|
||||||
normalizeAiPublishStatus,
|
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
interface SelectedMcpTool {
|
interface SelectedMcpTool {
|
||||||
@@ -163,18 +167,42 @@ const publicChatUrl = computed(() => {
|
|||||||
const publicChatEmbedUrl = computed(() => {
|
const publicChatEmbedUrl = computed(() => {
|
||||||
return buildPublicChatUrl(true);
|
return buildPublicChatUrl(true);
|
||||||
});
|
});
|
||||||
|
const botDisplayPublishStatus = computed(() =>
|
||||||
|
resolveAiResourceDisplayStatus(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const botApprovalPending = computed(() =>
|
||||||
|
isAiResourceApprovalPending(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
const publishStatusMeta = computed<{
|
const publishStatusMeta = computed<{
|
||||||
description: string;
|
description: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: 'danger' | 'info' | 'success' | 'warning';
|
type: 'danger' | 'info' | 'success' | 'warning';
|
||||||
}>(() => {
|
}>(() => {
|
||||||
switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) {
|
switch (botDisplayPublishStatus.value) {
|
||||||
case 'PUBLISHED':
|
case 'PUBLISHED':
|
||||||
return {
|
return {
|
||||||
label: $t('bot.publishStatusPublished'),
|
label: $t('bot.publishStatusPublished'),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
description: $t('bot.publishStatusPublishedDesc'),
|
description: $t('bot.publishStatusPublishedDesc'),
|
||||||
};
|
};
|
||||||
|
case 'OFFLINE_PENDING':
|
||||||
|
return {
|
||||||
|
label: $t('bot.publishStatusOfflinePending'),
|
||||||
|
type: 'warning',
|
||||||
|
description: $t('bot.publishStatusOfflinePendingDesc'),
|
||||||
|
};
|
||||||
|
case 'OFFLINE':
|
||||||
|
return {
|
||||||
|
label: $t('bot.publishStatusOffline'),
|
||||||
|
type: 'info',
|
||||||
|
description: $t('bot.publishStatusOfflineDesc'),
|
||||||
|
};
|
||||||
case 'PUBLISH_PENDING':
|
case 'PUBLISH_PENDING':
|
||||||
return {
|
return {
|
||||||
label: $t('bot.publishStatusPublishPending'),
|
label: $t('bot.publishStatusPublishPending'),
|
||||||
@@ -199,9 +227,44 @@ const canUsePublicAccess = computed(() =>
|
|||||||
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
|
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
|
||||||
);
|
);
|
||||||
const publishPrimaryActionLabel = computed(() =>
|
const publishPrimaryActionLabel = computed(() =>
|
||||||
isAiResourcePublished(botInfo.value?.publishStatus)
|
canAiResourceRepublish(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
? $t('button.republish')
|
? $t('button.republish')
|
||||||
: $t('button.submitPublishApproval'),
|
: $t('button.publish'),
|
||||||
|
);
|
||||||
|
const canShowPublishPrimaryAction = computed(() =>
|
||||||
|
canAiResourcePublish(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
) ||
|
||||||
|
canAiResourceRepublish(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const secondaryActionLabel = computed(() => {
|
||||||
|
if (
|
||||||
|
canAiResourceOffline(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return $t('button.offline');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
canAiResourceDelete(
|
||||||
|
botInfo.value?.displayPublishStatus,
|
||||||
|
botInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return $t('button.delete');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
const canShowSecondaryAction = computed(() =>
|
||||||
|
Boolean(secondaryActionLabel.value),
|
||||||
);
|
);
|
||||||
const iframeCode = computed(() => {
|
const iframeCode = computed(() => {
|
||||||
if (!publicChatEmbedUrl.value) {
|
if (!publicChatEmbedUrl.value) {
|
||||||
@@ -820,17 +883,24 @@ const handleDeletePresetQuestion = (item: any) => {
|
|||||||
const handlePublishWx = () => {
|
const handlePublishWx = () => {
|
||||||
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
|
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
|
||||||
};
|
};
|
||||||
const handleSubmitPublishApproval = async () => {
|
const handleLifecycleAction = async () => {
|
||||||
if (!botInfo.value) {
|
if (!botInfo.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
if (
|
||||||
|
botApprovalPending.value
|
||||||
|
) {
|
||||||
ElMessage.warning($t('bot.publishPendingHint'));
|
ElMessage.warning($t('bot.publishPendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('bot.submitPublishApprovalConfirm'),
|
canAiResourceRepublish(
|
||||||
|
botInfo.value.displayPublishStatus,
|
||||||
|
botInfo.value.publishStatus,
|
||||||
|
)
|
||||||
|
? $t('bot.submitRepublishApprovalConfirm')
|
||||||
|
: $t('bot.submitPublishApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -849,17 +919,32 @@ const handleSubmitPublishApproval = async () => {
|
|||||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleSubmitDeleteApproval = async () => {
|
const handleSecondaryAction = async () => {
|
||||||
if (!botInfo.value) {
|
if (!botInfo.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
if (
|
||||||
ElMessage.warning($t('bot.deletePendingHint'));
|
botApprovalPending.value
|
||||||
|
) {
|
||||||
|
ElMessage.warning($t('bot.publishPendingHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canOffline = canAiResourceOffline(
|
||||||
|
botInfo.value.displayPublishStatus,
|
||||||
|
botInfo.value.publishStatus,
|
||||||
|
);
|
||||||
|
const canDelete = canAiResourceDelete(
|
||||||
|
botInfo.value.displayPublishStatus,
|
||||||
|
botInfo.value.publishStatus,
|
||||||
|
);
|
||||||
|
if (!canOffline && !canDelete) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('bot.submitDeleteApprovalConfirm'),
|
canOffline
|
||||||
|
? $t('bot.submitOfflineApprovalConfirm')
|
||||||
|
: $t('bot.submitDeleteApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -870,7 +955,9 @@ const handleSubmitDeleteApproval = async () => {
|
|||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await submitBotDeleteApproval(String(botInfo.value.id));
|
const res = canOffline
|
||||||
|
? await submitBotOfflineApproval(String(botInfo.value.id))
|
||||||
|
: await submitBotDeleteApproval(String(botInfo.value.id));
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
getBotDetail();
|
getBotDetail();
|
||||||
@@ -1485,19 +1572,21 @@ const handleBasicInfoChange = async (
|
|||||||
</div>
|
</div>
|
||||||
<div class="publish-summary-actions">
|
<div class="publish-summary-actions">
|
||||||
<ElButton
|
<ElButton
|
||||||
|
v-if="canShowPublishPrimaryAction"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!hasSavePermission"
|
:disabled="!hasSavePermission"
|
||||||
@click="handleSubmitPublishApproval"
|
@click="handleLifecycleAction"
|
||||||
>
|
>
|
||||||
{{ publishPrimaryActionLabel }}
|
{{ publishPrimaryActionLabel }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
|
v-if="canShowSecondaryAction"
|
||||||
plain
|
plain
|
||||||
type="danger"
|
:type="secondaryActionLabel === $t('button.delete') ? 'danger' : 'default'"
|
||||||
:disabled="!hasSavePermission"
|
:disabled="!hasSavePermission"
|
||||||
@click="handleSubmitDeleteApproval"
|
@click="handleSecondaryAction"
|
||||||
>
|
>
|
||||||
{{ $t('button.submitDeleteApproval') }}
|
{{ secondaryActionLabel }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,9 +46,16 @@ import PageSide from '#/components/page/PageSide.vue';
|
|||||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||||
import {
|
import {
|
||||||
|
buildOfflineImpactMessage,
|
||||||
|
type OfflineImpactCheck,
|
||||||
|
} from '#/views/ai/shared/offline-impact';
|
||||||
|
import {
|
||||||
|
canAiResourceDelete,
|
||||||
|
canAiResourceOffline,
|
||||||
|
canAiResourcePublish,
|
||||||
|
canAiResourceRepublish,
|
||||||
isAiResourceApprovalPending,
|
isAiResourceApprovalPending,
|
||||||
isAiResourcePublished,
|
resolveAiResourceDisplayStatus,
|
||||||
normalizeAiPublishStatus,
|
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -186,24 +193,41 @@ const actions: ActionButton[] = [
|
|||||||
{
|
{
|
||||||
icon: Promotion,
|
icon: Promotion,
|
||||||
text: (row) =>
|
text: (row) =>
|
||||||
isAiResourcePublished(row.publishStatus)
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||||
? $t('button.republish')
|
? $t('button.republish')
|
||||||
: $t('button.submitPublishApproval'),
|
: $t('button.publish'),
|
||||||
permission: '/api/v1/documentCollection/save',
|
permission: '/api/v1/documentCollection/save',
|
||||||
placement: 'inline',
|
placement: 'inline',
|
||||||
|
visible: (row) =>
|
||||||
|
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||||
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick(row) {
|
onClick(row) {
|
||||||
if (!ensureManageKnowledgeItem(row)) {
|
if (!ensureManageKnowledgeItem(row)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
submitPublishApproval(row);
|
submitPublishAction(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: $t('button.submitDeleteApproval'),
|
icon: Promotion,
|
||||||
|
text: $t('button.offline'),
|
||||||
|
permission: '/api/v1/documentCollection/save',
|
||||||
|
placement: 'menu',
|
||||||
|
visible: (row) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||||
|
onClick(row) {
|
||||||
|
if (!ensureManageKnowledgeItem(row)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submitOfflineAction(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $t('button.delete'),
|
||||||
icon: Delete,
|
icon: Delete,
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
permission: '/api/v1/documentCollection/remove',
|
permission: '/api/v1/documentCollection/remove',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
|
visible: (row) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick(row) {
|
onClick(row) {
|
||||||
if (!ensureManageKnowledgeItem(row)) {
|
if (!ensureManageKnowledgeItem(row)) {
|
||||||
return;
|
return;
|
||||||
@@ -216,14 +240,22 @@ const actions: ActionButton[] = [
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getCategoryList();
|
getCategoryList();
|
||||||
});
|
});
|
||||||
const submitPublishApproval = async (item: any) => {
|
function isRepublishAction(item: any) {
|
||||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
return canAiResourceRepublish(item.displayPublishStatus, item.publishStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitPublishAction = async (item: any) => {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('documentCollection.submitPublishApprovalConfirm'),
|
isRepublishAction(item)
|
||||||
|
? $t('documentCollection.submitRepublishApprovalConfirm')
|
||||||
|
: $t('documentCollection.submitPublishApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -245,8 +277,71 @@ const submitPublishApproval = async (item: any) => {
|
|||||||
reloadKnowledgeList();
|
reloadKnowledgeList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const submitOfflineAction = async (item: any) => {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||||
|
) {
|
||||||
|
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const impactRes = await api.get<{
|
||||||
|
data: OfflineImpactCheck;
|
||||||
|
errorCode: number;
|
||||||
|
}>(
|
||||||
|
'/api/v1/documentCollection/offlineImpactCheck',
|
||||||
|
{
|
||||||
|
params: { id: item.id },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (impactRes.errorCode !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (impactRes.data?.hasWorkflowUsages) {
|
||||||
|
await ElMessageBox.alert(
|
||||||
|
buildOfflineImpactMessage(
|
||||||
|
$t('documentCollection.offlineImpactWorkflowBlockedIntro'),
|
||||||
|
impactRes.data.workflowUsages,
|
||||||
|
$t('documentCollection.offlineImpactWorkflowBlockedFooter'),
|
||||||
|
),
|
||||||
|
$t('message.noticeTitle'),
|
||||||
|
{
|
||||||
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
impactRes.data?.hasBotBindings
|
||||||
|
? buildOfflineImpactMessage(
|
||||||
|
$t('documentCollection.offlineImpactBoundBotsIntro'),
|
||||||
|
impactRes.data.botBindings,
|
||||||
|
$t('documentCollection.offlineImpactBoundBotsFooter'),
|
||||||
|
)
|
||||||
|
: $t('documentCollection.submitOfflineApprovalConfirm'),
|
||||||
|
$t('message.noticeTitle'),
|
||||||
|
{
|
||||||
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
cancelButtonText: $t('button.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.post('/api/v1/documentCollection/submitOfflineApproval', {
|
||||||
|
id: item.id,
|
||||||
|
});
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
|
reloadKnowledgeList();
|
||||||
|
}
|
||||||
|
};
|
||||||
const submitDeleteApproval = async (item: any) => {
|
const submitDeleteApproval = async (item: any) => {
|
||||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
if (
|
||||||
|
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -274,14 +369,29 @@ const submitDeleteApproval = async (item: any) => {
|
|||||||
reloadKnowledgeList();
|
reloadKnowledgeList();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function resolvePublishStatusMeta(status?: string) {
|
function resolvePublishStatusMeta(
|
||||||
switch (normalizeAiPublishStatus(status)) {
|
displayPublishStatus?: string,
|
||||||
|
publishStatus?: string,
|
||||||
|
) {
|
||||||
|
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||||
case 'DELETE_PENDING': {
|
case 'DELETE_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('documentCollection.publishStatusDeletePending'),
|
label: $t('documentCollection.publishStatusDeletePending'),
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'OFFLINE_PENDING': {
|
||||||
|
return {
|
||||||
|
label: $t('documentCollection.publishStatusOfflinePending'),
|
||||||
|
tone: 'pending',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'OFFLINE': {
|
||||||
|
return {
|
||||||
|
label: $t('documentCollection.publishStatusOffline'),
|
||||||
|
tone: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'PUBLISH_PENDING': {
|
case 'PUBLISH_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('documentCollection.publishStatusPublishPending'),
|
label: $t('documentCollection.publishStatusPublishPending'),
|
||||||
@@ -553,11 +663,11 @@ function changeCategory(category: any) {
|
|||||||
<template #publish>
|
<template #publish>
|
||||||
<div
|
<div
|
||||||
class="knowledge-publish-chip"
|
class="knowledge-publish-chip"
|
||||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||||
>
|
>
|
||||||
<span class="knowledge-publish-chip__dot"></span>
|
<span class="knowledge-publish-chip__dot"></span>
|
||||||
<span>{{
|
<span>{{
|
||||||
resolvePublishStatusMeta(item.publishStatus).label
|
resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).label
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,22 +1,87 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import { ElImage } from 'element-plus';
|
import { ElButton, ElEmpty, ElImage, ElScrollbar } from 'element-plus';
|
||||||
|
|
||||||
|
import { api } from '#/api/request';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openDialog,
|
openDialog,
|
||||||
});
|
});
|
||||||
const dialogVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
const data = ref<any>();
|
const data = ref<any>();
|
||||||
function openDialog(row: any) {
|
const docPreviewLoading = ref(false);
|
||||||
|
const docPreviewContent = ref('');
|
||||||
|
const docPreviewTruncated = ref(false);
|
||||||
|
const docPreviewError = ref('');
|
||||||
|
let previewRequestId = 0;
|
||||||
|
|
||||||
|
const isDocument = computed(() => data.value?.resourceType === 3);
|
||||||
|
const fileName = computed(() => {
|
||||||
|
const resourceName = data.value?.resourceName || '';
|
||||||
|
const suffix = data.value?.suffix || '';
|
||||||
|
return suffix ? `${resourceName}.${suffix}` : resourceName;
|
||||||
|
});
|
||||||
|
const previewWidth = computed(() => (isDocument.value ? 'xl' : 'md'));
|
||||||
|
|
||||||
|
async function openDialog(row: any) {
|
||||||
data.value = row;
|
data.value = row;
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
|
resetDocumentPreview();
|
||||||
|
if (row?.resourceType === 3) {
|
||||||
|
await loadDocumentPreview(row);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetDocumentPreview() {
|
||||||
|
docPreviewLoading.value = false;
|
||||||
|
docPreviewContent.value = '';
|
||||||
|
docPreviewTruncated.value = false;
|
||||||
|
docPreviewError.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDocumentPreview(row: any) {
|
||||||
|
if (!row?.id) {
|
||||||
|
docPreviewError.value = '当前素材缺少预览标识,请下载后查看';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentRequestId = ++previewRequestId;
|
||||||
|
docPreviewLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await api.get('/api/v1/resource/previewContent', {
|
||||||
|
params: { id: row.id },
|
||||||
|
});
|
||||||
|
if (currentRequestId !== previewRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docPreviewContent.value = res.data?.content || '';
|
||||||
|
docPreviewTruncated.value = !!res.data?.truncated;
|
||||||
|
if (!docPreviewContent.value) {
|
||||||
|
docPreviewError.value = '暂未提取到可预览内容,请下载后查看';
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (currentRequestId !== previewRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docPreviewError.value = '文档预览加载失败,请下载后查看';
|
||||||
|
} finally {
|
||||||
|
if (currentRequestId === previewRequestId) {
|
||||||
|
docPreviewLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSourceFile() {
|
||||||
|
if (data.value?.resourceUrl) {
|
||||||
|
window.open(data.value.resourceUrl, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -24,25 +89,119 @@ function closeDialog() {
|
|||||||
v-model:open="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
:title="$t('message.preview')"
|
:title="$t('message.preview')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
width="md"
|
:width="previewWidth"
|
||||||
:show-footer="false"
|
:show-footer="false"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center">
|
<div class="resource-preview flex justify-center">
|
||||||
<ElImage
|
<ElImage
|
||||||
v-if="data.resourceType === 0"
|
v-if="data.resourceType === 0"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
:preview-src-list="[data.resourceUrl]"
|
:preview-src-list="[data.resourceUrl]"
|
||||||
:src="data.resourceUrl"
|
:src="data.resourceUrl"
|
||||||
/>
|
/>
|
||||||
<video v-if="data.resourceType === 1" controls width="640" height="360">
|
<video
|
||||||
|
v-else-if="data.resourceType === 1"
|
||||||
|
controls
|
||||||
|
width="640"
|
||||||
|
height="360"
|
||||||
|
>
|
||||||
<source :src="data.resourceUrl" type="video/mp4" />
|
<source :src="data.resourceUrl" type="video/mp4" />
|
||||||
{{ $t('message.notVideo') }}
|
{{ $t('message.notVideo') }}
|
||||||
</video>
|
</video>
|
||||||
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
|
<audio
|
||||||
|
v-else-if="data.resourceType === 2"
|
||||||
|
controls
|
||||||
|
class="mt-8 w-full max-w-[640px]"
|
||||||
|
:src="data.resourceUrl"
|
||||||
|
>
|
||||||
{{ $t('message.notAudio') }}
|
{{ $t('message.notAudio') }}
|
||||||
</audio>
|
</audio>
|
||||||
|
<div
|
||||||
|
v-else-if="isDocument"
|
||||||
|
v-loading="docPreviewLoading"
|
||||||
|
:element-loading-text="$t('message.loading')"
|
||||||
|
class="resource-preview__document bg-background border-border w-full rounded-xl border"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="resource-preview__toolbar border-border flex items-center justify-between gap-3 border-b px-5 py-4"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate text-sm font-medium">{{ fileName }}</div>
|
||||||
|
<div
|
||||||
|
v-if="docPreviewTruncated"
|
||||||
|
class="text-muted-foreground mt-1 text-xs"
|
||||||
|
>
|
||||||
|
内容较长,当前仅展示前 20000 个字符
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElButton link type="primary" @click="openSourceFile">
|
||||||
|
{{ $t('button.download') }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<div class="resource-preview__body">
|
||||||
|
<ElEmpty
|
||||||
|
v-if="docPreviewError"
|
||||||
|
:description="docPreviewError"
|
||||||
|
class="resource-preview__empty"
|
||||||
|
>
|
||||||
|
<ElButton link type="primary" @click="openSourceFile">
|
||||||
|
{{ $t('button.download') }}
|
||||||
|
</ElButton>
|
||||||
|
</ElEmpty>
|
||||||
|
<ElScrollbar
|
||||||
|
v-else
|
||||||
|
class="resource-preview__scrollbar"
|
||||||
|
wrap-class="resource-preview__scrollbar-wrap"
|
||||||
|
>
|
||||||
|
<pre class="resource-preview__content">{{
|
||||||
|
docPreviewContent
|
||||||
|
}}</pre>
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EasyFlowPanelModal>
|
</EasyFlowPanelModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.resource-preview {
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__document {
|
||||||
|
min-height: 540px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__toolbar {
|
||||||
|
min-height: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__body {
|
||||||
|
height: 468px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__scrollbar {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.resource-preview__scrollbar-wrap) {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__content {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-family:
|
||||||
|
'SFMono-Regular', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono',
|
||||||
|
monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.75;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-preview__empty {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
|
export interface OfflineImpactBinding {
|
||||||
|
id?: number | string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OfflineImpactCheck {
|
||||||
|
canProceed: boolean;
|
||||||
|
hasBotBindings: boolean;
|
||||||
|
hasWorkflowUsages: boolean;
|
||||||
|
botBindings: OfflineImpactBinding[];
|
||||||
|
workflowUsages: OfflineImpactBinding[];
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTitle(item: OfflineImpactBinding) {
|
||||||
|
return item.title || String(item.id || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinOfflineImpactTitles(items: OfflineImpactBinding[] = []) {
|
||||||
|
return items.map(resolveTitle).filter(Boolean).join('、');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOfflineImpactMessage(
|
||||||
|
intro: string,
|
||||||
|
items: OfflineImpactBinding[] = [],
|
||||||
|
footer?: string,
|
||||||
|
) {
|
||||||
|
return h('div', [
|
||||||
|
h('p', intro),
|
||||||
|
h(
|
||||||
|
'ul',
|
||||||
|
items.map((item) => h('li', { key: String(item.id || item.title || '') }, resolveTitle(item))),
|
||||||
|
),
|
||||||
|
footer ? h('p', footer) : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
export type AiPublishStatus =
|
export type AiPublishStatus =
|
||||||
| 'DELETE_PENDING'
|
| 'DELETE_PENDING'
|
||||||
| 'DRAFT'
|
| 'DRAFT'
|
||||||
|
| 'OFFLINE'
|
||||||
|
| 'OFFLINE_PENDING'
|
||||||
| 'PUBLISHED'
|
| 'PUBLISHED'
|
||||||
| 'PUBLISH_PENDING';
|
| 'PUBLISH_PENDING';
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ export function normalizeAiPublishStatus(
|
|||||||
switch (value) {
|
switch (value) {
|
||||||
case 'PUBLISHED':
|
case 'PUBLISHED':
|
||||||
case 'PUBLISH_PENDING':
|
case 'PUBLISH_PENDING':
|
||||||
|
case 'OFFLINE':
|
||||||
|
case 'OFFLINE_PENDING':
|
||||||
case 'DELETE_PENDING':
|
case 'DELETE_PENDING':
|
||||||
return value;
|
return value;
|
||||||
default:
|
default:
|
||||||
@@ -20,6 +24,17 @@ export function normalizeAiPublishStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用于页面展示的发布状态。
|
||||||
|
* 已发布资源若存在当前审批实例,则视为“发布审批中”,用于统一状态文案与动作禁用。
|
||||||
|
*/
|
||||||
|
export function resolveAiResourceDisplayStatus(
|
||||||
|
displayValue?: null | string,
|
||||||
|
_fallbackValue?: null | string,
|
||||||
|
): AiPublishStatus {
|
||||||
|
return normalizeAiPublishStatus(displayValue);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前资源是否已有正式线上版本。
|
* 当前资源是否已有正式线上版本。
|
||||||
*/
|
*/
|
||||||
@@ -27,12 +42,23 @@ export function isAiResourcePublished(value?: null | string) {
|
|||||||
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前资源是否处于已下线状态。
|
||||||
|
*/
|
||||||
|
export function isAiResourceOffline(value?: null | string) {
|
||||||
|
return normalizeAiPublishStatus(value) === 'OFFLINE';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前资源是否允许对外可见。
|
* 当前资源是否允许对外可见。
|
||||||
*/
|
*/
|
||||||
export function isAiResourceExternallyVisible(value?: null | string) {
|
export function isAiResourceExternallyVisible(value?: null | string) {
|
||||||
const normalized = normalizeAiPublishStatus(value);
|
const normalized = normalizeAiPublishStatus(value);
|
||||||
return normalized === 'PUBLISHED' || normalized === 'DELETE_PENDING';
|
return (
|
||||||
|
normalized === 'PUBLISHED' ||
|
||||||
|
normalized === 'DELETE_PENDING' ||
|
||||||
|
normalized === 'OFFLINE_PENDING'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +71,68 @@ export function isAiResourceSelectableForBot(value?: null | string) {
|
|||||||
/**
|
/**
|
||||||
* 当前资源是否处于审批处理中。
|
* 当前资源是否处于审批处理中。
|
||||||
*/
|
*/
|
||||||
export function isAiResourceApprovalPending(value?: null | string) {
|
export function isAiResourceApprovalPending(
|
||||||
const normalized = normalizeAiPublishStatus(value);
|
displayValue?: null | string,
|
||||||
return normalized === 'PUBLISH_PENDING' || normalized === 'DELETE_PENDING';
|
_fallbackValue?: null | string,
|
||||||
|
) {
|
||||||
|
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||||
|
return (
|
||||||
|
normalized === 'PUBLISH_PENDING' ||
|
||||||
|
normalized === 'OFFLINE_PENDING' ||
|
||||||
|
normalized === 'DELETE_PENDING'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前资源是否允许发起发布。
|
||||||
|
*/
|
||||||
|
export function canAiResourcePublish(
|
||||||
|
displayValue?: null | string,
|
||||||
|
_fallbackValue?: null | string,
|
||||||
|
) {
|
||||||
|
if (isAiResourceApprovalPending(displayValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||||
|
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前资源是否允许重新发布。
|
||||||
|
*/
|
||||||
|
export function canAiResourceRepublish(
|
||||||
|
displayValue?: null | string,
|
||||||
|
_fallbackValue?: null | string,
|
||||||
|
) {
|
||||||
|
if (isAiResourceApprovalPending(displayValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前资源是否允许发起下线。
|
||||||
|
*/
|
||||||
|
export function canAiResourceOffline(
|
||||||
|
displayValue?: null | string,
|
||||||
|
_fallbackValue?: null | string,
|
||||||
|
) {
|
||||||
|
if (isAiResourceApprovalPending(displayValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前资源是否允许发起删除。
|
||||||
|
*/
|
||||||
|
export function canAiResourceDelete(
|
||||||
|
displayValue?: null | string,
|
||||||
|
_fallbackValue?: null | string,
|
||||||
|
) {
|
||||||
|
if (isAiResourceApprovalPending(displayValue)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||||
|
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ import { $t } from '#/locales';
|
|||||||
import { router } from '#/router';
|
import { router } from '#/router';
|
||||||
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||||
import {
|
import {
|
||||||
|
canAiResourceRepublish,
|
||||||
isAiResourceApprovalPending,
|
isAiResourceApprovalPending,
|
||||||
normalizeAiPublishStatus,
|
resolveAiResourceDisplayStatus,
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||||
@@ -251,18 +252,26 @@ const updatePluginNode = ref<any>(null);
|
|||||||
const pageLoading = ref(false);
|
const pageLoading = ref(false);
|
||||||
const chainInfo = ref<any>(null);
|
const chainInfo = ref<any>(null);
|
||||||
const publishActionText = computed(() => {
|
const publishActionText = computed(() => {
|
||||||
switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) {
|
switch (
|
||||||
|
resolveAiResourceDisplayStatus(
|
||||||
|
workflowInfo.value?.displayPublishStatus,
|
||||||
|
workflowInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
case 'DELETE_PENDING': {
|
case 'DELETE_PENDING': {
|
||||||
return $t('aiWorkflow.publishStatusDeletePending');
|
return $t('aiWorkflow.publishStatusDeletePending');
|
||||||
}
|
}
|
||||||
|
case 'OFFLINE_PENDING': {
|
||||||
|
return $t('aiWorkflow.publishStatusOfflinePending');
|
||||||
|
}
|
||||||
case 'PUBLISH_PENDING': {
|
case 'PUBLISH_PENDING': {
|
||||||
return $t('aiWorkflow.publishStatusPublishPending');
|
return $t('aiWorkflow.publishStatusPublishPending');
|
||||||
}
|
}
|
||||||
case 'PUBLISHED': {
|
case 'PUBLISHED': {
|
||||||
return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`;
|
return $t('button.republish');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`;
|
return $t('button.publish');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -272,7 +281,10 @@ const publishActionDisabled = computed(
|
|||||||
saveLoading.value ||
|
saveLoading.value ||
|
||||||
checkLoading.value ||
|
checkLoading.value ||
|
||||||
publishLoading.value ||
|
publishLoading.value ||
|
||||||
isAiResourceApprovalPending(workflowInfo.value?.publishStatus),
|
isAiResourceApprovalPending(
|
||||||
|
workflowInfo.value?.displayPublishStatus,
|
||||||
|
workflowInfo.value?.publishStatus,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
function syncNavTitle(title: string) {
|
function syncNavTitle(title: string) {
|
||||||
@@ -498,17 +510,27 @@ function closeCheckIssues() {
|
|||||||
async function handleCheck() {
|
async function handleCheck() {
|
||||||
await runCheck('PRE_EXECUTE');
|
await runCheck('PRE_EXECUTE');
|
||||||
}
|
}
|
||||||
async function handlePublish() {
|
async function handlePublishAction() {
|
||||||
if (publishLoading.value) {
|
if (publishLoading.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) {
|
if (
|
||||||
|
isAiResourceApprovalPending(
|
||||||
|
workflowInfo.value?.displayPublishStatus,
|
||||||
|
workflowInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
canAiResourceRepublish(
|
||||||
|
workflowInfo.value?.displayPublishStatus,
|
||||||
|
workflowInfo.value?.publishStatus,
|
||||||
|
)
|
||||||
|
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||||
|
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -680,8 +702,8 @@ function onAsyncExecute(info: any) {
|
|||||||
:loading="publishLoading"
|
:loading="publishLoading"
|
||||||
:disabled="publishActionDisabled"
|
:disabled="publishActionDisabled"
|
||||||
class="workflow-publish-button"
|
class="workflow-publish-button"
|
||||||
:class="`workflow-publish-button--${normalizeAiPublishStatus(workflowInfo?.publishStatus)}`"
|
:class="`workflow-publish-button--${resolveAiResourceDisplayStatus(workflowInfo?.displayPublishStatus, workflowInfo?.publishStatus)}`"
|
||||||
@click="handlePublish"
|
@click="handlePublishAction"
|
||||||
>
|
>
|
||||||
{{ publishActionText }}
|
{{ publishActionText }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@@ -809,12 +831,24 @@ function onAsyncExecute(info: any) {
|
|||||||
border-color: hsl(var(--warning) / 24%);
|
border-color: hsl(var(--warning) / 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.workflow-publish-button--OFFLINE_PENDING.el-button) {
|
||||||
|
color: hsl(var(--warning));
|
||||||
|
background: hsl(var(--warning) / 18%);
|
||||||
|
border-color: hsl(var(--warning) / 24%);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.workflow-publish-button--PUBLISHED.el-button) {
|
:deep(.workflow-publish-button--PUBLISHED.el-button) {
|
||||||
color: hsl(var(--success));
|
color: hsl(var(--success));
|
||||||
background: hsl(var(--success) / 18%);
|
background: hsl(var(--success) / 18%);
|
||||||
border-color: hsl(var(--success) / 24%);
|
border-color: hsl(var(--success) / 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.workflow-publish-button--OFFLINE.el-button) {
|
||||||
|
color: hsl(var(--foreground) / 78%);
|
||||||
|
background: hsl(var(--muted) / 62%);
|
||||||
|
border-color: hsl(var(--foreground) / 14%);
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
|
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
background: hsl(var(--destructive) / 16%);
|
background: hsl(var(--destructive) / 16%);
|
||||||
|
|||||||
@@ -50,9 +50,16 @@ import { router } from '#/router';
|
|||||||
import { useDictStore } from '#/store';
|
import { useDictStore } from '#/store';
|
||||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||||
import {
|
import {
|
||||||
|
buildOfflineImpactMessage,
|
||||||
|
type OfflineImpactCheck,
|
||||||
|
} from '#/views/ai/shared/offline-impact';
|
||||||
|
import {
|
||||||
|
canAiResourceDelete,
|
||||||
|
canAiResourceOffline,
|
||||||
|
canAiResourcePublish,
|
||||||
|
canAiResourceRepublish,
|
||||||
isAiResourceApprovalPending,
|
isAiResourceApprovalPending,
|
||||||
isAiResourcePublished,
|
resolveAiResourceDisplayStatus,
|
||||||
normalizeAiPublishStatus,
|
|
||||||
} from '#/views/ai/shared/publish-status';
|
} from '#/views/ai/shared/publish-status';
|
||||||
|
|
||||||
import WorkflowModal from './WorkflowModal.vue';
|
import WorkflowModal from './WorkflowModal.vue';
|
||||||
@@ -175,21 +182,35 @@ const actions: ActionButton[] = [
|
|||||||
{
|
{
|
||||||
icon: Promotion,
|
icon: Promotion,
|
||||||
text: (row: any) =>
|
text: (row: any) =>
|
||||||
isAiResourcePublished(row.publishStatus)
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||||
? $t('button.republish')
|
? $t('button.republish')
|
||||||
: $t('button.submitPublishApproval'),
|
: $t('button.publish'),
|
||||||
permission: '/api/v1/workflow/save',
|
permission: '/api/v1/workflow/save',
|
||||||
placement: 'inline',
|
placement: 'inline',
|
||||||
|
visible: (row: any) =>
|
||||||
|
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||||
|
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
submitPublishApproval(row);
|
submitPublishAction(row);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Promotion,
|
||||||
|
text: $t('button.offline'),
|
||||||
|
permission: '/api/v1/workflow/save',
|
||||||
|
placement: 'menu',
|
||||||
|
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||||
|
onClick: (row: any) => {
|
||||||
|
submitOfflineAction(row);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: Delete,
|
icon: Delete,
|
||||||
text: $t('button.submitDeleteApproval'),
|
text: $t('button.delete'),
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
permission: '/api/v1/workflow/remove',
|
permission: '/api/v1/workflow/remove',
|
||||||
placement: 'menu',
|
placement: 'menu',
|
||||||
|
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||||
onClick: (row: any) => {
|
onClick: (row: any) => {
|
||||||
submitDeleteApproval(row);
|
submitDeleteApproval(row);
|
||||||
},
|
},
|
||||||
@@ -282,14 +303,22 @@ function showDialog(row: any, importMode = false) {
|
|||||||
function resolveNavTitle(row: any) {
|
function resolveNavTitle(row: any) {
|
||||||
return row?.title || row?.name || '';
|
return row?.title || row?.name || '';
|
||||||
}
|
}
|
||||||
async function submitPublishApproval(row: any) {
|
function isRepublishAction(row: any) {
|
||||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPublishAction(row: any) {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
isRepublishAction(row)
|
||||||
|
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||||
|
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||||
$t('message.noticeTitle'),
|
$t('message.noticeTitle'),
|
||||||
{
|
{
|
||||||
confirmButtonText: $t('button.confirm'),
|
confirmButtonText: $t('button.confirm'),
|
||||||
@@ -300,7 +329,56 @@ async function submitPublishApproval(row: any) {
|
|||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
const res = await api.post(
|
||||||
|
'/api/v1/workflow/submitPublishApproval',
|
||||||
|
{
|
||||||
|
id: row.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.errorCode === 0) {
|
||||||
|
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||||
|
pageDataRef.value?.reload?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function submitOfflineAction(row: any) {
|
||||||
|
if (
|
||||||
|
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||||
|
) {
|
||||||
|
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const impactRes = await api.get<{
|
||||||
|
data: OfflineImpactCheck;
|
||||||
|
errorCode: number;
|
||||||
|
}>(
|
||||||
|
'/api/v1/workflow/offlineImpactCheck',
|
||||||
|
{
|
||||||
|
params: { id: row.id },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (impactRes.errorCode !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
impactRes.data?.hasBotBindings
|
||||||
|
? buildOfflineImpactMessage(
|
||||||
|
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||||
|
impactRes.data.botBindings,
|
||||||
|
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||||
|
)
|
||||||
|
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
|
||||||
|
$t('message.noticeTitle'),
|
||||||
|
{
|
||||||
|
confirmButtonText: $t('button.confirm'),
|
||||||
|
cancelButtonText: $t('button.cancel'),
|
||||||
|
type: 'warning',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await api.post('/api/v1/workflow/submitOfflineApproval', {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
});
|
});
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
@@ -309,7 +387,9 @@ async function submitPublishApproval(row: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function submitDeleteApproval(row: any) {
|
async function submitDeleteApproval(row: any) {
|
||||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
if (
|
||||||
|
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||||
|
) {
|
||||||
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
|
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -334,14 +414,29 @@ async function submitDeleteApproval(row: any) {
|
|||||||
pageDataRef.value?.reload?.();
|
pageDataRef.value?.reload?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function resolvePublishStatusMeta(status?: string) {
|
function resolvePublishStatusMetaByInstance(
|
||||||
switch (normalizeAiPublishStatus(status)) {
|
displayPublishStatus?: string,
|
||||||
|
publishStatus?: string,
|
||||||
|
) {
|
||||||
|
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||||
case 'DELETE_PENDING': {
|
case 'DELETE_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('aiWorkflow.publishStatusDeletePending'),
|
label: $t('aiWorkflow.publishStatusDeletePending'),
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'OFFLINE_PENDING': {
|
||||||
|
return {
|
||||||
|
label: $t('aiWorkflow.publishStatusOfflinePending'),
|
||||||
|
tone: 'pending',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'OFFLINE': {
|
||||||
|
return {
|
||||||
|
label: $t('aiWorkflow.publishStatusOffline'),
|
||||||
|
tone: 'draft',
|
||||||
|
};
|
||||||
|
}
|
||||||
case 'PUBLISH_PENDING': {
|
case 'PUBLISH_PENDING': {
|
||||||
return {
|
return {
|
||||||
label: $t('aiWorkflow.publishStatusPublishPending'),
|
label: $t('aiWorkflow.publishStatusPublishPending'),
|
||||||
@@ -572,11 +667,11 @@ function handleHeaderButtonClick(data: any) {
|
|||||||
<template #publish>
|
<template #publish>
|
||||||
<div
|
<div
|
||||||
class="workflow-publish-chip"
|
class="workflow-publish-chip"
|
||||||
:class="`workflow-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
:class="`workflow-publish-chip--${resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||||
>
|
>
|
||||||
<span class="workflow-publish-chip__dot"></span>
|
<span class="workflow-publish-chip__dot"></span>
|
||||||
<span>{{
|
<span>{{
|
||||||
resolvePublishStatusMeta(item.publishStatus).label
|
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
|
||||||
}}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const resourceLabelMap: Record<string, string> = {
|
|||||||
|
|
||||||
const actionLabelMap: Record<string, string> = {
|
const actionLabelMap: Record<string, string> = {
|
||||||
DELETE: $t('approval.action.delete'),
|
DELETE: $t('approval.action.delete'),
|
||||||
|
OFFLINE: $t('approval.action.offline'),
|
||||||
PUBLISH: $t('approval.action.publish'),
|
PUBLISH: $t('approval.action.publish'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,18 +139,28 @@ function formatPayload(payload: Record<string, any>) {
|
|||||||
return JSON.stringify(payload || {}, null, 2);
|
return JSON.stringify(payload || {}, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAccountDisplay(name?: string, id?: null | number | string) {
|
function formatAccountDisplay(
|
||||||
if (name && id) {
|
name?: string,
|
||||||
return `${name}(${id})`;
|
account?: null | string,
|
||||||
|
fallbackId?: null | number | string,
|
||||||
|
) {
|
||||||
|
if (name && account) {
|
||||||
|
return `${name}(${account})`;
|
||||||
}
|
}
|
||||||
if (name) {
|
if (name) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
return id || '-';
|
if (account) {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
return fallbackId || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOperatorId(id?: null | number | string) {
|
function formatOperatorId(
|
||||||
return id || '-';
|
account?: null | string,
|
||||||
|
fallbackId?: null | number | string,
|
||||||
|
) {
|
||||||
|
return account || fallbackId || '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOperatorName(name?: null | string) {
|
function formatOperatorName(name?: null | string) {
|
||||||
@@ -276,7 +287,13 @@ function formatEventInfo(row: Record<string, any>) {
|
|||||||
{{ detail.id || '-' }}
|
{{ detail.id || '-' }}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
||||||
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
|
{{
|
||||||
|
formatAccountDisplay(
|
||||||
|
detail.applicantName,
|
||||||
|
detail.applicantAccount,
|
||||||
|
detail.applicantId,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
||||||
{{ detail.submittedAt || '-' }}
|
{{ detail.submittedAt || '-' }}
|
||||||
@@ -347,7 +364,7 @@ function formatEventInfo(row: Record<string, any>) {
|
|||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatOperatorId(row.operatorId) }}
|
{{ formatOperatorId(row.operatorAccount, row.operatorId) }}
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
|
|
||||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||||
type ActionType = '' | 'DELETE' | 'PUBLISH';
|
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||||
type AssigneeType = 'ROLE' | 'USER';
|
type AssigneeType = 'ROLE' | 'USER';
|
||||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||||
@@ -77,6 +77,7 @@ const RESOURCE_OPTIONS = [
|
|||||||
|
|
||||||
const ACTION_OPTIONS = [
|
const ACTION_OPTIONS = [
|
||||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||||
|
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
|
import { hasPermission } from '#/api/common/hasPermission';
|
||||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import PageData from '#/components/page/PageData.vue';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -40,6 +41,7 @@ const RESOURCE_OPTIONS = [
|
|||||||
|
|
||||||
const ACTION_OPTIONS = [
|
const ACTION_OPTIONS = [
|
||||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||||
|
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -66,21 +68,25 @@ const TAB_CONFIG = [
|
|||||||
label: $t('approval.tab.flow'),
|
label: $t('approval.tab.flow'),
|
||||||
name: 'flow',
|
name: 'flow',
|
||||||
path: '/sys/approval/flow',
|
path: '/sys/approval/flow',
|
||||||
|
permission: '/page/approval/flow',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('approval.tab.pending'),
|
label: $t('approval.tab.pending'),
|
||||||
name: 'pending',
|
name: 'pending',
|
||||||
path: '/sys/approval/pending',
|
path: '/sys/approval/pending',
|
||||||
|
permission: '/page/approval/pending',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('approval.tab.processed'),
|
label: $t('approval.tab.processed'),
|
||||||
name: 'processed',
|
name: 'processed',
|
||||||
path: '/sys/approval/processed',
|
path: '/sys/approval/processed',
|
||||||
|
permission: '/page/approval/processed',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('approval.tab.initiated'),
|
label: $t('approval.tab.initiated'),
|
||||||
name: 'initiated',
|
name: 'initiated',
|
||||||
path: '/sys/approval/initiated',
|
path: '/sys/approval/initiated',
|
||||||
|
permission: '/page/approval/initiated',
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -115,14 +121,23 @@ const pendingBadgeText = computed(() =>
|
|||||||
const hasApprovalRootMenu = computed(
|
const hasApprovalRootMenu = computed(
|
||||||
() => !!accessStore.getMenuByPath('/sys/approval'),
|
() => !!accessStore.getMenuByPath('/sys/approval'),
|
||||||
);
|
);
|
||||||
|
const hasLegacyTabMenus = computed(() =>
|
||||||
|
TAB_CONFIG.some((item) => !!accessStore.getMenuByPath(item.path)),
|
||||||
|
);
|
||||||
|
const hasButtonTabPermissions = computed(() =>
|
||||||
|
TAB_CONFIG.some((item) => hasPermission([item.permission])),
|
||||||
|
);
|
||||||
const visibleTabs = computed(() => {
|
const visibleTabs = computed(() => {
|
||||||
const matchedTabs = TAB_CONFIG.filter((item) =>
|
if (!hasApprovalRootMenu.value) {
|
||||||
accessStore.getMenuByPath(item.path),
|
return [];
|
||||||
);
|
|
||||||
if (matchedTabs.length > 0) {
|
|
||||||
return matchedTabs;
|
|
||||||
}
|
}
|
||||||
return hasApprovalRootMenu.value ? [...TAB_CONFIG] : [];
|
if (hasButtonTabPermissions.value) {
|
||||||
|
return TAB_CONFIG.filter((item) => hasPermission([item.permission]));
|
||||||
|
}
|
||||||
|
if (hasLegacyTabMenus.value) {
|
||||||
|
return TAB_CONFIG.filter((item) => accessStore.getMenuByPath(item.path));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
});
|
});
|
||||||
const visibleTabNames = computed(() =>
|
const visibleTabNames = computed(() =>
|
||||||
visibleTabs.value.map((item) => item.name),
|
visibleTabs.value.map((item) => item.name),
|
||||||
@@ -139,7 +154,7 @@ const instanceStatusOptions = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void syncRouteTab(route.path);
|
void syncRouteTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(activeTab, () => {
|
watch(activeTab, () => {
|
||||||
@@ -153,9 +168,9 @@ watch(activeTab, () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
[() => route.path, visibleTabNames],
|
[() => route.fullPath, visibleTabNames],
|
||||||
async ([path]) => {
|
async () => {
|
||||||
await syncRouteTab(path);
|
await syncRouteTab();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -177,19 +192,55 @@ function resolveTabNameByPath(path: string): ApprovalTabName {
|
|||||||
return 'flow';
|
return 'flow';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncRouteTab(path: string) {
|
function resolveTabNameByRoute() {
|
||||||
|
const queryTab = String(route.query.tab || '');
|
||||||
|
if (
|
||||||
|
queryTab === 'flow' ||
|
||||||
|
queryTab === 'pending' ||
|
||||||
|
queryTab === 'processed' ||
|
||||||
|
queryTab === 'initiated'
|
||||||
|
) {
|
||||||
|
return queryTab as ApprovalTabName;
|
||||||
|
}
|
||||||
|
return resolveTabNameByPath(route.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncRouteTab() {
|
||||||
if (visibleTabs.value.length === 0) {
|
if (visibleTabs.value.length === 0) {
|
||||||
activeTab.value = 'flow';
|
activeTab.value = 'flow';
|
||||||
pendingBadgeCount.value = 0;
|
pendingBadgeCount.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const expectedTab = resolveTabNameByPath(path);
|
|
||||||
const fallbackTab = visibleTabs.value[0];
|
const fallbackTab = visibleTabs.value[0];
|
||||||
|
const queryTab = String(route.query.tab || '');
|
||||||
|
|
||||||
|
// 进入审批管理根路径时,默认落到当前可见页签中排序第一个的页签。
|
||||||
|
if (route.path === '/sys/approval' && !queryTab && fallbackTab) {
|
||||||
|
activeTab.value = fallbackTab.name;
|
||||||
|
if (fallbackTab.name !== 'flow') {
|
||||||
|
await router.replace({
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: { tab: fallbackTab.name },
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedTab = resolveTabNameByRoute();
|
||||||
if (visibleTabNames.value.includes(expectedTab)) {
|
if (visibleTabNames.value.includes(expectedTab)) {
|
||||||
activeTab.value = expectedTab;
|
activeTab.value = expectedTab;
|
||||||
} else {
|
} else {
|
||||||
if (fallbackTab && path !== fallbackTab.path) {
|
const fallbackQuery =
|
||||||
await router.replace(fallbackTab.path);
|
fallbackTab?.name === 'flow' ? {} : { tab: fallbackTab?.name };
|
||||||
|
const currentQueryTab = String(route.query.tab || '');
|
||||||
|
if (
|
||||||
|
fallbackTab &&
|
||||||
|
(route.path !== '/sys/approval' || currentQueryTab !== String(fallbackTab.name))
|
||||||
|
) {
|
||||||
|
await router.replace({
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: fallbackQuery,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeTab.value = fallbackTab?.name || 'flow';
|
activeTab.value = fallbackTab?.name || 'flow';
|
||||||
@@ -206,10 +257,16 @@ function handleTabChange(name: number | string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = visibleTabs.value.find((item) => item.name === name);
|
const target = visibleTabs.value.find((item) => item.name === name);
|
||||||
if (!target || route.path === target.path) {
|
const nextQuery = name === 'flow' ? {} : { tab: name };
|
||||||
|
const currentQueryTab = String(route.query.tab || '');
|
||||||
|
const currentTab = currentQueryTab || 'flow';
|
||||||
|
if (!target || currentTab === name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(target.path);
|
router.replace({
|
||||||
|
path: '/sys/approval',
|
||||||
|
query: nextQuery,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function reloadCurrentTab() {
|
function reloadCurrentTab() {
|
||||||
|
|||||||
@@ -611,6 +611,9 @@ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
|
|||||||
meta: { fullPathKey } = {},
|
meta: { fullPathKey } = {},
|
||||||
query = {},
|
query = {},
|
||||||
} = tab as RouteLocationNormalized;
|
} = tab as RouteLocationNormalized;
|
||||||
|
if (path === '/sys/approval') {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
// pageKey可能是数组(查询参数重复时可能出现)
|
// pageKey可能是数组(查询参数重复时可能出现)
|
||||||
const pageKey = Array.isArray(query.pageKey)
|
const pageKey = Array.isArray(query.pageKey)
|
||||||
? query.pageKey[0]
|
? query.pageKey[0]
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
interface BotInfo {
|
interface BotInfo {
|
||||||
alias: string;
|
alias: string;
|
||||||
anonymousEnabled: boolean;
|
anonymousEnabled: boolean;
|
||||||
|
approvalPending?: boolean;
|
||||||
currentApprovalInstanceId?: number | string;
|
currentApprovalInstanceId?: number | string;
|
||||||
|
currentApprovalActionType?: string;
|
||||||
|
displayPublishStatus?: string;
|
||||||
created: string;
|
created: string;
|
||||||
createdBy: number;
|
createdBy: number;
|
||||||
deptId: number;
|
deptId: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user