feat: 收敛AI资源发布审批生命周期

- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

- 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览

- 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
2026-04-09 17:13:54 +08:00
parent 81125ce55c
commit 4e565aef99
68 changed files with 3859 additions and 817 deletions

View File

@@ -19,6 +19,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import tech.easyflow.ai.easyagents.listener.PromptChoreChatStreamListener;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.publish.BotPublishAppService;
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.ai.service.*;
import tech.easyflow.ai.service.impl.BotServiceImpl;
import tech.easyflow.common.audio.core.AudioServiceManager;
@@ -70,6 +71,8 @@ public class BotController extends BaseCurdController<BotService, Bot> {
private CategoryPermissionService categoryPermissionService;
@Resource
private BotPublishAppService botPublishAppService;
@Resource
private AiResourceApprovalStateService aiResourceApprovalStateService;
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
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()) {
throw new BusinessException("聊天助手尚未发布");
}
if (StpUtil.isLogin()) {
aiResourceApprovalStateService.fillBotApprovalState(bot);
}
return Result.ok(bot);
}
@@ -221,6 +227,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
}
if (data.getModelId() == null) {
if (StpUtil.isLogin()) {
aiResourceApprovalStateService.fillBotApprovalState(data);
}
return Result.ok(data);
}
@@ -229,6 +238,9 @@ public class BotController extends BaseCurdController<BotService, Bot> {
if (llm == null) {
data.setModelId(null);
if (StpUtil.isLogin()) {
aiResourceApprovalStateService.fillBotApprovalState(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);
}
@PostMapping("/submitPublishApproval")
@SaCheckPermission("/api/v1/bot/save")
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")
@SaCheckPermission("/api/v1/bot/remove")
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
return Result.ok(botPublishAppService.submitDeleteApproval(id));
return buildApprovalActionResult(
botPublishAppService.submitDeleteApproval(id),
"已提交删除审批",
"已直接删除"
);
}
@Override
@@ -262,13 +295,26 @@ public class BotController extends BaseCurdController<BotService, Bot> {
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
applyCategoryPermission(queryWrapper);
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
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper 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) {
@@ -388,7 +434,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
@Override
@PostMapping("remove")
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
return Result.fail(1, "提交删除审批");
return Result.fail(1, "使用发布状态操作删除聊天助手");
}
/**

View File

@@ -21,6 +21,9 @@ import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.enums.PublishStatus;
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.KnowledgeRetrievalModes;
import tech.easyflow.ai.service.BotDocumentCollectionService;
@@ -71,6 +74,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
private KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
@Resource
private KnowledgePublishAppService knowledgePublishAppService;
@Resource
private AiResourceApprovalStateService aiResourceApprovalStateService;
public DocumentCollectionController(DocumentCollectionService service, DocumentChunkService chunkService, ModelService llmService) {
super(service);
@@ -177,6 +182,7 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
)
public Result<DocumentCollection> detail(String id) {
DocumentCollection detail = service.getDetail(id);
aiResourceApprovalStateService.fillKnowledgeApprovalState(detail);
return Result.ok(detail);
}
@@ -195,7 +201,46 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
@PostMapping("/submitPublishApproval")
@SaCheckPermission("/api/v1/documentCollection/save")
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")
@SaCheckPermission("/api/v1/documentCollection/remove")
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
return Result.ok(knowledgePublishAppService.submitDeleteApproval(id));
return buildApprovalActionResult(
knowledgePublishAppService.submitDeleteApproval(id),
"已提交删除审批",
"已直接删除"
);
}
@PostMapping("splitterProfile/save")
@@ -250,20 +299,33 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
applyPublishedOnlyFilter(queryWrapper);
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
protected Page<DocumentCollection> queryPage(Page<DocumentCollection> page, QueryWrapper queryWrapper) {
knowledgeVisibilityQueryHelper.applyReadableAccess(queryWrapper);
applyPublishedOnlyFilter(queryWrapper);
return super.queryPage(page, queryWrapper);
Page<DocumentCollection> result = super.queryPage(page, queryWrapper);
aiResourceApprovalStateService.fillKnowledgeApprovalState(result.getRecords());
return result;
}
@Override
@PostMapping("remove")
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) {

View File

@@ -4,14 +4,19 @@ import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.http.HttpUtil;
import com.mybatisflex.core.paginate.Page;
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.RestController;
import tech.easyflow.admin.model.ai.ResourcePreviewVo;
import tech.easyflow.ai.entity.Resource;
import tech.easyflow.ai.service.ResourceService;
import tech.easyflow.ai.utils.DocUtil;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
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.service.CategoryPermissionService;
@@ -31,6 +36,8 @@ import static tech.easyflow.ai.entity.table.ResourceTableDef.RESOURCE;
@RestController
@RequestMapping("/api/v1/resource")
public class ResourceController extends BaseCurdController<ResourceService, Resource> {
private static final int RESOURCE_PREVIEW_CONTENT_LIMIT = 20_000;
@javax.annotation.Resource
private CategoryPermissionService categoryPermissionService;
@@ -79,6 +86,26 @@ public class ResourceController extends BaseCurdController<ResourceService, Reso
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) {
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess("RESOURCE");
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())));
}
/**
* 解析用于预览的文件后缀。
*
* @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;
}
}

View File

@@ -27,6 +27,9 @@ import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
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.ModelService;
import tech.easyflow.ai.service.WorkflowService;
@@ -88,6 +91,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
@Resource
private WorkflowPublishAppService workflowPublishAppService;
@Resource
private AiResourceApprovalStateService aiResourceApprovalStateService;
public WorkflowController(WorkflowService service, ModelService modelService) {
super(service);
@@ -254,7 +259,46 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
@PostMapping("/submitPublishApproval")
@SaCheckPermission("/api/v1/workflow/save")
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")
@SaCheckPermission("/api/v1/workflow/remove")
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
return Result.ok(workflowPublishAppService.submitDeleteApproval(id));
return buildApprovalActionResult(
workflowPublishAppService.submitDeleteApproval(id),
"已提交删除审批",
"已直接删除"
);
}
@GetMapping("/supportedCodeEngines")
@@ -307,6 +355,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
)
public Result<Workflow> detail(String id) {
Workflow workflow = service.getDetail(id);
aiResourceApprovalStateService.fillWorkflowApprovalState(workflow);
return Result.ok(workflow);
}
@@ -368,20 +417,33 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
applyPublishedOnlyFilter(queryWrapper);
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
protected Page<Workflow> queryPage(Page<Workflow> page, QueryWrapper queryWrapper) {
workflowVisibilityQueryHelper.applyReadableAccess(queryWrapper);
applyPublishedOnlyFilter(queryWrapper);
return super.queryPage(page, queryWrapper);
Page<Workflow> result = super.queryPage(page, queryWrapper);
aiResourceApprovalStateService.fillWorkflowApprovalState(result.getRecords());
return result;
}
@Override
@PostMapping("remove")
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

View File

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