feat: 归档L03与L09审批发布能力

- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象

- 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
2026-04-07 14:41:52 +08:00
parent 7e7c236c2a
commit 3f128e977a
138 changed files with 13035 additions and 346 deletions

View File

@@ -80,6 +80,10 @@
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-system</artifactId>
</dependency>
<dependency>
<groupId>tech.easyflow</groupId>
<artifactId>easyflow-module-approval</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>

View File

@@ -14,25 +14,31 @@ import java.util.*;
public class WorkflowTool extends BaseTool {
private BigInteger workflowId;
private String definitionId;
public WorkflowTool() {
}
public WorkflowTool(Workflow workflow, boolean needEnglishName) {
this(workflow, needEnglishName, workflow.getId() == null ? null : workflow.getId().toString());
}
public WorkflowTool(Workflow workflow, boolean needEnglishName, String definitionId) {
this.workflowId = workflow.getId();
this.definitionId = definitionId;
if (needEnglishName) {
this.name = workflow.getEnglishName();
} else {
this.name = workflow.getTitle();
}
this.description = workflow.getDescription();
this.parameters = toParameters(workflow);
this.parameters = toParameters(workflow, definitionId);
}
static Parameter[] toParameters(Workflow workflow) {
static Parameter[] toParameters(Workflow workflow, String definitionId) {
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
ChainDefinition definition = executor.getDefinitionRepository().getChainDefinitionById(workflow.getId().toString());
ChainDefinition definition = executor.getDefinitionRepository().getChainDefinitionById(definitionId);
List<com.easyagents.flow.core.chain.Parameter> parameterDefs = definition.getStartParameters();
if (parameterDefs == null || parameterDefs.isEmpty()) {
return new Parameter[0];
@@ -131,16 +137,25 @@ public class WorkflowTool extends BaseTool {
this.workflowId = workflowId;
}
public String getDefinitionId() {
return definitionId;
}
public void setDefinitionId(String definitionId) {
this.definitionId = definitionId;
}
@Override
public Object invoke(Map<String, Object> argsMap) {
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
return executor.execute(workflowId.toString(), argsMap);
return executor.execute(definitionId, argsMap);
}
@Override
public String toString() {
return "AiWorkflowFunction{" +
"workflowId=" + workflowId +
", definitionId='" + definitionId + '\'' +
", name='" + name + '\'' +
", description='" + description + '\'' +
", parameters=" + Arrays.toString(parameters) +

View File

@@ -4,6 +4,7 @@ import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.service.WorkflowService;
@@ -22,10 +23,14 @@ public class ChainDefinitionRepositoryImpl implements ChainDefinitionRepository
@Override
public ChainDefinition getChainDefinitionById(String id) {
Workflow workflow = workflowService.getById(id);
boolean publishedDefinition = PublishedWorkflowDefinitionIds.isPublished(id);
String workflowId = PublishedWorkflowDefinitionIds.unwrap(id);
Workflow workflow = publishedDefinition
? workflowService.getPublishedById(new java.math.BigInteger(workflowId))
: workflowService.getById(workflowId);
String json = workflowDatacenterContentService.prepareContent(workflow.getContent());
ChainDefinition chainDefinition = chainParser.parse(json);
chainDefinition.setId(workflow.getId().toString());
chainDefinition.setId(id);
chainDefinition.setName(workflow.getEnglishName());
chainDefinition.setDescription(workflow.getDescription());
return chainDefinition;

View File

@@ -0,0 +1,45 @@
package tech.easyflow.ai.easyagentsflow.support;
/**
* 已发布工作流定义 ID 工具。
*/
public final class PublishedWorkflowDefinitionIds {
private static final String PREFIX = "published:";
private PublishedWorkflowDefinitionIds() {
}
/**
* 构建已发布定义 ID。
*
* @param workflowId 工作流 ID
* @return 已发布定义 ID
*/
public static String published(String workflowId) {
return PREFIX + workflowId;
}
/**
* 是否为已发布定义 ID。
*
* @param definitionId 定义 ID
* @return 命中已发布定义前缀时返回 true
*/
public static boolean isPublished(String definitionId) {
return definitionId != null && definitionId.startsWith(PREFIX);
}
/**
* 还原真实工作流 ID。
*
* @param definitionId 定义 ID
* @return 原始工作流 ID
*/
public static String unwrap(String definitionId) {
if (!isPublished(definitionId)) {
return definitionId;
}
return definitionId.substring(PREFIX.length());
}
}

View File

@@ -19,4 +19,8 @@ public class Workflow extends WorkflowBase implements VisibilityResource {
public Tool toFunction(boolean needEnglishName) {
return new WorkflowTool(this, needEnglishName);
}
public Tool toFunction(boolean needEnglishName, String definitionId) {
return new WorkflowTool(this, needEnglishName, definitionId);
}
}

View File

@@ -111,6 +111,36 @@ public class BotBase extends DateEntity implements Serializable {
@Column(comment = "修改者ID")
private BigInteger modifiedBy;
/**
* 发布状态
*/
@Column(comment = "发布状态")
private String publishStatus;
/**
* 当前审批实例ID
*/
@Column(comment = "当前审批实例ID")
private BigInteger currentApprovalInstanceId;
/**
* 已发布快照
*/
@Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照")
private Map<String, Object> publishedSnapshotJson;
/**
* 发布时间
*/
@Column(comment = "发布时间")
private Date publishedAt;
/**
* 发布人
*/
@Column(comment = "发布人")
private BigInteger publishedBy;
public BigInteger getId() {
return id;
}
@@ -239,4 +269,44 @@ public class BotBase extends DateEntity implements Serializable {
this.modifiedBy = modifiedBy;
}
public String getPublishStatus() {
return publishStatus;
}
public void setPublishStatus(String publishStatus) {
this.publishStatus = publishStatus;
}
public BigInteger getCurrentApprovalInstanceId() {
return currentApprovalInstanceId;
}
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) {
this.currentApprovalInstanceId = currentApprovalInstanceId;
}
public Map<String, Object> getPublishedSnapshotJson() {
return publishedSnapshotJson;
}
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) {
this.publishedSnapshotJson = publishedSnapshotJson;
}
public Date getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
public BigInteger getPublishedBy() {
return publishedBy;
}
public void setPublishedBy(BigInteger publishedBy) {
this.publishedBy = publishedBy;
}
}

View File

@@ -166,6 +166,36 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
@Column(comment = "可见范围")
private String visibilityScope;
/**
* 发布状态
*/
@Column(comment = "发布状态")
private String publishStatus;
/**
* 当前审批实例ID
*/
@Column(comment = "当前审批实例ID")
private BigInteger currentApprovalInstanceId;
/**
* 已发布快照
*/
@Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照")
private Map<String, Object> publishedSnapshotJson;
/**
* 发布时间
*/
@Column(comment = "发布时间")
private Date publishedAt;
/**
* 发布人
*/
@Column(comment = "发布人")
private BigInteger publishedBy;
public BigInteger getId() {
return id;
}
@@ -366,4 +396,44 @@ public class DocumentCollectionBase extends DateEntity implements Serializable {
this.visibilityScope = visibilityScope;
}
public String getPublishStatus() {
return publishStatus;
}
public void setPublishStatus(String publishStatus) {
this.publishStatus = publishStatus;
}
public BigInteger getCurrentApprovalInstanceId() {
return currentApprovalInstanceId;
}
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) {
this.currentApprovalInstanceId = currentApprovalInstanceId;
}
public Map<String, Object> getPublishedSnapshotJson() {
return publishedSnapshotJson;
}
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) {
this.publishedSnapshotJson = publishedSnapshotJson;
}
public Date getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
public BigInteger getPublishedBy() {
return publishedBy;
}
public void setPublishedBy(BigInteger publishedBy) {
this.publishedBy = publishedBy;
}
}

View File

@@ -3,9 +3,11 @@ package tech.easyflow.ai.entity.base;
import com.mybatisflex.annotation.Column;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.KeyType;
import com.mybatisflex.core.handler.FastjsonTypeHandler;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.Date;
import java.util.Map;
import tech.easyflow.common.entity.DateEntity;
@@ -109,6 +111,36 @@ public class WorkflowBase extends DateEntity implements Serializable {
@Column(comment = "可见范围")
private String visibilityScope;
/**
* 发布状态
*/
@Column(comment = "发布状态")
private String publishStatus;
/**
* 当前审批实例ID
*/
@Column(comment = "当前审批实例ID")
private BigInteger currentApprovalInstanceId;
/**
* 已发布快照
*/
@Column(typeHandler = FastjsonTypeHandler.class, comment = "已发布快照")
private Map<String, Object> publishedSnapshotJson;
/**
* 发布时间
*/
@Column(comment = "发布时间")
private Date publishedAt;
/**
* 发布人
*/
@Column(comment = "发布人")
private BigInteger publishedBy;
public BigInteger getId() {
return id;
}
@@ -237,4 +269,44 @@ public class WorkflowBase extends DateEntity implements Serializable {
this.visibilityScope = visibilityScope;
}
public String getPublishStatus() {
return publishStatus;
}
public void setPublishStatus(String publishStatus) {
this.publishStatus = publishStatus;
}
public BigInteger getCurrentApprovalInstanceId() {
return currentApprovalInstanceId;
}
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) {
this.currentApprovalInstanceId = currentApprovalInstanceId;
}
public Map<String, Object> getPublishedSnapshotJson() {
return publishedSnapshotJson;
}
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) {
this.publishedSnapshotJson = publishedSnapshotJson;
}
public Date getPublishedAt() {
return publishedAt;
}
public void setPublishedAt(Date publishedAt) {
this.publishedAt = publishedAt;
}
public BigInteger getPublishedBy() {
return publishedBy;
}
public void setPublishedBy(BigInteger publishedBy) {
this.publishedBy = publishedBy;
}
}

View File

@@ -0,0 +1,56 @@
package tech.easyflow.ai.enums;
import java.util.Arrays;
import java.util.Locale;
/**
* AI 资源发布状态。
*/
public enum PublishStatus {
DRAFT("DRAFT"),
PUBLISH_PENDING("PUBLISH_PENDING"),
PUBLISHED("PUBLISHED"),
DELETE_PENDING("DELETE_PENDING");
private final String code;
PublishStatus(String code) {
this.code = code;
}
/**
* 获取状态编码。
*
* @return 状态编码
*/
public String getCode() {
return code;
}
/**
* 是否允许作为线上版本使用。
*
* @return 允许外部访问或线上运行时返回 true
*/
public boolean isExternallyVisible() {
return this == PUBLISHED || this == DELETE_PENDING;
}
/**
* 解析发布状态。
*
* @param code 状态编码
* @return 发布状态
*/
public static PublishStatus from(String code) {
if (code == null || code.isBlank()) {
return DRAFT;
}
String normalized = code.trim().toUpperCase(Locale.ROOT);
return Arrays.stream(values())
.filter(item -> item.code.equals(normalized))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("不支持的发布状态: " + code));
}
}

View File

@@ -0,0 +1,438 @@
package tech.easyflow.ai.publish;
import com.alibaba.fastjson2.JSON;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.BotCategory;
import tech.easyflow.ai.entity.BotMcp;
import tech.easyflow.ai.entity.BotPlugin;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.entity.PluginItem;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.BotCategoryService;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.BotMcpService;
import tech.easyflow.ai.service.BotPluginService;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.ai.service.BotWorkflowService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.McpService;
import tech.easyflow.ai.service.ModelService;
import tech.easyflow.ai.service.PluginItemService;
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.ApprovalSubjectHandler;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.service.SysDeptService;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 聊天助手审批处理器。
*/
@Component
public class BotApprovalSubjectHandler implements ApprovalSubjectHandler {
private static final String SNAPSHOT_KEY = "resourceSnapshot";
private final BotService botService;
private final BotWorkflowService botWorkflowService;
private final BotDocumentCollectionService botDocumentCollectionService;
private final BotPluginService botPluginService;
private final BotMcpService botMcpService;
private final WorkflowService workflowService;
private final DocumentCollectionService documentCollectionService;
private final ApprovalInstanceService approvalInstanceService;
private final CategoryPermissionService categoryPermissionService;
private final ModelService modelService;
private final BotCategoryService botCategoryService;
private final SysDeptService sysDeptService;
private final PluginItemService pluginItemService;
private final McpService mcpService;
public BotApprovalSubjectHandler(BotService botService,
BotWorkflowService botWorkflowService,
BotDocumentCollectionService botDocumentCollectionService,
BotPluginService botPluginService,
BotMcpService botMcpService,
WorkflowService workflowService,
DocumentCollectionService documentCollectionService,
ApprovalInstanceService approvalInstanceService,
CategoryPermissionService categoryPermissionService,
ModelService modelService,
BotCategoryService botCategoryService,
SysDeptService sysDeptService,
PluginItemService pluginItemService,
McpService mcpService) {
this.botService = botService;
this.botWorkflowService = botWorkflowService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.botPluginService = botPluginService;
this.botMcpService = botMcpService;
this.workflowService = workflowService;
this.documentCollectionService = documentCollectionService;
this.approvalInstanceService = approvalInstanceService;
this.categoryPermissionService = categoryPermissionService;
this.modelService = modelService;
this.botCategoryService = botCategoryService;
this.sysDeptService = sysDeptService;
this.pluginItemService = pluginItemService;
this.mcpService = mcpService;
}
@Override
public String resourceType() {
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
public void assertPublishedAccess(Object identifier, String denyMessage) {
Bot bot = botService.getDetail(String.valueOf(identifier));
if (bot == null || !PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()
|| bot.getPublishedSnapshotJson() == null || bot.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException(denyMessage);
}
}
private Bot requireBot(BigInteger id) {
Bot bot = botService.getById(id);
if (bot == null) {
throw new BusinessException("聊天助手不存在");
}
return bot;
}
private void assertManagePermission(Bot bot) {
LoginAccount account = SaTokenUtil.getLoginAccount();
boolean superAdmin = categoryPermissionService.isCurrentSuperAdmin();
boolean creator = account != null && account.getId() != null && account.getId().equals(bot.getCreatedBy());
if (!superAdmin && !creator) {
throw new BusinessException("仅创建者或超级管理员可管理聊天助手");
}
}
private Map<String, Object> buildResourceSnapshot(Bot bot) {
Model model = resolveModel(bot.getModelId());
BotCategory category = resolveCategory(bot.getCategoryId());
SysDept dept = resolveDept(bot.getDeptId());
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("id", bot.getId());
snapshot.put("alias", bot.getAlias());
snapshot.put("deptId", bot.getDeptId());
snapshot.put("deptName", dept == null ? null : dept.getDeptName());
snapshot.put("categoryId", bot.getCategoryId());
snapshot.put("categoryName", category == null ? null : category.getCategoryName());
snapshot.put("title", bot.getTitle());
snapshot.put("description", bot.getDescription());
snapshot.put("icon", bot.getIcon());
snapshot.put("modelId", bot.getModelId());
snapshot.put("modelName", resolveModelName(model));
snapshot.put("modelOptions", bot.getModelOptions());
snapshot.put("systemPrompt", resolveSystemPrompt(bot));
snapshot.put("temperature", readNumberOption(bot.getModelOptions(), "temperature"));
snapshot.put("topP", readNumberOption(bot.getModelOptions(), "topP"));
snapshot.put("topK", readNumberOption(bot.getModelOptions(), "topK"));
snapshot.put("maxReplyLength", readNumberOption(bot.getModelOptions(), "maxReplyLength"));
snapshot.put("maxMessageCount", readNumberOption(bot.getModelOptions(), Bot.KEY_MAX_MESSAGE_COUNT));
snapshot.put("status", bot.getStatus());
snapshot.put("options", bot.getOptions());
snapshot.put("anonymousEnabled", bot.isAnonymousEnabled());
snapshot.put("workflowBindings", buildWorkflowBindings(bot.getId()));
snapshot.put("knowledgeBindings", buildKnowledgeBindings(bot.getId()));
snapshot.put("pluginBindings", buildPluginBindings(bot.getId()));
snapshot.put("mcpBindings", buildMcpBindings(bot.getId()));
return snapshot;
}
private List<Map<String, Object>> buildWorkflowBindings(BigInteger botId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getBotId, botId);
List<BotWorkflow> relations = botWorkflowService.getMapper().selectListWithRelationsByQuery(queryWrapper);
List<Map<String, Object>> result = new ArrayList<>();
for (BotWorkflow relation : relations) {
Workflow workflow = relation.getWorkflow();
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
throw new BusinessException("聊天助手绑定的工作流未发布,无法发布聊天助手");
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("workflowId", relation.getWorkflowId());
item.put("workflowName", workflow.getTitle());
result.add(item);
}
return result;
}
private List<Map<String, Object>> buildKnowledgeBindings(BigInteger botId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId);
List<BotDocumentCollection> relations = botDocumentCollectionService.getMapper().selectListWithRelationsByQuery(queryWrapper);
List<Map<String, Object>> result = new ArrayList<>();
for (BotDocumentCollection relation : relations) {
DocumentCollection knowledge = relation.getKnowledge();
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
throw new BusinessException("聊天助手绑定的知识库未发布,无法发布聊天助手");
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("knowledgeId", relation.getDocumentCollectionId());
item.put("knowledgeName", knowledge.getTitle());
item.put("retrievalMode", relation.getRetrievalMode() == null ? null : relation.getRetrievalMode().name());
result.add(item);
}
return result;
}
private List<Map<String, Object>> buildPluginBindings(BigInteger botId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotPlugin::getBotId, botId);
List<BotPlugin> relations = botPluginService.list(queryWrapper);
List<Map<String, Object>> result = new ArrayList<>();
for (BotPlugin relation : relations) {
PluginItem pluginItem = pluginItemService.getById(relation.getPluginItemId());
if (pluginItem == null) {
throw new BusinessException("聊天助手绑定的插件工具不存在,无法发布聊天助手");
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("pluginItemId", relation.getPluginItemId());
item.put("pluginItemName", resolvePluginName(pluginItem));
result.add(item);
}
return result;
}
private List<Map<String, Object>> buildMcpBindings(BigInteger botId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotMcp::getBotId, botId);
List<BotMcp> relations = botMcpService.list(queryWrapper);
List<Map<String, Object>> result = new ArrayList<>();
for (BotMcp relation : relations) {
Mcp mcp = mcpService.getById(relation.getMcpId());
if (mcp == null) {
throw new BusinessException("聊天助手绑定的MCP不存在无法发布聊天助手");
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("mcpId", relation.getMcpId());
item.put("mcpName", mcp.getTitle());
item.put("mcpToolName", relation.getMcpToolName());
item.put("mcpToolDescription", relation.getMcpToolDescription());
result.add(item);
}
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) {
botDocumentCollectionService.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
botWorkflowService.remove(QueryWrapper.create().eq(BotWorkflow::getBotId, botId));
botPluginService.remove(QueryWrapper.create().eq(BotPlugin::getBotId, botId));
botMcpService.remove(QueryWrapper.create().eq(BotMcp::getBotId, botId));
}
}

View File

@@ -0,0 +1,52 @@
package tech.easyflow.ai.publish;
import org.springframework.stereotype.Service;
import tech.easyflow.approval.annotation.ApprovalAction;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
/**
* 聊天助手发布生命周期应用服务。
*/
@Service
public class BotPublishAppService {
/**
* 提交聊天助手发布审批。
*
* @param id 助手 ID
* @return 助手 ID
*/
@ApprovalAction(
resourceType = "BOT",
actionType = "PUBLISH",
idExpr = "#id"
)
public BigInteger submitPublishApproval(BigInteger id) {
assertId(id);
return id;
}
/**
* 提交聊天助手删除审批。
*
* @param id 助手 ID
* @return 助手 ID
*/
@ApprovalAction(
resourceType = "BOT",
actionType = "DELETE",
idExpr = "#id"
)
public BigInteger submitDeleteApproval(BigInteger id) {
assertId(id);
return id;
}
private void assertId(BigInteger id) {
if (id == null) {
throw new BusinessException("聊天助手审批时资源ID不能为空");
}
}
}

View File

@@ -0,0 +1,333 @@
package tech.easyflow.ai.publish;
import com.alibaba.fastjson2.JSON;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.DocumentCollectionCategory;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.BotDocumentCollectionService;
import tech.easyflow.ai.service.DocumentCollectionCategoryService;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.ai.service.ModelService;
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.ApprovalSubjectHandler;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.enums.VisibilityScope;
import tech.easyflow.system.service.SysDeptService;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 知识库审批处理器。
*/
@Component
public class KnowledgeApprovalSubjectHandler implements ApprovalSubjectHandler {
private static final String SNAPSHOT_KEY = "resourceSnapshot";
private final DocumentCollectionService documentCollectionService;
private final ResourceAccessService resourceAccessService;
private final ApprovalInstanceService approvalInstanceService;
private final BotDocumentCollectionService botDocumentCollectionService;
private final ModelService modelService;
private final DocumentCollectionCategoryService documentCollectionCategoryService;
private final SysDeptService sysDeptService;
public KnowledgeApprovalSubjectHandler(DocumentCollectionService documentCollectionService,
ResourceAccessService resourceAccessService,
ApprovalInstanceService approvalInstanceService,
BotDocumentCollectionService botDocumentCollectionService,
ModelService modelService,
DocumentCollectionCategoryService documentCollectionCategoryService,
SysDeptService sysDeptService) {
this.documentCollectionService = documentCollectionService;
this.resourceAccessService = resourceAccessService;
this.approvalInstanceService = approvalInstanceService;
this.botDocumentCollectionService = botDocumentCollectionService;
this.modelService = modelService;
this.documentCollectionCategoryService = documentCollectionCategoryService;
this.sysDeptService = sysDeptService;
}
@Override
public String resourceType() {
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
public void assertPublishedAccess(Object identifier, String denyMessage) {
DocumentCollection collection = documentCollectionService.getDetail(String.valueOf(identifier));
if (collection == null || !PublishStatus.from(collection.getPublishStatus()).isExternallyVisible()
|| collection.getPublishedSnapshotJson() == null || collection.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException(denyMessage);
}
}
private DocumentCollection requireKnowledge(BigInteger id) {
DocumentCollection knowledge = documentCollectionService.getById(id);
if (knowledge == null) {
throw new BusinessException("知识库不存在");
}
return knowledge;
}
private boolean hasBotBinding(BigInteger knowledgeId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotDocumentCollection::getDocumentCollectionId, knowledgeId);
return botDocumentCollectionService.exists(queryWrapper);
}
private Map<String, Object> buildResourceSnapshot(DocumentCollection collection) {
Model vectorModel = resolveModel(collection.getVectorEmbedModelId(), "知识库向量模型不存在,无法提交审批");
Model rerankModel = resolveOptionalModel(collection.getRerankModelId(), "知识库重排模型不存在,无法提交审批");
DocumentCollectionCategory category = resolveCategory(collection.getCategoryId());
SysDept dept = resolveDept(collection.getDeptId());
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("id", collection.getId());
snapshot.put("collectionType", collection.getCollectionType());
snapshot.put("collectionTypeLabel", resolveCollectionTypeLabel(collection.getCollectionType()));
snapshot.put("alias", collection.getAlias());
snapshot.put("deptId", collection.getDeptId());
snapshot.put("deptName", dept == null ? null : dept.getDeptName());
snapshot.put("icon", collection.getIcon());
snapshot.put("title", collection.getTitle());
snapshot.put("description", collection.getDescription());
snapshot.put("slug", collection.getSlug());
snapshot.put("vectorStoreEnable", collection.getVectorStoreEnable());
snapshot.put("vectorStoreType", collection.getVectorStoreType());
snapshot.put("vectorEmbedModelId", collection.getVectorEmbedModelId());
snapshot.put("vectorEmbedModelName", resolveModelName(vectorModel));
snapshot.put("dimensionOfVectorModel", collection.getDimensionOfVectorModel());
Map<String, Object> options = collection.getOptions() == null
? Collections.emptyMap()
: new LinkedHashMap<>(collection.getOptions());
snapshot.put("options", options);
snapshot.put("canUpdateEmbeddingModel", collection.getOptionsByKey(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL));
snapshot.put("rerankEnable", collection.getOptionsByKey(DocumentCollection.KEY_RERANK_ENABLE));
snapshot.put("rerankModelId", collection.getRerankModelId());
snapshot.put("rerankModelName", rerankModel == null ? null : resolveModelName(rerankModel));
snapshot.put("searchEngineEnable", collection.getSearchEngineEnable());
snapshot.put("englishName", collection.getEnglishName());
snapshot.put("categoryId", collection.getCategoryId());
snapshot.put("categoryName", category == null ? null : category.getCategoryName());
snapshot.put("visibilityScope", collection.getVisibilityScope());
snapshot.put("visibilityScopeLabel", resolveVisibilityScopeLabel(collection.getVisibilityScope()));
return snapshot;
}
/**
* 解析知识库分类信息。
*
* @param categoryId 分类 ID
* @return 分类实体,不存在时返回 {@code null}
*/
private DocumentCollectionCategory resolveCategory(BigInteger categoryId) {
if (categoryId == null) {
return null;
}
return documentCollectionCategoryService.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
* @param errorMessage 模型不存在时抛出的提示
* @return 模型实体
*/
private Model resolveModel(BigInteger modelId, String errorMessage) {
if (modelId == null) {
throw new BusinessException(errorMessage);
}
Model model = modelService.getById(modelId);
if (model == null) {
throw new BusinessException(errorMessage);
}
return model;
}
/**
* 解析可选模型。
*
* @param modelId 模型 ID
* @param errorMessage 模型不存在时抛出的提示
* @return 模型实体,不存在时返回 {@code null}
*/
private Model resolveOptionalModel(BigInteger modelId, String errorMessage) {
if (modelId == null) {
return null;
}
Model model = modelService.getById(modelId);
if (model == null) {
throw new BusinessException(errorMessage);
}
return model;
}
/**
* 生成模型展示名称。
*
* @param model 模型实体
* @return 模型名称
*/
private String resolveModelName(Model model) {
if (model == null) {
return null;
}
if (model.getTitle() != null && !model.getTitle().isBlank()) {
return model.getTitle();
}
return 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) {
if (DocumentCollection.TYPE_FAQ.equalsIgnoreCase(collectionType)) {
return "FAQ";
}
return "文档";
}
@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 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);
}
}

View File

@@ -0,0 +1,52 @@
package tech.easyflow.ai.publish;
import org.springframework.stereotype.Service;
import tech.easyflow.approval.annotation.ApprovalAction;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
/**
* 知识库发布生命周期应用服务。
*/
@Service
public class KnowledgePublishAppService {
/**
* 提交知识库发布审批。
*
* @param id 知识库 ID
* @return 知识库 ID
*/
@ApprovalAction(
resourceType = "KNOWLEDGE",
actionType = "PUBLISH",
idExpr = "#id"
)
public BigInteger submitPublishApproval(BigInteger id) {
assertId(id);
return id;
}
/**
* 提交知识库删除审批。
*
* @param id 知识库 ID
* @return 知识库 ID
*/
@ApprovalAction(
resourceType = "KNOWLEDGE",
actionType = "DELETE",
idExpr = "#id"
)
public BigInteger submitDeleteApproval(BigInteger id) {
assertId(id);
return id;
}
private void assertId(BigInteger id) {
if (id == null) {
throw new BusinessException("知识库审批时资源ID不能为空");
}
}
}

View File

@@ -0,0 +1,190 @@
package tech.easyflow.ai.publish;
import com.alibaba.fastjson2.JSON;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.BotWorkflow;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.BotWorkflowService;
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.ApprovalSubjectHandler;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 工作流审批处理器。
*/
@Component
public class WorkflowApprovalSubjectHandler implements ApprovalSubjectHandler {
private static final String SNAPSHOT_KEY = "resourceSnapshot";
private final WorkflowService workflowService;
private final ResourceAccessService resourceAccessService;
private final ApprovalInstanceService approvalInstanceService;
private final BotWorkflowService botWorkflowService;
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
ResourceAccessService resourceAccessService,
ApprovalInstanceService approvalInstanceService,
BotWorkflowService botWorkflowService) {
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.approvalInstanceService = approvalInstanceService;
this.botWorkflowService = botWorkflowService;
}
@Override
public String resourceType() {
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
public void assertPublishedAccess(Object identifier, String denyMessage) {
Workflow workflow = workflowService.getDetail(String.valueOf(identifier));
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
throw new BusinessException(denyMessage);
}
}
private Workflow requireWorkflow(BigInteger id) {
Workflow workflow = workflowService.getById(id);
if (workflow == null) {
throw new BusinessException("工作流不存在");
}
return workflow;
}
private boolean hasBotBinding(BigInteger workflowId) {
QueryWrapper queryWrapper = QueryWrapper.create().eq(BotWorkflow::getWorkflowId, workflowId);
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);
}
}

View File

@@ -0,0 +1,54 @@
package tech.easyflow.ai.publish;
import org.springframework.stereotype.Service;
import tech.easyflow.approval.annotation.ApprovalAction;
import tech.easyflow.approval.enums.ApprovalActionType;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.common.web.exceptions.BusinessException;
import java.math.BigInteger;
/**
* 工作流发布生命周期应用服务。
*/
@Service
public class WorkflowPublishAppService {
/**
* 提交工作流发布审批。
*
* @param id 工作流 ID
* @return 工作流 ID
*/
@ApprovalAction(
resourceType = "WORKFLOW",
actionType = "PUBLISH",
idExpr = "#id"
)
public BigInteger submitPublishApproval(BigInteger id) {
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.PUBLISH.getCode());
return id;
}
/**
* 提交工作流删除审批。
*
* @param id 工作流 ID
* @return 工作流 ID
*/
@ApprovalAction(
resourceType = "WORKFLOW",
actionType = "DELETE",
idExpr = "#id"
)
public BigInteger submitDeleteApproval(BigInteger id) {
assertId(id, ApprovalResourceType.WORKFLOW.getCode(), ApprovalActionType.DELETE.getCode());
return id;
}
private void assertId(BigInteger id, String resourceType, String actionType) {
if (id == null) {
throw new BusinessException(resourceType + " " + actionType + " 审批时资源ID不能为空");
}
}
}

View File

@@ -30,6 +30,30 @@ public interface BotService extends IService<Bot> {
Bot getByAlias(String alias);
/**
* 获取已发布视图。
*
* @param idOrAlias ID 或别名
* @return 已发布视图
*/
Bot getPublishedDetail(String idOrAlias);
/**
* 根据 ID 获取已发布视图。
*
* @param id 机器人 ID
* @return 已发布视图
*/
Bot getPublishedById(BigInteger id);
/**
* 把当前资源映射为已发布视图。
*
* @param bot 当前资源
* @return 已发布视图
*/
Bot toPublishedView(Bot bot);
SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, BotServiceImpl.ChatCheckResult chatCheckResult);
SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages,

View File

@@ -23,4 +23,28 @@ public interface DocumentCollectionService extends IService<DocumentCollection>
DocumentCollection getDetail(String idOrAlias);
DocumentCollection getByAlias(String idOrAlias);
/**
* 获取已发布视图。
*
* @param idOrAlias ID 或别名
* @return 已发布视图
*/
DocumentCollection getPublishedDetail(String idOrAlias);
/**
* 根据 ID 获取已发布视图。
*
* @param id 知识库 ID
* @return 已发布视图
*/
DocumentCollection getPublishedById(BigInteger id);
/**
* 把当前资源映射为已发布视图。
*
* @param collection 当前资源
* @return 已发布视图
*/
DocumentCollection toPublishedView(DocumentCollection collection);
}

View File

@@ -3,6 +3,8 @@ package tech.easyflow.ai.service;
import tech.easyflow.ai.entity.Workflow;
import com.mybatisflex.core.service.IService;
import java.math.BigInteger;
/**
* 服务层。
*
@@ -18,4 +20,28 @@ public interface WorkflowService extends IService<Workflow> {
Workflow getByAlias(String alias);
/**
* 获取已发布视图。
*
* @param idOrAlias ID 或别名
* @return 已发布视图
*/
Workflow getPublishedDetail(String idOrAlias);
/**
* 根据 ID 获取已发布视图。
*
* @param id 工作流 ID
* @return 已发布视图
*/
Workflow getPublishedById(BigInteger id);
/**
* 把当前资源映射为已发布视图。
*
* @param workflow 当前资源
* @return 已发布视图
*/
Workflow toPublishedView(Workflow workflow);
}

View File

@@ -1,5 +1,6 @@
package tech.easyflow.ai.service.impl;
import com.alibaba.fastjson2.JSON;
import cn.dev33.satoken.stp.StpUtil;
import com.easyagents.core.file2text.File2TextService;
import com.easyagents.core.file2text.source.HttpDocumentSource;
@@ -27,7 +28,10 @@ import tech.easyflow.ai.easyagents.listener.ChatStreamListener;
import tech.easyflow.ai.easyagents.memory.DefaultBotMessageMemory;
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
import tech.easyflow.ai.entity.*;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.BotMapper;
import tech.easyflow.ai.service.*;
import tech.easyflow.ai.utils.CustomBeanUtils;
@@ -75,6 +79,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
private Map<String, Object> modelOptions;
private ChatModel chatModel;
private String conversationIdStr;
private boolean publishedAccess;
public Bot getAiBot() {return aiBot;}
@@ -91,6 +96,10 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
public String getConversationIdStr() {return conversationIdStr;}
public void setConversationIdStr(String conversationIdStr) {this.conversationIdStr = conversationIdStr;}
public boolean isPublishedAccess() {return publishedAccess;}
public void setPublishedAccess(boolean publishedAccess) {this.publishedAccess = publishedAccess;}
}
@Resource(name = "sseThreadPool")
@@ -100,8 +109,12 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
@Resource
private BotWorkflowService botWorkflowService;
@Resource
private WorkflowService workflowService;
@Resource
private BotDocumentCollectionService botDocumentCollectionService;
@Resource
private DocumentCollectionService documentCollectionService;
@Resource
private BotPluginService botPluginService;
@Resource
private PluginItemService pluginItemService;
@@ -141,6 +154,57 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
return getOne(queryWrapper);
}
/**
* {@inheritDoc}
*/
@Override
public Bot getPublishedDetail(String id) {
return toPublishedView(getDetail(id));
}
/**
* {@inheritDoc}
*/
@Override
public Bot getPublishedById(BigInteger id) {
return toPublishedView(getById(id));
}
/**
* {@inheritDoc}
*/
@Override
public Bot toPublishedView(Bot bot) {
if (bot == null) {
return null;
}
Map<String, Object> snapshot = bot.getPublishedSnapshotJson();
if (snapshot == null || snapshot.isEmpty()) {
return bot;
}
Bot published = JSON.parseObject(JSON.toJSONString(snapshot), Bot.class);
if (published == null) {
return bot;
}
published.setId(bot.getId());
published.setTenantId(bot.getTenantId());
published.setCategoryId(bot.getCategoryId());
published.setDeptId(bot.getDeptId());
published.setCreated(bot.getCreated());
published.setCreatedBy(bot.getCreatedBy());
published.setModified(bot.getModified());
published.setModifiedBy(bot.getModifiedBy());
published.setPublishStatus(bot.getPublishStatus());
published.setCurrentApprovalInstanceId(bot.getCurrentApprovalInstanceId());
published.setPublishedSnapshotJson(bot.getPublishedSnapshotJson());
published.setPublishedAt(bot.getPublishedAt());
published.setPublishedBy(bot.getPublishedBy());
if (published.getPublishStatus() == null) {
published.setPublishStatus(PublishStatus.DRAFT.getCode());
}
return published;
}
public SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, ChatCheckResult chatCheckResult) {
if (!StringUtils.hasLength(prompt)) {
return ChatSseUtil.sendSystemError(conversationId, "提示词不能为空");
@@ -175,6 +239,16 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
if ((!login || anonymousAccount) && !aiBot.isAnonymousEnabled()) {
return ChatSseUtil.sendSystemError(conversationId, "此聊天助手不支持匿名访问");
}
if (!login || anonymousAccount) {
Bot publishedBot = toPublishedView(aiBot);
if (!PublishStatus.from(aiBot.getPublishStatus()).isExternallyVisible()) {
return ChatSseUtil.sendSystemError(conversationId, "聊天助手尚未发布");
}
aiBot = publishedBot;
chatCheckResult.setPublishedAccess(true);
} else {
chatCheckResult.setPublishedAccess(false);
}
Map<String, Object> modelOptions = aiBot.getModelOptions();
Model model = modelService.getModelInstance(aiBot.getModelId());
if (model == null) {
@@ -213,7 +287,10 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
prompt = "【用户问题】:\n" + prompt + "\n\n请基于用户上传的附件内容回答用户问题 \n" + "【用户上传的附件内容】:\n" + attachmentsToString ;
}
UserMessage userMessage = new UserMessage(prompt);
userMessage.addTools(buildFunctionList(Maps.of("botId", botId).set("needEnglishName", false)));
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
.set("needEnglishName", false)
.set("bot", chatCheckResult.getAiBot())
.set("publishedOnly", chatCheckResult.isPublishedAccess())));
ChatOptions chatOptions = getChatOptions(modelOptions);
Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
chatOptions.setThinkingEnabled(enableDeepThinking);
@@ -270,6 +347,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
.set("needEnglishName", false)
.set("needAccountId", false)
.set("bot", chatCheckResult.getAiBot())
.set("publishedOnly", chatCheckResult.isPublishedAccess())
));
ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
SseEmitter emitter = chatSseEmitter.getEmitter();
@@ -380,30 +459,39 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
if (needEnglishName == null) {
needEnglishName = false;
}
Bot runtimeBot = (Bot) buildParams.get("bot");
boolean usePublishedSnapshot = Boolean.TRUE.equals(buildParams.get("publishedOnly"))
&& runtimeBot != null
&& runtimeBot.getPublishedSnapshotJson() != null
&& PublishStatus.from(runtimeBot.getPublishStatus()).isExternallyVisible();
QueryWrapper queryWrapper = QueryWrapper.create();
// 工作流 function 集合
queryWrapper.eq(BotWorkflow::getBotId, botId);
List<BotWorkflow> botWorkflows = botWorkflowService.getMapper()
.selectListWithRelationsByQuery(queryWrapper);
if (botWorkflows != null && !botWorkflows.isEmpty()) {
for (BotWorkflow botWorkflow : botWorkflows) {
Tool function = botWorkflow.getWorkflow().toFunction(needEnglishName);
functionList.add(function);
if (usePublishedSnapshot) {
appendPublishedWorkflowTools(functionList, runtimeBot, needEnglishName);
appendPublishedKnowledgeTools(functionList, runtimeBot, needEnglishName);
} else {
// 工作流 function 集合
queryWrapper.eq(BotWorkflow::getBotId, botId);
List<BotWorkflow> botWorkflows = botWorkflowService.getMapper()
.selectListWithRelationsByQuery(queryWrapper);
if (botWorkflows != null && !botWorkflows.isEmpty()) {
for (BotWorkflow botWorkflow : botWorkflows) {
Tool function = botWorkflow.getWorkflow().toFunction(needEnglishName);
functionList.add(function);
}
}
}
// 知识库 function 集合
queryWrapper = QueryWrapper.create();
queryWrapper.eq(BotDocumentCollection::getBotId, botId);
List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
.selectListWithRelationsByQuery(queryWrapper);
if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) {
for (BotDocumentCollection botDocumentCollection : botDocumentCollections) {
Tool function = botDocumentCollection.getKnowledge()
.toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name());
functionList.add(function);
// 知识库 function 集合
queryWrapper = QueryWrapper.create();
queryWrapper.eq(BotDocumentCollection::getBotId, botId);
List<BotDocumentCollection> botDocumentCollections = botDocumentCollectionService.getMapper()
.selectListWithRelationsByQuery(queryWrapper);
if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) {
for (BotDocumentCollection botDocumentCollection : botDocumentCollections) {
Tool function = botDocumentCollection.getKnowledge()
.toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name());
functionList.add(function);
}
}
}
@@ -437,6 +525,59 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
return functionList;
}
@SuppressWarnings("unchecked")
private void appendPublishedWorkflowTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
Object workflows = runtimeBot.getPublishedSnapshotJson().get("workflowBindings");
if (!(workflows instanceof List<?> workflowBindings)) {
return;
}
for (Object item : workflowBindings) {
if (!(item instanceof Map<?, ?> workflowMap)) {
continue;
}
Object workflowId = workflowMap.get("workflowId");
if (workflowId == null) {
continue;
}
Workflow workflow = workflowService.getPublishedById(new BigInteger(String.valueOf(workflowId)));
if (workflow == null) {
continue;
}
WorkflowTool tool = new WorkflowTool(
workflow,
needEnglishName,
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
);
functionList.add(tool);
}
}
@SuppressWarnings("unchecked")
private void appendPublishedKnowledgeTools(List<Tool> functionList, Bot runtimeBot, boolean needEnglishName) {
Object knowledges = runtimeBot.getPublishedSnapshotJson().get("knowledgeBindings");
if (!(knowledges instanceof List<?> knowledgeBindings)) {
return;
}
for (Object item : knowledgeBindings) {
if (!(item instanceof Map<?, ?> bindingMap)) {
continue;
}
Object knowledgeId = bindingMap.get("knowledgeId");
if (knowledgeId == null) {
continue;
}
DocumentCollection knowledge = documentCollectionService.getPublishedById(new BigInteger(String.valueOf(knowledgeId)));
if (knowledge == null) {
continue;
}
Object retrievalMode = bindingMap.get("retrievalMode");
functionList.add(knowledge.toFunction(
needEnglishName,
retrievalMode == null ? null : String.valueOf(retrievalMode)
));
}
}
public String attachmentsToString(List<String> fileList) {
StringBuilder messageBuilder = new StringBuilder();
if (fileList != null && !fileList.isEmpty()) {

View File

@@ -1,5 +1,6 @@
package tech.easyflow.ai.service.impl;
import com.alibaba.fastjson2.JSON;
import com.easyagents.core.document.Document;
import com.easyagents.core.model.rerank.RerankException;
import com.easyagents.core.model.rerank.RerankModel;
@@ -29,6 +30,7 @@ import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.entity.FaqItem;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.DocumentChunkMapper;
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
import tech.easyflow.ai.mapper.FaqItemMapper;
@@ -148,6 +150,57 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
return formatDocuments(searchDocuments, shouldApplyMinSimilarityFilter(retrievalMode, reranked), minSimilarity, docRecallMaxNum);
}
/**
* {@inheritDoc}
*/
@Override
public DocumentCollection getPublishedDetail(String idOrAlias) {
return toPublishedView(getDetail(idOrAlias));
}
/**
* {@inheritDoc}
*/
@Override
public DocumentCollection getPublishedById(BigInteger id) {
return toPublishedView(getById(id));
}
/**
* {@inheritDoc}
*/
@Override
public DocumentCollection toPublishedView(DocumentCollection collection) {
if (collection == null) {
return null;
}
Map<String, Object> snapshot = collection.getPublishedSnapshotJson();
if (snapshot == null || snapshot.isEmpty()) {
return collection;
}
DocumentCollection published = JSON.parseObject(JSON.toJSONString(snapshot), DocumentCollection.class);
if (published == null) {
return collection;
}
published.setId(collection.getId());
published.setTenantId(collection.getTenantId());
published.setCategoryId(collection.getCategoryId());
published.setDeptId(collection.getDeptId());
published.setCreated(collection.getCreated());
published.setCreatedBy(collection.getCreatedBy());
published.setModified(collection.getModified());
published.setModifiedBy(collection.getModifiedBy());
published.setPublishStatus(collection.getPublishStatus());
published.setCurrentApprovalInstanceId(collection.getCurrentApprovalInstanceId());
published.setPublishedSnapshotJson(collection.getPublishedSnapshotJson());
published.setPublishedAt(collection.getPublishedAt());
published.setPublishedBy(collection.getPublishedBy());
if (published.getPublishStatus() == null) {
published.setPublishStatus(PublishStatus.DRAFT.getCode());
}
return published;
}
private VectorRetriever buildVectorRetriever(DocumentCollection documentCollection,
int docRecallMaxNum,
Float minSimilarity) {

View File

@@ -1,7 +1,8 @@
package tech.easyflow.ai.service.impl;
import com.alibaba.fastjson2.JSON;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.mapper.WorkflowMapper;
import tech.easyflow.ai.service.WorkflowService;
import com.mybatisflex.spring.service.impl.ServiceImpl;
@@ -11,6 +12,9 @@ import com.mybatisflex.core.query.QueryWrapper;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.ai.utils.CustomBeanUtils;
import java.math.BigInteger;
import java.util.Map;
/**
* 服务层实现。
*
@@ -52,6 +56,57 @@ public class WorkflowServiceImpl extends ServiceImpl<WorkflowMapper, Workflow> i
}
/**
* {@inheritDoc}
*/
@Override
public Workflow getPublishedDetail(String idOrAlias) {
return toPublishedView(getDetail(idOrAlias));
}
/**
* {@inheritDoc}
*/
@Override
public Workflow getPublishedById(BigInteger id) {
return toPublishedView(getById(id));
}
/**
* {@inheritDoc}
*/
@Override
public Workflow toPublishedView(Workflow workflow) {
if (workflow == null) {
return null;
}
Map<String, Object> snapshot = workflow.getPublishedSnapshotJson();
if (snapshot == null || snapshot.isEmpty()) {
return workflow;
}
Workflow published = JSON.parseObject(JSON.toJSONString(snapshot), Workflow.class);
if (published == null) {
return workflow;
}
published.setId(workflow.getId());
published.setTenantId(workflow.getTenantId());
published.setCategoryId(workflow.getCategoryId());
published.setDeptId(workflow.getDeptId());
published.setCreated(workflow.getCreated());
published.setCreatedBy(workflow.getCreatedBy());
published.setModified(workflow.getModified());
published.setModifiedBy(workflow.getModifiedBy());
published.setPublishStatus(workflow.getPublishStatus());
published.setCurrentApprovalInstanceId(workflow.getCurrentApprovalInstanceId());
published.setPublishedSnapshotJson(workflow.getPublishedSnapshotJson());
published.setPublishedAt(workflow.getPublishedAt());
published.setPublishedBy(workflow.getPublishedBy());
if (published.getPublishStatus() == null) {
published.setPublishStatus(PublishStatus.DRAFT.getCode());
}
return published;
}
@Override
public boolean updateById(Workflow entity, boolean ignoreNulls) {