Compare commits
36 Commits
2689adfa40
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| cb379e071c | |||
| 8b80770960 | |||
| c316eff5be | |||
| 1ea863cb2c | |||
| 0f4d10c43c | |||
| cc3bb9cff0 | |||
| e39f7521e2 | |||
| 1c205c3720 | |||
| 11e595b088 | |||
| 72df00f25b | |||
| 6c3d98eaac | |||
| b7f3ae2854 | |||
| 2907acac95 | |||
| 0947009ee6 | |||
| a186066641 | |||
| 1a6ea64e80 | |||
| da58077d59 | |||
| 47c2bad839 | |||
| 2ad8935a61 | |||
| 21b1bc82f6 | |||
| 4a15124183 | |||
| e27834ee0c | |||
| c1590b0d8a | |||
| ff863e3c27 | |||
| 516d43ce7d | |||
| 8d07b306e5 | |||
| ba70fec9a5 | |||
| 31b0e21d3d | |||
| 5827ecde42 | |||
| 1d8b9d9662 | |||
| a5aab86de2 | |||
| 51198ff492 | |||
| 9feb889637 | |||
| 8546d927bc | |||
| 4130381658 | |||
| ad67ba85ad |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ build/
|
|||||||
.jlsp/
|
.jlsp/
|
||||||
.arts/
|
.arts/
|
||||||
luceneKnowledge
|
luceneKnowledge
|
||||||
|
**/*.lic
|
||||||
|
|
||||||
# v1
|
# v1
|
||||||
/easyflow-ui-react
|
/easyflow-ui-react
|
||||||
31
Dockerfile
31
Dockerfile
@@ -1,3 +1,4 @@
|
|||||||
|
# 后端构建脚本
|
||||||
FROM --platform=linux/amd64 swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/eclipse-temurin:17-jre
|
FROM --platform=linux/amd64 swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/eclipse-temurin:17-jre
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
@@ -8,12 +9,40 @@ ENV EASYFLOW_JAR_PATH=/app/artifacts/easyflow.jar
|
|||||||
ENV EASYFLOW_CONFIG_PATH=file:/app/application.yml
|
ENV EASYFLOW_CONFIG_PATH=file:/app/application.yml
|
||||||
ENV EASYFLOW_LOG_FILE=/app/logs/app.log
|
ENV EASYFLOW_LOG_FILE=/app/logs/app.log
|
||||||
ENV EASYFLOW_JAR_RESTART_GRACE_SECONDS=30
|
ENV EASYFLOW_JAR_RESTART_GRACE_SECONDS=30
|
||||||
|
ENV NPM_CONFIG_REGISTRY=https://registry.npmmirror.com
|
||||||
|
ENV PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
ENV PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN useradd --system --create-home easyflow && \
|
RUN useradd --system --create-home easyflow && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends python3 inotify-tools tini && \
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
gnupg && \
|
||||||
|
mkdir -p /etc/apt/keyrings && \
|
||||||
|
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /tmp/nodesource.gpg.key && \
|
||||||
|
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg /tmp/nodesource.gpg.key && \
|
||||||
|
chmod 644 /etc/apt/keyrings/nodesource.gpg && \
|
||||||
|
printf "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main\n" > /etc/apt/sources.list.d/nodesource.list && \
|
||||||
|
rm -f /tmp/nodesource.gpg.key && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
inotify-tools \
|
||||||
|
nodejs \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
tini && \
|
||||||
|
ln -sf /usr/bin/python3 /usr/local/bin/python && \
|
||||||
|
ln -sf /usr/bin/pip3 /usr/local/bin/pip && \
|
||||||
|
npm config set registry "${NPM_CONFIG_REGISTRY}" && \
|
||||||
|
printf "registry=%s\n" "${NPM_CONFIG_REGISTRY}" > /etc/npmrc && \
|
||||||
|
npm install -g pnpm@10.17.1 && \
|
||||||
|
pnpm config set registry "${NPM_CONFIG_REGISTRY}" && \
|
||||||
|
mkdir -p /etc/pip && \
|
||||||
|
printf "[global]\nindex-url = %s\ntrusted-host = %s\n" "${PIP_INDEX_URL}" "${PIP_TRUSTED_HOST}" > /etc/pip.conf && \
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
rm -rf /var/lib/apt/lists/* && \
|
||||||
mkdir -p /app/logs /app/artifacts /app/data && \
|
mkdir -p /app/logs /app/artifacts /app/data && \
|
||||||
chown -R easyflow:easyflow /app
|
chown -R easyflow:easyflow /app
|
||||||
|
|||||||
61
config/proguard/common-keep.pro
Normal file
61
config/proguard/common-keep.pro
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
-dontshrink
|
||||||
|
-dontoptimize
|
||||||
|
-dontpreverify
|
||||||
|
-ignorewarnings
|
||||||
|
-dontnote
|
||||||
|
|
||||||
|
-libraryjars <java.home>/jmods/java.base.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.compiler.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.datatransfer.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.desktop.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.instrument.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.logging.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.management.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.naming.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.net.http.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.prefs.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.rmi.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.scripting.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.security.jgss.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.security.sasl.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.sql.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.transaction.xa.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.xml.jmod(!**.jar;!module-info.class)
|
||||||
|
-libraryjars <java.home>/jmods/java.xml.crypto.jmod(!**.jar;!module-info.class)
|
||||||
|
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations,AnnotationDefault,Signature,InnerClasses,EnclosingMethod,Record,SourceFile,LineNumberTable,MethodParameters
|
||||||
|
|
||||||
|
-keep @org.springframework.stereotype.Controller class * { *; }
|
||||||
|
-keep @org.springframework.web.bind.annotation.RestController class * { *; }
|
||||||
|
-keep @org.springframework.context.annotation.Configuration class * { *; }
|
||||||
|
-keep @org.springframework.boot.context.properties.ConfigurationProperties class * { *; }
|
||||||
|
-keep @org.springframework.boot.autoconfigure.SpringBootApplication class * { *; }
|
||||||
|
|
||||||
|
-keep class **.*Controller { *; }
|
||||||
|
-keep class **.*Mapper { *; }
|
||||||
|
-keep class **.mapper.** { *; }
|
||||||
|
-keep class **.entity.** { *; }
|
||||||
|
-keep class **.dto.** { *; }
|
||||||
|
-keep class **.vo.** { *; }
|
||||||
|
-keep class **.model.** { *; }
|
||||||
|
-keep class **.config.** { *; }
|
||||||
|
-keep class **.enums.** { *; }
|
||||||
|
-keep class **.annotation.** { *; }
|
||||||
|
-keep class **.*Exception { *; }
|
||||||
|
-keep class **.*ErrorCode { *; }
|
||||||
|
-keep class **.*Properties { *; }
|
||||||
|
-keep class **.*Config { *; }
|
||||||
|
-keep class **.*Configuration { *; }
|
||||||
|
-keep interface tech.easyflow.** { *; }
|
||||||
|
-keep enum tech.easyflow.** { *; }
|
||||||
|
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@jakarta.annotation.Resource <fields>;
|
||||||
|
@org.springframework.beans.factory.annotation.Autowired <fields>;
|
||||||
|
@org.springframework.beans.factory.annotation.Value <fields>;
|
||||||
|
@org.springframework.context.annotation.Bean <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * {
|
||||||
|
public <init>(...);
|
||||||
|
}
|
||||||
28
config/proguard/easyflow-module-ai.pro
Normal file
28
config/proguard/easyflow-module-ai.pro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.ai.chattime.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.constants.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.document.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.documentimport.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagents.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.exception.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.mcp.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.node.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.permission.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.plugin.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.publish.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.rag.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.support.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.utils.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.model.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.protocol.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.exception.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.mapper.OpenAiProtocolMapper { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.provider.ModelProviderGateway { *; }
|
||||||
|
-keep class tech.easyflow.ai.invoke.provider.UnifiedChatChunkObserver { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.config.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.entity.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.service.** { *; }
|
||||||
|
-keep class tech.easyflow.ai.easyagentsflow.support.** { *; }
|
||||||
5
config/proguard/easyflow-module-autoconfig.pro
Normal file
5
config/proguard/easyflow-module-autoconfig.pro
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseBootstrapValidator { *; }
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseProperties { *; }
|
||||||
|
-keep class tech.easyflow.autoconfig.license.EasyflowLicenseVerificationResult { *; }
|
||||||
10
config/proguard/easyflow-module-datacenter.pro
Normal file
10
config/proguard/easyflow-module-datacenter.pro
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-include ../../config/proguard/common-keep.pro
|
||||||
|
|
||||||
|
-keep class tech.easyflow.datacenter.connector.DatacenterConnector { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.QueryExecutor { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.WriteExecutor { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.MetadataExplorer { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.SourceHealthChecker { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.connector.SqlDialect { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.execution.model.** { *; }
|
||||||
|
-keep class tech.easyflow.datacenter.meta.enums.** { *; }
|
||||||
@@ -20,6 +20,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-agent</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-chatlog</artifactId>
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.entity.AgentCategory;
|
||||||
|
import tech.easyflow.agent.mapper.AgentMapper;
|
||||||
|
import tech.easyflow.agent.service.AgentCategoryService;
|
||||||
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
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.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 分类管理控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agentCategory")
|
||||||
|
@UsePermission(moduleName = "/api/v1/agent")
|
||||||
|
public class AgentCategoryController extends BaseCurdController<AgentCategoryService, AgentCategory> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AgentMapper agentMapper;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 分类管理控制器。
|
||||||
|
*
|
||||||
|
* @param service Agent 分类服务
|
||||||
|
*/
|
||||||
|
public AgentCategoryController(AgentCategoryService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户可见的 Agent 分类。
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否转树
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方式
|
||||||
|
* @return 可见分类列表
|
||||||
|
*/
|
||||||
|
@GetMapping("visibleList")
|
||||||
|
public Result<List<AgentCategory>> visibleList(AgentCategory entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||||
|
if (access.isRestricted()) {
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
queryWrapper.in("id", access.getCategoryIds());
|
||||||
|
}
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除分类前校验是否仍被 Agent 使用。
|
||||||
|
*
|
||||||
|
* @param ids 分类 ID 集合
|
||||||
|
* @return 校验结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create().eq(Agent::getCategoryId, id);
|
||||||
|
List<Agent> agents = agentMapper.selectListByQuery(queryWrapper);
|
||||||
|
if (agents != null && !agents.isEmpty()) {
|
||||||
|
throw new BusinessException("请先删除该分类下的所有 Agent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onRemoveBefore(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import com.mybatisflex.core.paginate.Page;
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||||
|
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||||
|
import tech.easyflow.agent.publish.AgentPublishAppService;
|
||||||
|
import tech.easyflow.agent.runtime.AgentChatRequest;
|
||||||
|
import tech.easyflow.agent.runtime.AgentDraftChatRequest;
|
||||||
|
import tech.easyflow.agent.runtime.AgentRunService;
|
||||||
|
import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||||
|
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||||
|
import tech.easyflow.agent.service.AgentService;
|
||||||
|
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static tech.easyflow.agent.entity.table.AgentTableDef.AGENT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agent")
|
||||||
|
public class AgentController extends BaseCurdController<AgentService, Agent> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AgentToolBindingService agentToolBindingService;
|
||||||
|
@Resource
|
||||||
|
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||||
|
@Resource
|
||||||
|
private AgentRunService agentRunService;
|
||||||
|
@Resource
|
||||||
|
private AgentPublishAppService agentPublishAppService;
|
||||||
|
@Resource
|
||||||
|
private ResourceAccessService resourceAccessService;
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
@Resource
|
||||||
|
private AgentApprovalStateService agentApprovalStateService;
|
||||||
|
@Resource
|
||||||
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 控制器。
|
||||||
|
*
|
||||||
|
* @param service Agent 服务
|
||||||
|
*/
|
||||||
|
public AgentController(AgentService service) {
|
||||||
|
super(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 详情。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/getDetail")
|
||||||
|
public Result<Agent> getDetail(BigInteger id) {
|
||||||
|
Agent agent = service.getDetail(id);
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(agent);
|
||||||
|
return Result.ok(agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Agent 草稿。
|
||||||
|
*
|
||||||
|
* @param agent Agent 草稿
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@PostMapping("save")
|
||||||
|
public Result<?> save(@JsonBody Agent agent) {
|
||||||
|
return Result.ok(service.saveDraft(agent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 草稿。
|
||||||
|
*
|
||||||
|
* @param agent Agent 草稿
|
||||||
|
* @return Agent 详情
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@PostMapping("update")
|
||||||
|
public Result<?> update(@JsonBody Agent agent) {
|
||||||
|
return Result.ok(service.updateDraft(agent));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 列表。
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否转树
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方式
|
||||||
|
* @return Agent 列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Result<List<Agent>> list(Agent entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
HttpServletRequest request = currentRequest();
|
||||||
|
QueryWrapper queryWrapper = request == null ? QueryWrapper.create() : buildQueryWrapper(request);
|
||||||
|
if (!applyCategoryPermission(queryWrapper)) {
|
||||||
|
return Result.ok(Collections.emptyList());
|
||||||
|
}
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
List<Agent> agents = service.list(queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
agents = agents.stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList();
|
||||||
|
}
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(agents);
|
||||||
|
aiResourceCreatorNameSupport.fillAgentCreatorNames(agents);
|
||||||
|
return Result.ok(agents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行 Agent 纯文本聊天。
|
||||||
|
*
|
||||||
|
* @param request 聊天请求
|
||||||
|
* @return SSE Emitter
|
||||||
|
*/
|
||||||
|
@PostMapping("chat")
|
||||||
|
public SseEmitter chat(@JsonBody AgentChatRequest request) {
|
||||||
|
return agentRunService.chat(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行 Agent 草稿态纯文本试用。
|
||||||
|
*
|
||||||
|
* @param request 草稿试用请求
|
||||||
|
* @return SSE Emitter
|
||||||
|
*/
|
||||||
|
@PostMapping("/chat/draft")
|
||||||
|
public SseEmitter chatDraft(@JsonBody AgentDraftChatRequest request) {
|
||||||
|
return agentRunService.chatDraft(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 Agent 草稿试运行会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 草稿试运行会话 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/chat/draft/clear")
|
||||||
|
public Result<Void> clearDraftSession(@JsonBody(value = "sessionId", required = true) String sessionId) {
|
||||||
|
agentRunService.clearDraftSession(sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批准工具执行。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/run/approve")
|
||||||
|
public Result<Void> approve(@JsonBody("requestId") String requestId,
|
||||||
|
@JsonBody(value = "resumeToken", required = true) String resumeToken) {
|
||||||
|
agentRunService.approve(requestId, resumeToken);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝工具执行。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @param reason 拒绝原因
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/run/reject")
|
||||||
|
public Result<Void> reject(@JsonBody("requestId") String requestId,
|
||||||
|
@JsonBody(value = "resumeToken", required = true) String resumeToken,
|
||||||
|
@JsonBody("reason") String reason) {
|
||||||
|
agentRunService.reject(requestId, resumeToken, reason);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 工具绑定。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param bindings 工具绑定
|
||||||
|
* @return 保存后的启用绑定
|
||||||
|
*/
|
||||||
|
@PostMapping("/toolBinding/update")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<List<AgentToolBinding>> updateToolBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
|
||||||
|
@JsonBody("bindings") List<AgentToolBinding> bindings) {
|
||||||
|
return Result.ok(agentToolBindingService.replaceBindings(agentId, bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 Agent 知识库绑定。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param bindings 知识库绑定
|
||||||
|
* @return 保存后的启用绑定
|
||||||
|
*/
|
||||||
|
@PostMapping("/knowledgeBinding/update")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<List<AgentKnowledgeBinding>> updateKnowledgeBinding(@JsonBody(value = "agentId", required = true) BigInteger agentId,
|
||||||
|
@JsonBody("bindings") List<AgentKnowledgeBinding> bindings) {
|
||||||
|
return Result.ok(agentKnowledgeBindingService.replaceBindings(agentId, bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交发布审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitPublishApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<BigInteger> submitPublishApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitPublishApproval(id), "已提交发布审批", "已直接发布");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交下线审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitOfflineApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/save")
|
||||||
|
public Result<BigInteger> submitOfflineApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitOfflineApproval(id), "已提交下线审批", "已直接下线");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交删除审批。
|
||||||
|
*
|
||||||
|
* @param id Agent ID
|
||||||
|
* @return 审批实例 ID
|
||||||
|
*/
|
||||||
|
@PostMapping("/submitDeleteApproval")
|
||||||
|
@SaCheckPermission("/api/v1/agent/remove")
|
||||||
|
public Result<BigInteger> submitDeleteApproval(@JsonBody("id") BigInteger id) {
|
||||||
|
return buildApprovalActionResult(agentPublishAppService.submitDeleteApproval(id), "已提交删除审批", "已直接删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
|
for (Serializable id : ids) {
|
||||||
|
Agent agent = service.getById(String.valueOf(id));
|
||||||
|
if (agent != null) {
|
||||||
|
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限删除该 Agent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
agentToolBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||||
|
agentKnowledgeBindingService.remove(QueryWrapper.create().in("agent_id", ids));
|
||||||
|
return super.onRemoveBefore(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 分页。
|
||||||
|
*
|
||||||
|
* @param page 分页参数
|
||||||
|
* @param queryWrapper 查询条件
|
||||||
|
* @return Agent 分页
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Page<Agent> queryPage(Page<Agent> page, QueryWrapper queryWrapper) {
|
||||||
|
if (!applyCategoryPermission(queryWrapper)) {
|
||||||
|
return new Page<>(Collections.emptyList(), page.getPageNumber(), page.getPageSize(), 0L);
|
||||||
|
}
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
|
Page<Agent> result = super.queryPage(page, queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
result.setRecords(result.getRecords().stream().map(agent -> service.fromSnapshot(agent.getPublishedSnapshotJson())).toList());
|
||||||
|
}
|
||||||
|
agentApprovalStateService.fillAgentApprovalState(result.getRecords());
|
||||||
|
aiResourceCreatorNameSupport.fillAgentCreatorNames(result.getRecords());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean applyCategoryPermission(QueryWrapper queryWrapper) {
|
||||||
|
RoleCategoryAccessSnapshot access = categoryPermissionService.getCurrentAccess(CategoryResourceType.AGENT.getCode());
|
||||||
|
if (!access.isRestricted()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (access.getCategoryIds().isEmpty()) {
|
||||||
|
queryWrapper.eq(Agent::getCreatedBy, access.getAccountId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
queryWrapper.and(AGENT.CREATED_BY.eq(access.getAccountId()).or(AGENT.CATEGORY_ID.in(access.getCategoryIds())));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPublishedOnlyRequest() {
|
||||||
|
HttpServletRequest request = currentRequest();
|
||||||
|
if (request == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(request.getParameter("publishedOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 HTTP 请求。
|
||||||
|
*
|
||||||
|
* @return 当前请求,不在 Web 请求上下文中时返回 null
|
||||||
|
*/
|
||||||
|
private HttpServletRequest currentRequest() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return attributes.getRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result<BigInteger> buildApprovalActionResult(ApprovalActionResult actionResult,
|
||||||
|
String approvalMessage,
|
||||||
|
String directMessage) {
|
||||||
|
return Result.ok(actionResult.isApprovalRequired() ? approvalMessage : directMessage, actionResult.getInstanceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package tech.easyflow.admin.controller.agent;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.service.agent.AgentSessionService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端会话控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/agent/session")
|
||||||
|
public class AgentSessionController {
|
||||||
|
|
||||||
|
private final AgentSessionService agentSessionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 管理端会话控制器。
|
||||||
|
*
|
||||||
|
* @param agentSessionService Agent 会话服务
|
||||||
|
*/
|
||||||
|
public AgentSessionController(AgentSessionService agentSessionService) {
|
||||||
|
this.agentSessionService = agentSessionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 Agent 会话 ID。
|
||||||
|
*
|
||||||
|
* @return 会话 ID 字符串
|
||||||
|
*/
|
||||||
|
@GetMapping("/generateId")
|
||||||
|
public Result<String> generateId() {
|
||||||
|
long nextId = new SnowFlakeIDKeyGenerator().nextId();
|
||||||
|
return Result.ok(String.valueOf(nextId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话分页。
|
||||||
|
*
|
||||||
|
* @param agentId Agent ID,可为空
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 会话分页
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public Result<ChatWorkspaceSessionPage> list(BigInteger agentId, ChatPageQuery query) {
|
||||||
|
return Result.ok(agentSessionService.queryCurrentUserSessions(currentAccount(), agentId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话详情。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 会话详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> detail(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(agentSessionService.getCurrentUserSession(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 会话消息。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 消息分页
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> messages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
return Result.ok(agentSessionService.queryCurrentUserMessages(currentAccount(), sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 Agent 完整会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话
|
||||||
|
*/
|
||||||
|
@GetMapping("/{sessionId}/conversation")
|
||||||
|
public Result<ChatWorkspaceConversationView> conversation(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(agentSessionService.getCurrentUserConversation(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/rename")
|
||||||
|
public Result<Void> rename(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "title", required = true) String title) {
|
||||||
|
agentSessionService.renameCurrentUserSession(currentAccount(), sessionId, title);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 Agent 会话临时知识库。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param knowledgeIds 临时知识库 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/extraKnowledges")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> saveExtraKnowledges(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "knowledgeIds") List<BigInteger> knowledgeIds) {
|
||||||
|
return Result.ok(agentSessionService.saveCurrentUserExtraKnowledges(currentAccount(), sessionId, knowledgeIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 操作结果
|
||||||
|
*/
|
||||||
|
@PostMapping("/{sessionId}/delete")
|
||||||
|
public Result<Void> delete(@PathVariable BigInteger sessionId) {
|
||||||
|
agentSessionService.deleteCurrentUserSession(currentAccount(), sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount currentAccount() {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,12 +13,17 @@ import com.mybatisflex.core.query.QueryWrapper;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
|
||||||
|
import tech.easyflow.admin.service.ai.ChatWorkspaceService;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
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.enums.PublishStatus;
|
||||||
import tech.easyflow.ai.publish.BotPublishAppService;
|
import tech.easyflow.ai.publish.BotPublishAppService;
|
||||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
@@ -30,9 +35,11 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
import tech.easyflow.core.runtime.ChatChannel;
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
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;
|
||||||
@@ -73,9 +80,13 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Resource
|
@Resource
|
||||||
private BotPublishAppService botPublishAppService;
|
private BotPublishAppService botPublishAppService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private ChatRoundOperateService chatRoundOperateService;
|
||||||
|
@Resource
|
||||||
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
private AiResourceApprovalStateService aiResourceApprovalStateService;
|
||||||
@Resource
|
@Resource
|
||||||
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
private AiResourceCreatorNameSupport aiResourceCreatorNameSupport;
|
||||||
|
@Resource
|
||||||
|
private ChatWorkspaceService chatWorkspaceService;
|
||||||
|
|
||||||
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public BotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
BotDocumentCollectionService botDocumentCollectionService, BotMessageService botMessageService) {
|
||||||
@@ -161,13 +172,30 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@JsonBody(value = "botId", required = true) BigInteger botId,
|
@JsonBody(value = "botId", required = true) BigInteger botId,
|
||||||
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
||||||
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
||||||
@JsonBody(value = "attachments") List<String> attachments
|
@JsonBody(value = "attachments") List<String> attachments,
|
||||||
|
@JsonBody(value = "publishedOnly") Boolean publishedOnly,
|
||||||
|
@JsonBody(value = "extraKnowledgeIds") List<BigInteger> extraKnowledgeIds,
|
||||||
|
@JsonBody(value = "regenerateRoundId") BigInteger regenerateRoundId
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
boolean usePublishedOnly = Boolean.TRUE.equals(publishedOnly);
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
|
if (usePublishedOnly) {
|
||||||
|
chatWorkspaceService.assertSessionContinuable(requireCurrentLoginAccount(), conversationId, botId);
|
||||||
|
}
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId);
|
||||||
|
}
|
||||||
|
|
||||||
// 前置校验:失败则直接返回错误SseEmitter
|
// 前置校验:失败则直接返回错误SseEmitter
|
||||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
|
SseEmitter errorEmitter = botService.checkChatBeforeStart(
|
||||||
|
botId,
|
||||||
|
prompt,
|
||||||
|
conversationId.toString(),
|
||||||
|
chatCheckResult,
|
||||||
|
usePublishedOnly,
|
||||||
|
regenerateRoundId
|
||||||
|
);
|
||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
return errorEmitter;
|
||||||
}
|
}
|
||||||
@@ -178,7 +206,7 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
messages,
|
messages,
|
||||||
chatCheckResult,
|
chatCheckResult,
|
||||||
attachments,
|
attachments,
|
||||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments)
|
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, extraKnowledgeIds, regenerateRoundId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,16 +221,24 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@GetMapping("getDetail")
|
@GetMapping("getDetail")
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
public Result<Bot> getDetail(String id) {
|
public Result<Bot> getDetail(String id) {
|
||||||
Bot bot = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
boolean publishedOnly = isPublishedOnlyRequest();
|
||||||
if (bot != null && StpUtil.isLogin()) {
|
Bot rawBot = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||||
categoryPermissionService.assertCategoryResourceVisible("BOT", bot.getCreatedBy(), bot.getCategoryId(), "无权限访问聊天助手");
|
if (rawBot != null && StpUtil.isLogin()) {
|
||||||
|
categoryPermissionService.assertCategoryResourceVisible("BOT", rawBot.getCreatedBy(), rawBot.getCategoryId(), "无权限访问聊天助手");
|
||||||
}
|
}
|
||||||
if (bot == null) {
|
if (rawBot == null) {
|
||||||
return Result.ok(null);
|
return Result.ok(null);
|
||||||
}
|
}
|
||||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(bot.getPublishStatus()).isExternallyVisible()) {
|
if (!StpUtil.isLogin() && !PublishStatus.from(rawBot.getPublishStatus()).isExternallyVisible()) {
|
||||||
throw new BusinessException("聊天助手尚未发布");
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
}
|
}
|
||||||
|
Bot bot = rawBot;
|
||||||
|
if (publishedOnly && StpUtil.isLogin()) {
|
||||||
|
if (PublishStatus.from(rawBot.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
bot = botService.toPublishedView(rawBot);
|
||||||
|
}
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
aiResourceApprovalStateService.fillBotApprovalState(bot);
|
||||||
}
|
}
|
||||||
@@ -212,17 +248,25 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Override
|
@Override
|
||||||
@SaIgnore
|
@SaIgnore
|
||||||
public Result<Bot> detail(String id) {
|
public Result<Bot> detail(String id) {
|
||||||
Bot data = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
boolean publishedOnly = isPublishedOnlyRequest();
|
||||||
if (data == null) {
|
Bot rawData = StpUtil.isLogin() ? botService.getDetail(id) : botService.getPublishedDetail(id);
|
||||||
return Result.ok(data);
|
if (rawData == null) {
|
||||||
|
return Result.ok(rawData);
|
||||||
}
|
}
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
categoryPermissionService.assertCategoryResourceVisible("BOT", data.getCreatedBy(), data.getCategoryId(), "无权限访问聊天助手");
|
categoryPermissionService.assertCategoryResourceVisible("BOT", rawData.getCreatedBy(), rawData.getCategoryId(), "无权限访问聊天助手");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StpUtil.isLogin() && !tech.easyflow.ai.enums.PublishStatus.from(data.getPublishStatus()).isExternallyVisible()) {
|
if (!StpUtil.isLogin() && !PublishStatus.from(rawData.getPublishStatus()).isExternallyVisible()) {
|
||||||
throw new BusinessException("聊天助手尚未发布");
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
}
|
}
|
||||||
|
Bot data = rawData;
|
||||||
|
if (publishedOnly && StpUtil.isLogin()) {
|
||||||
|
if (PublishStatus.from(rawData.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
throw new BusinessException("聊天助手尚未发布");
|
||||||
|
}
|
||||||
|
data = botService.toPublishedView(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, Object> llmOptions = data.getModelOptions();
|
Map<String, Object> llmOptions = data.getModelOptions();
|
||||||
if (llmOptions == null) {
|
if (llmOptions == null) {
|
||||||
@@ -297,8 +341,12 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
public Result<List<Bot>> list(Bot entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
applyCategoryPermission(queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
List<Bot> bots = service.list(queryWrapper);
|
List<Bot> bots = service.list(queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
bots = bots.stream().map(botService::toPublishedView).toList();
|
||||||
|
}
|
||||||
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
aiResourceApprovalStateService.fillBotApprovalState(bots);
|
||||||
return Result.ok(bots);
|
return Result.ok(bots);
|
||||||
}
|
}
|
||||||
@@ -306,7 +354,11 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Override
|
@Override
|
||||||
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
protected Page<Bot> queryPage(Page<Bot> page, QueryWrapper queryWrapper) {
|
||||||
applyCategoryPermission(queryWrapper);
|
applyCategoryPermission(queryWrapper);
|
||||||
|
applyPublishedOnlyFilter(queryWrapper);
|
||||||
Page<Bot> result = super.queryPage(page, queryWrapper);
|
Page<Bot> result = super.queryPage(page, queryWrapper);
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
result.setRecords(result.getRecords().stream().map(botService::toPublishedView).toList());
|
||||||
|
}
|
||||||
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
aiResourceApprovalStateService.fillBotApprovalState(result.getRecords());
|
||||||
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
aiResourceCreatorNameSupport.fillBotCreatorNames(result.getRecords());
|
||||||
return result;
|
return result;
|
||||||
@@ -406,24 +458,55 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments,
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
List<BigInteger> extraKnowledgeIds,
|
||||||
|
BigInteger regenerateRoundId) {
|
||||||
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
context.setChannel(ChatChannel.ADMIN);
|
context.setChannel(ChatChannel.ADMIN);
|
||||||
context.setSessionId(conversationId);
|
context.setSessionId(conversationId);
|
||||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
context.setTenantId(account.getTenantId());
|
||||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
context.setDeptId(account.getDeptId());
|
||||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
context.setUserId(account.getId());
|
||||||
context.setUserAccount(account == null ? "admin" : account.getLoginName());
|
context.setUserAccount(account.getLoginName());
|
||||||
context.setUserName(account == null ? "管理员" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
context.setAttachments(attachments);
|
context.setAttachments(attachments);
|
||||||
|
if (extraKnowledgeIds != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS, extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, regenerateRoundId);
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyPublishedOnlyFilter(QueryWrapper queryWrapper) {
|
||||||
|
if (isPublishedOnlyRequest()) {
|
||||||
|
queryWrapper.eq("publish_status", PublishStatus.PUBLISHED.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPublishedOnlyRequest() {
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(attributes.getRequest().getParameter("publishedOnly"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||||
|
|||||||
@@ -2,17 +2,23 @@ package tech.easyflow.admin.controller.ai;
|
|||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
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.chatlog.domain.dto.ChatHistoryPage;
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
|
||||||
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/chatHistory")
|
@RequestMapping("/api/v1/chatHistory")
|
||||||
@@ -38,4 +44,18 @@ public class ChatHistoryController {
|
|||||||
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query));
|
return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
return Result.ok(chatHistoryManageService.listAdminRoundVariants(sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.selectAdminRoundVariant(sessionId, roundId, variantIndex, account.getId()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.service.ai.ChatWorkspaceService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台控制器。
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/chatWorkspace")
|
||||||
|
public class ChatWorkspaceController {
|
||||||
|
|
||||||
|
private final ChatWorkspaceService chatWorkspaceService;
|
||||||
|
|
||||||
|
public ChatWorkspaceController(ChatWorkspaceService chatWorkspaceService) {
|
||||||
|
this.chatWorkspaceService = chatWorkspaceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions")
|
||||||
|
public Result<ChatWorkspaceSessionPage> listSessions(BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
return Result.ok(chatWorkspaceService.queryCurrentUserSessions(currentAccount(), assistantId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}")
|
||||||
|
public Result<ChatWorkspaceSessionDetailView> getSession(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(chatWorkspaceService.getCurrentUserSession(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/messages")
|
||||||
|
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
return Result.ok(chatWorkspaceService.queryCurrentUserMessages(currentAccount(), sessionId, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/conversation")
|
||||||
|
public Result<ChatWorkspaceConversationView> getConversation(@PathVariable BigInteger sessionId) {
|
||||||
|
return Result.ok(chatWorkspaceService.getCurrentUserConversation(currentAccount(), sessionId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rename")
|
||||||
|
public Result<Void> renameSession(@PathVariable BigInteger sessionId,
|
||||||
|
@JsonBody(value = "title", required = true) String title) {
|
||||||
|
chatWorkspaceService.renameCurrentUserSession(currentAccount(), sessionId, title);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/delete")
|
||||||
|
public Result<Void> deleteSession(@PathVariable BigInteger sessionId) {
|
||||||
|
chatWorkspaceService.deleteCurrentUserSession(currentAccount(), sessionId);
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
return Result.ok(chatWorkspaceService.listCurrentUserRoundVariants(currentAccount(), sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
return Result.ok(chatWorkspaceService.selectCurrentUserRoundVariant(currentAccount(), sessionId, roundId, variantIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoginAccount currentAccount() {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import tech.easyflow.ai.entity.Model;
|
|||||||
import tech.easyflow.ai.service.DocumentChunkService;
|
import tech.easyflow.ai.service.DocumentChunkService;
|
||||||
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.ai.support.DocumentStoreLifecycleSupport;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
@@ -93,22 +94,26 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
// 设置向量模型
|
try {
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
// 设置向量模型
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
|
Document document = Document.of(documentChunk.getContent());
|
||||||
|
document.setId(documentChunk.getId());
|
||||||
|
Map<String, Object> metadata = new HashMap<>();
|
||||||
|
metadata.put("keywords", documentChunk.getMetadataKeyWords());
|
||||||
|
metadata.put("questions", documentChunk.getMetadataQuestions());
|
||||||
|
document.setMetadataMap(metadata);
|
||||||
|
StoreResult result = documentStore.update(document, options); // 更新已有记录
|
||||||
|
return Result.ok(result);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
|
||||||
documentStore.setEmbeddingModel(embeddingModel);
|
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
|
||||||
Document document = Document.of(documentChunk.getContent());
|
|
||||||
document.setId(documentChunk.getId());
|
|
||||||
Map<String, Object> metadata = new HashMap<>();
|
|
||||||
metadata.put("keywords", documentChunk.getMetadataKeyWords());
|
|
||||||
metadata.put("questions", documentChunk.getMetadataQuestions());
|
|
||||||
document.setMetadataMap(metadata);
|
|
||||||
StoreResult result = documentStore.update(document, options); // 更新已有记录
|
|
||||||
return Result.ok(result);
|
|
||||||
}
|
}
|
||||||
return Result.ok(false);
|
return Result.ok(false);
|
||||||
}
|
}
|
||||||
@@ -135,19 +140,23 @@ public class DocumentChunkController extends BaseCurdController<DocumentChunkSer
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(3, "知识库没有配置向量库");
|
return Result.fail(3, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
// 设置向量模型
|
try {
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
// 设置向量模型
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(4, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
}
|
return Result.fail(4, "知识库没有配置向量模型");
|
||||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
}
|
||||||
documentStore.setEmbeddingModel(embeddingModel);
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
List<BigInteger> deleteList = new ArrayList<>();
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
deleteList.add(chunkId);
|
List<BigInteger> deleteList = new ArrayList<>();
|
||||||
documentStore.delete(deleteList, options);
|
deleteList.add(chunkId);
|
||||||
documentChunkService.removeChunk(knowledge, chunkId);
|
documentStore.delete(deleteList, options);
|
||||||
|
documentChunkService.removeChunk(knowledge, chunkId);
|
||||||
|
|
||||||
return super.remove(chunkId);
|
return super.remove(chunkId);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,6 +407,8 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
|||||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
item.setSorting(index + 1);
|
item.setSorting(index + 1);
|
||||||
item.setContent(document.getContent());
|
item.setContent(document.getContent());
|
||||||
|
item.setRenderMarkdown(readMetadataAsString(document, "renderMarkdown"));
|
||||||
|
item.setSourceFileName(readMetadataAsString(document, "sourceFileName"));
|
||||||
item.setScore(roundScore(document.getScore()));
|
item.setScore(roundScore(document.getScore()));
|
||||||
item.setHitSource(readMetadataAsString(document, RagRetrievalMetadataKeys.HIT_SOURCE));
|
item.setHitSource(readMetadataAsString(document, RagRetrievalMetadataKeys.HIT_SOURCE));
|
||||||
item.setVectorScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.VECTOR_SCORE)));
|
item.setVectorScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.VECTOR_SCORE)));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
|
import com.easyagents.mcp.client.McpEnvironmentCheckResult;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -64,6 +65,11 @@ public class McpController extends BaseCurdController<McpService, Mcp> {
|
|||||||
return Result.ok(service.getMcpTools(id));
|
return Result.ok(service.getMcpTools(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/check")
|
||||||
|
public Result<McpEnvironmentCheckResult> check(@JsonBody("configJson") String configJson) {
|
||||||
|
return Result.ok(service.checkMcp(configJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("pageTools")
|
@GetMapping("pageTools")
|
||||||
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
public Result<Page<Mcp>> pageTools(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package tech.easyflow.admin.controller.ai;
|
package tech.easyflow.admin.controller.ai;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
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.chatlog.domain.dto.ChatMessageRecord;
|
||||||
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
||||||
import tech.easyflow.chatlog.service.PublicChatSessionRestoreService;
|
import tech.easyflow.chatlog.service.PublicChatSessionRestoreService;
|
||||||
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.jsonbody.JsonBody;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/public-chat")
|
@RequestMapping("/api/v1/public-chat")
|
||||||
@@ -35,4 +40,24 @@ public class PublicChatSessionController {
|
|||||||
);
|
);
|
||||||
return Result.ok(result);
|
return Result.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/session/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
BigInteger botId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
BigInteger userId = account == null ? null : account.getId();
|
||||||
|
return Result.ok(publicChatSessionRestoreService.listVariants(userId, botId, sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/session/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
BigInteger botId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
BigInteger userId = account == null ? null : account.getId();
|
||||||
|
BigInteger operatorId = account == null ? BigInteger.ZERO : account.getId();
|
||||||
|
return Result.ok(publicChatSessionRestoreService.selectVariant(userId, botId, sessionId, roundId, variantIndex, operatorId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import tech.easyflow.ai.service.KnowledgeEmbeddingService;
|
|||||||
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
||||||
import tech.easyflow.ai.service.KnowledgeShareService;
|
import tech.easyflow.ai.service.KnowledgeShareService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
|
||||||
import tech.easyflow.ai.vo.FaqImportResultVo;
|
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||||
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
import tech.easyflow.ai.vo.KnowledgeShareAuthContext;
|
||||||
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
import tech.easyflow.ai.vo.KnowledgeShareViewDetail;
|
||||||
@@ -520,19 +521,23 @@ public class ShareKnowledgeController {
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
try {
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||||
|
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||||
|
doc.setId(documentChunk.getId());
|
||||||
|
StoreResult result = documentStore.update(doc, options);
|
||||||
|
audit(context, "更新分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", documentChunk.getId()));
|
||||||
|
return Result.ok(result);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
|
||||||
documentStore.setEmbeddingModel(embeddingModel);
|
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
|
||||||
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
|
||||||
doc.setId(documentChunk.getId());
|
|
||||||
StoreResult result = documentStore.update(doc, options);
|
|
||||||
audit(context, "更新分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
|
||||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", documentChunk.getId()));
|
|
||||||
return Result.ok(result);
|
|
||||||
}
|
}
|
||||||
return Result.ok(false);
|
return Result.ok(false);
|
||||||
}
|
}
|
||||||
@@ -559,17 +564,21 @@ public class ShareKnowledgeController {
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
try {
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(context.getKnowledge().getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
||||||
|
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||||
|
documentChunkService.removeById(chunkId);
|
||||||
|
audit(context, "删除分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
||||||
|
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", chunkId));
|
||||||
|
return Result.ok(true);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(context.getKnowledge().getVectorStoreCollection());
|
|
||||||
documentStore.delete(Collections.singletonList(chunkId), options);
|
|
||||||
documentChunkService.removeById(chunkId);
|
|
||||||
audit(context, "删除分享文档 Chunk", "KNOWLEDGE_SHARE_URL_WRITE", true,
|
|
||||||
auditDetail("knowledgeId", context.getKnowledge().getId(), "chunkId", chunkId));
|
|
||||||
return Result.ok(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -932,6 +941,10 @@ public class ShareKnowledgeController {
|
|||||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
item.setSorting(index + 1);
|
item.setSorting(index + 1);
|
||||||
item.setContent(document.getContent());
|
item.setContent(document.getContent());
|
||||||
|
Object renderMarkdown = document.getMetadata("renderMarkdown");
|
||||||
|
item.setRenderMarkdown(renderMarkdown == null ? null : String.valueOf(renderMarkdown));
|
||||||
|
Object sourceFileName = document.getMetadata("sourceFileName");
|
||||||
|
item.setSourceFileName(sourceFileName == null ? null : String.valueOf(sourceFileName));
|
||||||
item.setScore(document.getScore() == null ? null : document.getScore().doubleValue());
|
item.setScore(document.getScore() == null ? null : document.getScore().doubleValue());
|
||||||
Object hitSource = document.getMetadata("hitSource");
|
Object hitSource = document.getMetadata("hitSource");
|
||||||
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import cn.dev33.satoken.stp.StpUtil;
|
|||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.IdUtil;
|
||||||
import com.mybatisflex.core.paginate.Page;
|
import com.mybatisflex.core.paginate.Page;
|
||||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
@@ -25,6 +22,7 @@ import tech.easyflow.ai.easyagentsflow.service.CodeEngineCapabilityService;
|
|||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
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;
|
||||||
@@ -77,8 +75,6 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private CodeEngineCapabilityService codeEngineCapabilityService;
|
private CodeEngineCapabilityService codeEngineCapabilityService;
|
||||||
@@ -87,6 +83,8 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
@Resource
|
@Resource
|
||||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
private ResourceAccessService resourceAccessService;
|
private ResourceAccessService resourceAccessService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
@@ -126,6 +124,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -155,6 +154,7 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
throw new RuntimeException("工作流不存在");
|
throw new RuntimeException("工作流不存在");
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -245,17 +245,10 @@ public class WorkflowController extends BaseCurdController<WorkflowService, Work
|
|||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
return Result.fail(2, "节点配置错误,请检查! ");
|
||||||
}
|
}
|
||||||
List<Parameter> chainParameters = definition.getStartParameters();
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
|
||||||
res.put("parameters", chainParameters);
|
|
||||||
res.put("title", workflow.getTitle());
|
|
||||||
res.put("description", workflow.getDescription());
|
|
||||||
res.put("icon", workflow.getIcon());
|
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package tech.easyflow.admin.controller.ai.support;
|
package tech.easyflow.admin.controller.ai.support;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
import tech.easyflow.ai.entity.Bot;
|
import tech.easyflow.ai.entity.Bot;
|
||||||
import tech.easyflow.ai.entity.DocumentCollection;
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
import tech.easyflow.ai.entity.Plugin;
|
import tech.easyflow.ai.entity.Plugin;
|
||||||
@@ -66,6 +67,15 @@ public class AiResourceCreatorNameSupport {
|
|||||||
fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName);
|
fillCreatorNames(plugins, Plugin::getCreatedBy, Plugin::setCreatedByName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量填充 Agent 创建人名称。
|
||||||
|
*
|
||||||
|
* @param agents Agent 集合
|
||||||
|
*/
|
||||||
|
public void fillAgentCreatorNames(Collection<Agent> agents) {
|
||||||
|
fillCreatorNames(agents, Agent::getCreatedBy, Agent::setCreatedByName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用的创建人名称填充逻辑。
|
* 通用的创建人名称填充逻辑。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
package tech.easyflow.admin.controller.dashboard;
|
package tech.easyflow.admin.controller.dashboard;
|
||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
import tech.easyflow.admin.service.dashboard.DashboardService;
|
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理端工作台统计接口。
|
* 管理端工作台统计接口。
|
||||||
*/
|
*/
|
||||||
@@ -28,4 +38,26 @@ public class DashboardController {
|
|||||||
public Result<DashboardOverviewVo> overview(DashboardOverviewQuery query) {
|
public Result<DashboardOverviewVo> overview(DashboardOverviewQuery query) {
|
||||||
return Result.ok(dashboardService.getOverview(SaTokenUtil.getLoginAccount(), query));
|
return Result.ok(dashboardService.getOverview(SaTokenUtil.getLoginAccount(), query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-ranks")
|
||||||
|
@SaCheckPermission("/api/v1/dashboard/query")
|
||||||
|
public Result<List<DashboardUserRankItemVo>> userRanks(DashboardUserRankQuery query) {
|
||||||
|
return Result.ok(dashboardService.getUserRanks(SaTokenUtil.getLoginAccount(), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping(value = "/user-ranks/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
@SaCheckPermission("/api/v1/dashboard/query")
|
||||||
|
public void exportUserRanks(DashboardUserRankQuery query, HttpServletResponse response) throws Exception {
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||||
|
String fileName = URLEncoder.encode("dashboard_user_ranks_" + timestamp, "UTF-8")
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
|
||||||
|
dashboardService.exportUserRanks(
|
||||||
|
SaTokenUtil.getLoginAccount(),
|
||||||
|
query,
|
||||||
|
response.getOutputStream()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
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.ai.service.KnowledgeSharePermissionService;
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
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;
|
||||||
@@ -46,6 +47,8 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
private SysApiKeyResourceMappingService sysApiKeyResourceMappingService;
|
||||||
@Resource
|
@Resource
|
||||||
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
private KnowledgeSharePermissionService knowledgeSharePermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
/**
|
/**
|
||||||
* 添加(保存)数据
|
* 添加(保存)数据
|
||||||
*
|
*
|
||||||
@@ -88,6 +91,9 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
if (entity.getKnowledgeShareEnabled() != null) {
|
if (entity.getKnowledgeShareEnabled() != null) {
|
||||||
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
knowledgeSharePermissionService.replaceApiShareEnabled(entity.getId(), entity.getKnowledgeShareEnabled());
|
||||||
}
|
}
|
||||||
|
if (entity.getWorkflowApiEnabled() != null) {
|
||||||
|
workflowApiPermissionService.replaceWorkflowApiEnabled(entity.getId(), entity.getWorkflowApiEnabled());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -129,5 +135,11 @@ public class SysApiKeyController extends BaseCurdController<SysApiKeyService, Sy
|
|||||||
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
.eq(SysApiKeyResourceMapping::getResourceType, "KNOWLEDGE");
|
||||||
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
entity.setKnowledgeShareEnabled(sysApiKeyResourceMappingService.count(knowledgeWrapper) > 0);
|
||||||
|
|
||||||
|
QueryWrapper workflowWrapper = QueryWrapper.create()
|
||||||
|
.select(SysApiKeyResourceMapping::getId)
|
||||||
|
.eq(SysApiKeyResourceMapping::getApiKeyId, entity.getId())
|
||||||
|
.eq(SysApiKeyResourceMapping::getResourceType, WorkflowApiPermissionService.RESOURCE_TYPE_WORKFLOW);
|
||||||
|
entity.setWorkflowApiEnabled(sysApiKeyResourceMappingService.count(workflowWrapper) > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package tech.easyflow.admin.controller.system;
|
package tech.easyflow.admin.controller.system;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import tech.easyflow.system.entity.SysApiKeyResource;
|
import tech.easyflow.system.entity.SysApiKeyResource;
|
||||||
import tech.easyflow.system.service.SysApiKeyResourceService;
|
import tech.easyflow.system.service.SysApiKeyResourceService;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 请求接口表 控制层。
|
* 请求接口表 控制层。
|
||||||
*
|
*
|
||||||
@@ -20,4 +25,26 @@ public class SysApiKeyResourceController extends BaseCurdController<SysApiKeyRes
|
|||||||
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
public SysApiKeyResourceController(SysApiKeyResourceService service) {
|
||||||
super(service);
|
super(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询普通 API Key 接口授权资源。
|
||||||
|
*
|
||||||
|
* <p>工作流 Public API 使用独立的全局授权开关,不进入普通接口授权列表,避免用户误以为勾选
|
||||||
|
* 具体接口资源即可完成工作流调用授权。</p>
|
||||||
|
*
|
||||||
|
* @param entity 查询条件
|
||||||
|
* @param asTree 是否树形返回
|
||||||
|
* @param sortKey 排序字段
|
||||||
|
* @param sortType 排序方向
|
||||||
|
* @return 普通接口授权资源
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@GetMapping("list")
|
||||||
|
public Result<List<SysApiKeyResource>> list(SysApiKeyResource entity, Boolean asTree, String sortKey, String sortType) {
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity));
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/workflow/");
|
||||||
|
queryWrapper.notLike(SysApiKeyResource::getRequestInterface, "/public-api/knowledge-share/");
|
||||||
|
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||||
|
return Result.ok(service.list(queryWrapper));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台助手展示快照。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceAssistantView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
private String alias;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台完整会话视图。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceConversationView implements Serializable {
|
||||||
|
|
||||||
|
private long total;
|
||||||
|
private List<ChatMessageRecord> records = new ArrayList<>();
|
||||||
|
private Map<String, List<ChatMessageRecord>> variantsByRound = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前主线可见消息数量。
|
||||||
|
*
|
||||||
|
* @return 主线消息数量
|
||||||
|
*/
|
||||||
|
public long getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前主线可见消息数量。
|
||||||
|
*
|
||||||
|
* @param total 主线消息数量
|
||||||
|
*/
|
||||||
|
public void setTotal(long total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前主线可见消息。
|
||||||
|
*
|
||||||
|
* @return 当前主线可见消息
|
||||||
|
*/
|
||||||
|
public List<ChatMessageRecord> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前主线可见消息。
|
||||||
|
*
|
||||||
|
* @param records 当前主线可见消息
|
||||||
|
*/
|
||||||
|
public void setRecords(List<ChatMessageRecord> records) {
|
||||||
|
this.records = records == null ? new ArrayList<>() : records;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取按轮次分组的全部答案版本。
|
||||||
|
*
|
||||||
|
* @return roundId 到答案版本列表的映射
|
||||||
|
*/
|
||||||
|
public Map<String, List<ChatMessageRecord>> getVariantsByRound() {
|
||||||
|
return variantsByRound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置按轮次分组的全部答案版本。
|
||||||
|
*
|
||||||
|
* @param variantsByRound roundId 到答案版本列表的映射
|
||||||
|
*/
|
||||||
|
public void setVariantsByRound(Map<String, List<ChatMessageRecord>> variantsByRound) {
|
||||||
|
this.variantsByRound = variantsByRound == null ? new LinkedHashMap<>() : variantsByRound;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台知识库展示对象。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceKnowledgeView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger id;
|
||||||
|
private String alias;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private String icon;
|
||||||
|
|
||||||
|
public BigInteger getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(BigInteger id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIcon() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIcon(String icon) {
|
||||||
|
this.icon = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台只读原因。
|
||||||
|
*/
|
||||||
|
public enum ChatWorkspaceReadOnlyReason {
|
||||||
|
ASSISTANT_OFFLINE,
|
||||||
|
ASSISTANT_DELETED,
|
||||||
|
NO_PERMISSION
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话详情。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionDetailView extends ChatWorkspaceSessionView implements Serializable {
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView assistant;
|
||||||
|
private List<ChatWorkspaceKnowledgeView> boundKnowledges = new ArrayList<>();
|
||||||
|
private List<ChatWorkspaceKnowledgeView> extraKnowledges = new ArrayList<>();
|
||||||
|
private List<String> removedExtraKnowledgeNames = new ArrayList<>();
|
||||||
|
|
||||||
|
public ChatWorkspaceAssistantView getAssistant() {
|
||||||
|
return assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistant(ChatWorkspaceAssistantView assistant) {
|
||||||
|
this.assistant = assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceKnowledgeView> getBoundKnowledges() {
|
||||||
|
return boundKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBoundKnowledges(List<ChatWorkspaceKnowledgeView> boundKnowledges) {
|
||||||
|
this.boundKnowledges = boundKnowledges == null ? new ArrayList<>() : boundKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceKnowledgeView> getExtraKnowledges() {
|
||||||
|
return extraKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExtraKnowledges(List<ChatWorkspaceKnowledgeView> extraKnowledges) {
|
||||||
|
this.extraKnowledges = extraKnowledges == null ? new ArrayList<>() : extraKnowledges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRemovedExtraKnowledgeNames() {
|
||||||
|
return removedExtraKnowledgeNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemovedExtraKnowledgeNames(List<String> removedExtraKnowledgeNames) {
|
||||||
|
this.removedExtraKnowledgeNames = removedExtraKnowledgeNames == null ? new ArrayList<>() : removedExtraKnowledgeNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话分页结果。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionPage implements Serializable {
|
||||||
|
|
||||||
|
private Long total;
|
||||||
|
private Long pageNumber;
|
||||||
|
private Long pageSize;
|
||||||
|
private List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
|
||||||
|
public Long getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(Long total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPageNumber() {
|
||||||
|
return pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageNumber(Long pageNumber) {
|
||||||
|
this.pageNumber = pageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPageSize() {
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPageSize(Long pageSize) {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatWorkspaceSessionView> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecords(List<ChatWorkspaceSessionView> records) {
|
||||||
|
this.records = records == null ? new ArrayList<>() : records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package tech.easyflow.admin.dto.chatworkspace;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台会话摘要。
|
||||||
|
*/
|
||||||
|
public class ChatWorkspaceSessionView implements Serializable {
|
||||||
|
|
||||||
|
private BigInteger sessionId;
|
||||||
|
private BigInteger assistantId;
|
||||||
|
private String assistantCode;
|
||||||
|
private String assistantName;
|
||||||
|
private String title;
|
||||||
|
private String lastMessagePreview;
|
||||||
|
private Integer messageCount;
|
||||||
|
private Date accessAt;
|
||||||
|
private Date lastMessageAt;
|
||||||
|
private Boolean continuable;
|
||||||
|
private ChatWorkspaceReadOnlyReason readOnlyReason;
|
||||||
|
|
||||||
|
public BigInteger getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionId(BigInteger sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAssistantCode() {
|
||||||
|
return assistantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantCode(String assistantCode) {
|
||||||
|
this.assistantCode = assistantCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAssistantName() {
|
||||||
|
return assistantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantName(String assistantName) {
|
||||||
|
this.assistantName = assistantName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastMessagePreview() {
|
||||||
|
return lastMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastMessagePreview(String lastMessagePreview) {
|
||||||
|
this.lastMessagePreview = lastMessagePreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMessageCount() {
|
||||||
|
return messageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageCount(Integer messageCount) {
|
||||||
|
this.messageCount = messageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getAccessAt() {
|
||||||
|
return accessAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessAt(Date accessAt) {
|
||||||
|
this.accessAt = accessAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getLastMessageAt() {
|
||||||
|
return lastMessageAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastMessageAt(Date lastMessageAt) {
|
||||||
|
this.lastMessageAt = lastMessageAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getContinuable() {
|
||||||
|
return continuable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContinuable(Boolean continuable) {
|
||||||
|
this.continuable = continuable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatWorkspaceReadOnlyReason getReadOnlyReason() {
|
||||||
|
return readOnlyReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadOnlyReason(ChatWorkspaceReadOnlyReason readOnlyReason) {
|
||||||
|
this.readOnlyReason = readOnlyReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台智能体趋势点位。
|
||||||
|
*/
|
||||||
|
public class DashboardAssistantTrendPointVo {
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
public String getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKey(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台智能体趋势序列。
|
||||||
|
*/
|
||||||
|
public class DashboardAssistantTrendSeriesVo {
|
||||||
|
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
private Long totalSessionCount;
|
||||||
|
|
||||||
|
private List<DashboardAssistantTrendPointVo> points;
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTotalSessionCount() {
|
||||||
|
return totalSessionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalSessionCount(Long totalSessionCount) {
|
||||||
|
this.totalSessionCount = totalSessionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DashboardAssistantTrendPointVo> getPoints() {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoints(List<DashboardAssistantTrendPointVo> points) {
|
||||||
|
this.points = points;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天统计可用状态。
|
||||||
|
*/
|
||||||
|
public class DashboardChatStatusVo {
|
||||||
|
|
||||||
|
private Boolean available;
|
||||||
|
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public Boolean getAvailable() {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvailable(Boolean available) {
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package tech.easyflow.admin.model.dashboard;
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作台分布/排行项。
|
* 工作台分布/排行项。
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +23,16 @@ public class DashboardDistributionItemVo {
|
|||||||
|
|
||||||
private Long knowledgeBaseTotal;
|
private Long knowledgeBaseTotal;
|
||||||
|
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
private Long messageTotal;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
private Double avgSessionPerUser;
|
||||||
|
|
||||||
|
private Double avgMessagePerSession;
|
||||||
|
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -84,4 +96,44 @@ public class DashboardDistributionItemVo {
|
|||||||
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||||
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMessageTotal() {
|
||||||
|
return messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageTotal(Long messageTotal) {
|
||||||
|
this.messageTotal = messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgSessionPerUser() {
|
||||||
|
return avgSessionPerUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvgSessionPerUser(Double avgSessionPerUser) {
|
||||||
|
this.avgSessionPerUser = avgSessionPerUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Double getAvgMessagePerSession() {
|
||||||
|
return avgMessagePerSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvgMessagePerSession(Double avgMessagePerSession) {
|
||||||
|
this.avgMessagePerSession = avgMessagePerSession;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ package tech.easyflow.admin.model.dashboard;
|
|||||||
public class DashboardOverviewQuery {
|
public class DashboardOverviewQuery {
|
||||||
|
|
||||||
private String range;
|
private String range;
|
||||||
|
private String startDate;
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
public String getRange() {
|
public String getRange() {
|
||||||
return range;
|
return range;
|
||||||
@@ -14,4 +16,20 @@ public class DashboardOverviewQuery {
|
|||||||
public void setRange(String range) {
|
public void setRange(String range) {
|
||||||
this.range = range;
|
this.range = range;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartDate(String startDate) {
|
||||||
|
this.startDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndDate() {
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndDate(String endDate) {
|
||||||
|
this.endDate = endDate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ public class DashboardOverviewVo {
|
|||||||
|
|
||||||
private DashboardSummaryVo summary;
|
private DashboardSummaryVo summary;
|
||||||
|
|
||||||
|
private DashboardChatStatusVo chatStatus;
|
||||||
|
|
||||||
private List<DashboardTrendItemVo> trends;
|
private List<DashboardTrendItemVo> trends;
|
||||||
|
|
||||||
|
private List<DashboardAssistantTrendSeriesVo> assistantTrends;
|
||||||
|
|
||||||
private List<DashboardDistributionItemVo> distribution;
|
private List<DashboardDistributionItemVo> distribution;
|
||||||
|
|
||||||
private DashboardOverviewQuery query;
|
private DashboardOverviewQuery query;
|
||||||
@@ -34,6 +38,14 @@ public class DashboardOverviewVo {
|
|||||||
this.trends = trends;
|
this.trends = trends;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DashboardChatStatusVo getChatStatus() {
|
||||||
|
return chatStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatStatus(DashboardChatStatusVo chatStatus) {
|
||||||
|
this.chatStatus = chatStatus;
|
||||||
|
}
|
||||||
|
|
||||||
public List<DashboardDistributionItemVo> getDistribution() {
|
public List<DashboardDistributionItemVo> getDistribution() {
|
||||||
return distribution;
|
return distribution;
|
||||||
}
|
}
|
||||||
@@ -42,6 +54,14 @@ public class DashboardOverviewVo {
|
|||||||
this.distribution = distribution;
|
this.distribution = distribution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<DashboardAssistantTrendSeriesVo> getAssistantTrends() {
|
||||||
|
return assistantTrends;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantTrends(List<DashboardAssistantTrendSeriesVo> assistantTrends) {
|
||||||
|
this.assistantTrends = assistantTrends;
|
||||||
|
}
|
||||||
|
|
||||||
public DashboardOverviewQuery getQuery() {
|
public DashboardOverviewQuery getQuery() {
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ public class DashboardSummaryVo {
|
|||||||
|
|
||||||
private Long knowledgeBaseTotal;
|
private Long knowledgeBaseTotal;
|
||||||
|
|
||||||
|
private Long chatMessageTotal;
|
||||||
|
|
||||||
|
private Long chatSessionTotal;
|
||||||
|
|
||||||
|
private Long activeAssistantTotal;
|
||||||
|
|
||||||
|
private Long chatActiveUserTotal;
|
||||||
|
|
||||||
public Long getUserTotal() {
|
public Long getUserTotal() {
|
||||||
return userTotal;
|
return userTotal;
|
||||||
}
|
}
|
||||||
@@ -54,4 +62,36 @@ public class DashboardSummaryVo {
|
|||||||
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
public void setKnowledgeBaseTotal(Long knowledgeBaseTotal) {
|
||||||
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
this.knowledgeBaseTotal = knowledgeBaseTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getChatMessageTotal() {
|
||||||
|
return chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatMessageTotal(Long chatMessageTotal) {
|
||||||
|
this.chatMessageTotal = chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatSessionTotal() {
|
||||||
|
return chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatSessionTotal(Long chatSessionTotal) {
|
||||||
|
this.chatSessionTotal = chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getActiveAssistantTotal() {
|
||||||
|
return activeAssistantTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveAssistantTotal(Long activeAssistantTotal) {
|
||||||
|
this.activeAssistantTotal = activeAssistantTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatActiveUserTotal() {
|
||||||
|
return chatActiveUserTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatActiveUserTotal(Long chatActiveUserTotal) {
|
||||||
|
this.chatActiveUserTotal = chatActiveUserTotal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ public class DashboardTrendItemVo {
|
|||||||
|
|
||||||
private Long activeUserTotal;
|
private Long activeUserTotal;
|
||||||
|
|
||||||
|
private Long chatMessageTotal;
|
||||||
|
|
||||||
|
private Long chatSessionTotal;
|
||||||
|
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
@@ -34,4 +38,20 @@ public class DashboardTrendItemVo {
|
|||||||
public void setActiveUserTotal(Long activeUserTotal) {
|
public void setActiveUserTotal(Long activeUserTotal) {
|
||||||
this.activeUserTotal = activeUserTotal;
|
this.activeUserTotal = activeUserTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getChatMessageTotal() {
|
||||||
|
return chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatMessageTotal(Long chatMessageTotal) {
|
||||||
|
this.chatMessageTotal = chatMessageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getChatSessionTotal() {
|
||||||
|
return chatSessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatSessionTotal(Long chatSessionTotal) {
|
||||||
|
this.chatSessionTotal = chatSessionTotal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台用户活跃排行项。
|
||||||
|
*/
|
||||||
|
public class DashboardUserRankItemVo {
|
||||||
|
|
||||||
|
private BigInteger userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最终展示名称。
|
||||||
|
*/
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录账号。
|
||||||
|
*/
|
||||||
|
private String loginName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 昵称。
|
||||||
|
*/
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
private Long sessionTotal;
|
||||||
|
|
||||||
|
private Long messageTotal;
|
||||||
|
|
||||||
|
public BigInteger getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(BigInteger userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLoginName() {
|
||||||
|
return loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginName(String loginName) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNickname() {
|
||||||
|
return nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSessionTotal() {
|
||||||
|
return sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTotal(Long sessionTotal) {
|
||||||
|
this.sessionTotal = sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMessageTotal() {
|
||||||
|
return messageTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageTotal(Long messageTotal) {
|
||||||
|
this.messageTotal = messageTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package tech.easyflow.admin.model.dashboard;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户活跃榜查询参数。
|
||||||
|
*/
|
||||||
|
public class DashboardUserRankQuery {
|
||||||
|
|
||||||
|
private String range;
|
||||||
|
private String startDate;
|
||||||
|
private String endDate;
|
||||||
|
private BigInteger assistantId;
|
||||||
|
|
||||||
|
public String getRange() {
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRange(String range) {
|
||||||
|
this.range = range;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStartDate() {
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartDate(String startDate) {
|
||||||
|
this.startDate = startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndDate() {
|
||||||
|
return endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndDate(String endDate) {
|
||||||
|
this.endDate = endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getAssistantId() {
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssistantId(BigInteger assistantId) {
|
||||||
|
this.assistantId = assistantId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
package tech.easyflow.admin.service.agent;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.*;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.runtime.AgentRuntimeStateCleanupService;
|
||||||
|
import tech.easyflow.agent.service.AgentService;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.*;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||||
|
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 管理端会话服务。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentSessionService {
|
||||||
|
|
||||||
|
private static final String ASSISTANT_CODE = "AGENT";
|
||||||
|
|
||||||
|
private final ChatSessionQueryService chatSessionQueryService;
|
||||||
|
private final ChatSessionCommandService chatSessionCommandService;
|
||||||
|
private final AgentService agentService;
|
||||||
|
private final DocumentCollectionService documentCollectionService;
|
||||||
|
private final ResourceAccessService resourceAccessService;
|
||||||
|
private final AgentRuntimeStateCleanupService agentRuntimeStateCleanupService;
|
||||||
|
private final ChatJsonSupport chatJsonSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 管理端会话服务。
|
||||||
|
*
|
||||||
|
* @param chatSessionQueryService 聊天会话查询服务
|
||||||
|
* @param chatSessionCommandService 聊天会话命令服务
|
||||||
|
* @param agentService Agent 服务
|
||||||
|
* @param documentCollectionService 知识库服务
|
||||||
|
* @param resourceAccessService 资源访问服务
|
||||||
|
* @param agentRuntimeStateCleanupService Agent 运行态清理服务
|
||||||
|
* @param chatJsonSupport 聊天 JSON 工具
|
||||||
|
*/
|
||||||
|
public AgentSessionService(ChatSessionQueryService chatSessionQueryService,
|
||||||
|
ChatSessionCommandService chatSessionCommandService,
|
||||||
|
AgentService agentService,
|
||||||
|
DocumentCollectionService documentCollectionService,
|
||||||
|
ResourceAccessService resourceAccessService,
|
||||||
|
AgentRuntimeStateCleanupService agentRuntimeStateCleanupService,
|
||||||
|
ChatJsonSupport chatJsonSupport) {
|
||||||
|
this.chatSessionQueryService = chatSessionQueryService;
|
||||||
|
this.chatSessionCommandService = chatSessionCommandService;
|
||||||
|
this.agentService = agentService;
|
||||||
|
this.documentCollectionService = documentCollectionService;
|
||||||
|
this.resourceAccessService = resourceAccessService;
|
||||||
|
this.agentRuntimeStateCleanupService = agentRuntimeStateCleanupService;
|
||||||
|
this.chatJsonSupport = chatJsonSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话分页。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param agentId Agent ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return Agent 会话分页
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionPage queryCurrentUserSessions(LoginAccount account, BigInteger agentId, ChatPageQuery query) {
|
||||||
|
ChatSessionPage page = chatSessionQueryService.pageSessions(account.getId(), agentId, ASSISTANT_CODE, query);
|
||||||
|
Map<BigInteger, AgentAvailability> availabilityMap = resolveAgentAvailability(page.getRecords());
|
||||||
|
ChatWorkspaceSessionPage result = new ChatWorkspaceSessionPage();
|
||||||
|
result.setTotal(page.getTotal());
|
||||||
|
result.setPageNumber(page.getPageNumber());
|
||||||
|
result.setPageSize(page.getPageSize());
|
||||||
|
List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
for (ChatSessionSummary summary : page.getRecords()) {
|
||||||
|
records.add(toSessionView(summary, availabilityMap.get(summary.getAssistantId())));
|
||||||
|
}
|
||||||
|
result.setRecords(records);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话详情。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return Agent 会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView getCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = requireUserAgentSession(account, sessionId);
|
||||||
|
AgentAvailability availability = resolveAgentAvailability(List.of(summary)).get(summary.getAssistantId());
|
||||||
|
ChatWorkspaceSessionDetailView detail = new ChatWorkspaceSessionDetailView();
|
||||||
|
fillSessionView(detail, summary, availability);
|
||||||
|
Agent displayAgent = availability == null ? null : availability.displayAgent();
|
||||||
|
detail.setAssistant(toAssistantView(displayAgent, summary));
|
||||||
|
detail.setBoundKnowledges(resolveBoundKnowledges(displayAgent));
|
||||||
|
ExtraKnowledgeResolution extraKnowledgeResolution = resolveExtraKnowledges(summary);
|
||||||
|
detail.setExtraKnowledges(extraKnowledgeResolution.validKnowledges());
|
||||||
|
detail.setRemovedExtraKnowledgeNames(extraKnowledgeResolution.removedNames());
|
||||||
|
if (extraKnowledgeResolution.shouldSync()) {
|
||||||
|
syncSessionExtraKnowledges(summary, extraKnowledgeResolution.validKnowledgeIds(), account.getId());
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 会话消息。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 消息分页
|
||||||
|
*/
|
||||||
|
public ChatHistoryPage queryCurrentUserMessages(LoginAccount account, BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
return chatSessionQueryService.pageMainlineMessages(sessionId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的 Agent 完整会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceConversationView getCurrentUserConversation(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
List<ChatMessageRecord> records = chatSessionQueryService.listMainlineMessages(sessionId);
|
||||||
|
ChatWorkspaceConversationView view = new ChatWorkspaceConversationView();
|
||||||
|
view.setRecords(records);
|
||||||
|
view.setTotal(records.size());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名当前用户的 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
*/
|
||||||
|
public void renameCurrentUserSession(LoginAccount account, BigInteger sessionId, String title) {
|
||||||
|
if (!StringUtils.hasText(title)) {
|
||||||
|
throw new BusinessException("标题不能为空");
|
||||||
|
}
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
chatSessionCommandService.renameSession(sessionId, account.getId(), title.trim(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前用户 Agent 会话的临时知识库。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param knowledgeIds 临时知识库 ID
|
||||||
|
* @return 更新后的会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView saveCurrentUserExtraKnowledges(LoginAccount account,
|
||||||
|
BigInteger sessionId,
|
||||||
|
List<BigInteger> knowledgeIds) {
|
||||||
|
ChatSessionSummary summary = requireUserAgentSession(account, sessionId);
|
||||||
|
ExtraKnowledgeResolution resolution = resolveVisibleKnowledgeViews(normalizeExtraKnowledgeIds(knowledgeIds));
|
||||||
|
if (!resolution.removedNames().isEmpty()) {
|
||||||
|
throw new BusinessException("所选知识库已失效或无权限使用");
|
||||||
|
}
|
||||||
|
syncSessionExtraKnowledges(summary, resolution.validKnowledgeIds(), account.getId());
|
||||||
|
return getCurrentUserSession(account, sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户的 Agent 会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录账号
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
*/
|
||||||
|
public void deleteCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserAgentSession(account, sessionId);
|
||||||
|
agentRuntimeStateCleanupService.clearChatSession(sessionId, account.getId());
|
||||||
|
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatSessionSummary requireUserAgentSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())
|
||||||
|
|| !ASSISTANT_CODE.equals(summary.getAssistantCode())) {
|
||||||
|
throw new BusinessException("Agent 会话不存在");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该 Agent 会话");
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BigInteger, AgentAvailability> resolveAgentAvailability(List<ChatSessionSummary> sessions) {
|
||||||
|
Map<BigInteger, AgentAvailability> result = new LinkedHashMap<>();
|
||||||
|
if (sessions == null || sessions.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Set<BigInteger> agentIds = new LinkedHashSet<>();
|
||||||
|
for (ChatSessionSummary session : sessions) {
|
||||||
|
if (session != null && session.getAssistantId() != null) {
|
||||||
|
agentIds.add(session.getAssistantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (agentIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<Agent> agents = agentService.list(QueryWrapper.create().in("id", agentIds));
|
||||||
|
Map<BigInteger, Agent> agentMap = new LinkedHashMap<>();
|
||||||
|
for (Agent agent : agents) {
|
||||||
|
agentMap.put(agent.getId(), agent);
|
||||||
|
}
|
||||||
|
for (BigInteger agentId : agentIds) {
|
||||||
|
Agent currentAgent = agentMap.get(agentId);
|
||||||
|
if (currentAgent == null) {
|
||||||
|
result.put(agentId, new AgentAvailability(false, ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!resourceAccessService.canAccess(CategoryResourceType.AGENT, currentAgent, ResourceAction.USE)) {
|
||||||
|
result.put(agentId, new AgentAvailability(false, ChatWorkspaceReadOnlyReason.NO_PERMISSION, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boolean online = Integer.valueOf(1).equals(currentAgent.getStatus())
|
||||||
|
&& PublishStatus.from(currentAgent.getPublishStatus()) == PublishStatus.PUBLISHED;
|
||||||
|
result.put(agentId, new AgentAvailability(
|
||||||
|
online,
|
||||||
|
online ? null : ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE,
|
||||||
|
toDisplayAgent(currentAgent)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Agent toDisplayAgent(Agent currentAgent) {
|
||||||
|
if (currentAgent.getPublishedSnapshotJson() != null && !currentAgent.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
return agentService.fromSnapshot(currentAgent.getPublishedSnapshotJson());
|
||||||
|
}
|
||||||
|
return currentAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceSessionView toSessionView(ChatSessionSummary summary, AgentAvailability availability) {
|
||||||
|
ChatWorkspaceSessionView view = new ChatWorkspaceSessionView();
|
||||||
|
fillSessionView(view, summary, availability);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillSessionView(ChatWorkspaceSessionView view, ChatSessionSummary summary, AgentAvailability availability) {
|
||||||
|
view.setSessionId(summary.getId());
|
||||||
|
view.setAssistantId(summary.getAssistantId());
|
||||||
|
view.setAssistantCode(summary.getAssistantCode());
|
||||||
|
view.setAssistantName(summary.getAssistantName());
|
||||||
|
view.setTitle(summary.getTitle());
|
||||||
|
view.setLastMessagePreview(summary.getLastMessagePreview());
|
||||||
|
view.setMessageCount(summary.getMessageCount());
|
||||||
|
view.setAccessAt(summary.getAccessAt());
|
||||||
|
view.setLastMessageAt(summary.getLastMessageAt());
|
||||||
|
view.setContinuable(availability != null && availability.continuable());
|
||||||
|
view.setReadOnlyReason(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView toAssistantView(Agent agent, ChatSessionSummary summary) {
|
||||||
|
ChatWorkspaceAssistantView view = new ChatWorkspaceAssistantView();
|
||||||
|
if (agent != null) {
|
||||||
|
view.setId(agent.getId());
|
||||||
|
view.setAlias(agent.getId() == null ? null : agent.getId().toString());
|
||||||
|
view.setTitle(agent.getName());
|
||||||
|
view.setDescription(agent.getDescription());
|
||||||
|
view.setIcon(agent.getAvatar());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
view.setId(summary == null ? null : summary.getAssistantId());
|
||||||
|
view.setAlias(summary == null ? null : summary.getAssistantCode());
|
||||||
|
view.setTitle(summary == null ? null : summary.getAssistantName());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<ChatWorkspaceKnowledgeView> resolveBoundKnowledges(Agent displayAgent) {
|
||||||
|
if (displayAgent == null || displayAgent.getKnowledgeBindings() == null || displayAgent.getKnowledgeBindings().isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> knowledgeIds = displayAgent.getKnowledgeBindings().stream()
|
||||||
|
.map(binding -> binding.getKnowledgeId())
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.toList();
|
||||||
|
if (knowledgeIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(knowledgeIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
List<ChatWorkspaceKnowledgeView> views = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
DocumentCollection collection = collectionMap.get(knowledgeId);
|
||||||
|
if (collection == null || PublishStatus.from(collection.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
views.add(toKnowledgeView(documentCollectionService.toPublishedView(collection)));
|
||||||
|
}
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceKnowledgeView toKnowledgeView(DocumentCollection collection) {
|
||||||
|
ChatWorkspaceKnowledgeView view = new ChatWorkspaceKnowledgeView();
|
||||||
|
view.setId(collection.getId());
|
||||||
|
view.setAlias(collection.getAlias());
|
||||||
|
view.setTitle(collection.getTitle());
|
||||||
|
view.setDescription(collection.getDescription());
|
||||||
|
view.setIcon(collection.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveExtraKnowledges(ChatSessionSummary summary) {
|
||||||
|
ChatSessionExtPayload payload = chatJsonSupport.fromJson(summary.getExtJson(), ChatSessionExtPayload.class);
|
||||||
|
List<BigInteger> extraKnowledgeIds = payload == null ? List.of() : payload.getExtraKnowledgeIds();
|
||||||
|
return resolveVisibleKnowledgeViews(extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveVisibleKnowledgeViews(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = normalizeExtraKnowledgeIds(knowledgeIds);
|
||||||
|
if (normalizedIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(normalizedIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
List<ChatWorkspaceKnowledgeView> validKnowledges = new ArrayList<>();
|
||||||
|
List<BigInteger> validKnowledgeIds = new ArrayList<>();
|
||||||
|
List<String> removedNames = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
for (BigInteger knowledgeId : normalizedIds) {
|
||||||
|
DocumentCollection current = collectionMap.get(knowledgeId);
|
||||||
|
if (current == null) {
|
||||||
|
removedNames.add("知识库#" + knowledgeId);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(current.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!resourceAccessService.canAccess(CategoryResourceType.KNOWLEDGE, current, ResourceAction.USE)) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validKnowledges.add(toKnowledgeView(documentCollectionService.toPublishedView(current)));
|
||||||
|
validKnowledgeIds.add(current.getId());
|
||||||
|
}
|
||||||
|
if (!Objects.equals(normalizedIds, validKnowledgeIds)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return new ExtraKnowledgeResolution(validKnowledges, validKnowledgeIds, removedNames, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BigInteger> normalizeExtraKnowledgeIds(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
if (knowledgeId != null && !normalizedIds.contains(knowledgeId)) {
|
||||||
|
normalizedIds.add(knowledgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedIds.size() > 3) {
|
||||||
|
throw new BusinessException("临时知识库最多选择 3 个");
|
||||||
|
}
|
||||||
|
return normalizedIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSessionExtraKnowledges(ChatSessionSummary summary, List<BigInteger> validKnowledgeIds, BigInteger operatorId) {
|
||||||
|
ChatSessionExtPayload payload = new ChatSessionExtPayload();
|
||||||
|
payload.setExtraKnowledgeIds(validKnowledgeIds);
|
||||||
|
ChatSessionUpsertCommand command = new ChatSessionUpsertCommand();
|
||||||
|
command.setSessionId(summary.getId());
|
||||||
|
command.setTenantId(summary.getTenantId());
|
||||||
|
command.setDeptId(summary.getDeptId());
|
||||||
|
command.setUserId(summary.getUserId());
|
||||||
|
command.setUserAccount(summary.getUserAccount());
|
||||||
|
command.setAssistantId(summary.getAssistantId());
|
||||||
|
command.setAssistantCode(summary.getAssistantCode());
|
||||||
|
command.setAssistantName(summary.getAssistantName());
|
||||||
|
command.setTitle(summary.getTitle());
|
||||||
|
command.setExtJson(chatJsonSupport.toJson(payload));
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
chatSessionCommandService.createOrTouchSession(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AgentAvailability(boolean continuable,
|
||||||
|
ChatWorkspaceReadOnlyReason reason,
|
||||||
|
Agent displayAgent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExtraKnowledgeResolution(List<ChatWorkspaceKnowledgeView> validKnowledges,
|
||||||
|
List<BigInteger> validKnowledgeIds,
|
||||||
|
List<String> removedNames,
|
||||||
|
boolean shouldSync) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
package tech.easyflow.admin.service.ai;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceAssistantView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceConversationView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceKnowledgeView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceReadOnlyReason;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionDetailView;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionPage;
|
||||||
|
import tech.easyflow.admin.dto.chatworkspace.ChatWorkspaceSessionView;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.entity.DocumentCollection;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||||
|
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionExtPayload;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionCommandService;
|
||||||
|
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||||
|
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static tech.easyflow.ai.entity.table.BotTableDef.BOT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端聊天工作台服务。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ChatWorkspaceService {
|
||||||
|
|
||||||
|
private final ChatSessionQueryService chatSessionQueryService;
|
||||||
|
private final ChatSessionCommandService chatSessionCommandService;
|
||||||
|
private final ChatRoundOperateService chatRoundOperateService;
|
||||||
|
private final BotService botService;
|
||||||
|
private final DocumentCollectionService documentCollectionService;
|
||||||
|
private final CategoryPermissionService categoryPermissionService;
|
||||||
|
private final KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper;
|
||||||
|
private final ChatJsonSupport chatJsonSupport;
|
||||||
|
|
||||||
|
public ChatWorkspaceService(ChatSessionQueryService chatSessionQueryService,
|
||||||
|
ChatSessionCommandService chatSessionCommandService,
|
||||||
|
ChatRoundOperateService chatRoundOperateService,
|
||||||
|
BotService botService,
|
||||||
|
DocumentCollectionService documentCollectionService,
|
||||||
|
CategoryPermissionService categoryPermissionService,
|
||||||
|
KnowledgeVisibilityQueryHelper knowledgeVisibilityQueryHelper,
|
||||||
|
ChatJsonSupport chatJsonSupport) {
|
||||||
|
this.chatSessionQueryService = chatSessionQueryService;
|
||||||
|
this.chatSessionCommandService = chatSessionCommandService;
|
||||||
|
this.chatRoundOperateService = chatRoundOperateService;
|
||||||
|
this.botService = botService;
|
||||||
|
this.documentCollectionService = documentCollectionService;
|
||||||
|
this.categoryPermissionService = categoryPermissionService;
|
||||||
|
this.knowledgeVisibilityQueryHelper = knowledgeVisibilityQueryHelper;
|
||||||
|
this.chatJsonSupport = chatJsonSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话分页。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param assistantId 助手过滤条件
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 工作台会话分页
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionPage queryCurrentUserSessions(LoginAccount account, BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
ChatSessionPage page = chatSessionQueryService.pageSessions(account.getId(), assistantId, query);
|
||||||
|
Map<BigInteger, AssistantAvailability> availabilityMap = resolveAssistantAvailability(account, page.getRecords());
|
||||||
|
ChatWorkspaceSessionPage result = new ChatWorkspaceSessionPage();
|
||||||
|
result.setTotal(page.getTotal());
|
||||||
|
result.setPageNumber(page.getPageNumber());
|
||||||
|
result.setPageSize(page.getPageSize());
|
||||||
|
List<ChatWorkspaceSessionView> records = new ArrayList<>();
|
||||||
|
for (ChatSessionSummary summary : page.getRecords()) {
|
||||||
|
records.add(toSessionView(summary, availabilityMap.get(summary.getAssistantId())));
|
||||||
|
}
|
||||||
|
result.setRecords(records);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话详情。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 工作台会话详情
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceSessionDetailView getCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = requireUserSession(account, sessionId);
|
||||||
|
AssistantAvailability availability = resolveAssistantAvailability(account, List.of(summary)).get(summary.getAssistantId());
|
||||||
|
ChatWorkspaceSessionDetailView detail = new ChatWorkspaceSessionDetailView();
|
||||||
|
fillSessionView(detail, summary, availability);
|
||||||
|
if (availability != null && availability.displayBot() != null) {
|
||||||
|
detail.setAssistant(toAssistantView(availability.displayBot(), summary));
|
||||||
|
detail.setBoundKnowledges(resolveBoundKnowledges(availability.displayBot()));
|
||||||
|
} else {
|
||||||
|
detail.setAssistant(toAssistantView(null, summary));
|
||||||
|
}
|
||||||
|
ExtraKnowledgeResolution extraKnowledgeResolution = resolveExtraKnowledges(summary);
|
||||||
|
detail.setExtraKnowledges(extraKnowledgeResolution.validKnowledges());
|
||||||
|
detail.setRemovedExtraKnowledgeNames(extraKnowledgeResolution.removedNames());
|
||||||
|
if (extraKnowledgeResolution.shouldSync()) {
|
||||||
|
syncSessionExtraKnowledges(summary, extraKnowledgeResolution.validKnowledgeIds(), account.getId());
|
||||||
|
}
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户会话消息。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 历史消息分页
|
||||||
|
*/
|
||||||
|
public ChatHistoryPage queryCurrentUserMessages(LoginAccount account, BigInteger sessionId, ChatPageQuery query) {
|
||||||
|
ChatSessionSummary summary = requireUserSession(account, sessionId);
|
||||||
|
ChatHistoryPage firstPage = restoreRecentMessages(summary, query);
|
||||||
|
if (firstPage != null) {
|
||||||
|
return firstPage;
|
||||||
|
}
|
||||||
|
return chatSessionQueryService.pageMainlineMessages(sessionId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户完整工作台会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @return 完整会话视图
|
||||||
|
*/
|
||||||
|
public ChatWorkspaceConversationView getCurrentUserConversation(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
List<ChatMessageRecord> records = chatSessionQueryService.listMainlineMessages(sessionId);
|
||||||
|
Map<String, List<ChatMessageRecord>> variantsByRound = new LinkedHashMap<>();
|
||||||
|
Set<BigInteger> roundIds = new LinkedHashSet<>();
|
||||||
|
for (ChatMessageRecord record : records) {
|
||||||
|
if (record == null || record.getRoundId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer variantCount = record.getVariantCount();
|
||||||
|
if (variantCount != null && variantCount > 1) {
|
||||||
|
roundIds.add(record.getRoundId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (BigInteger roundId : roundIds) {
|
||||||
|
variantsByRound.put(roundId.toString(), chatRoundOperateService.listVariants(sessionId, roundId));
|
||||||
|
}
|
||||||
|
ChatWorkspaceConversationView view = new ChatWorkspaceConversationView();
|
||||||
|
view.setRecords(records);
|
||||||
|
view.setVariantsByRound(variantsByRound);
|
||||||
|
view.setTotal(records.size());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重命名当前用户会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param title 新标题
|
||||||
|
*/
|
||||||
|
public void renameCurrentUserSession(LoginAccount account, BigInteger sessionId, String title) {
|
||||||
|
if (!StringUtils.hasText(title)) {
|
||||||
|
throw new BusinessException("标题不能为空");
|
||||||
|
}
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
chatSessionCommandService.renameSession(sessionId, account.getId(), title.trim(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除当前用户会话。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
*/
|
||||||
|
public void deleteCurrentUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
chatSessionCommandService.deleteSession(sessionId, account.getId(), account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ChatMessageRecord> listCurrentUserRoundVariants(LoginAccount account, BigInteger sessionId, BigInteger roundId) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
return chatRoundOperateService.listVariants(sessionId, roundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatMessageRecord selectCurrentUserRoundVariant(LoginAccount account, BigInteger sessionId, BigInteger roundId, Integer variantIndex) {
|
||||||
|
requireUserSession(account, sessionId);
|
||||||
|
return chatRoundOperateService.selectVariant(sessionId, roundId, variantIndex, account.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送前校验会话是否仍可继续聊天。
|
||||||
|
*
|
||||||
|
* @param account 当前登录用户
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param requestBotId 本次请求助手 ID
|
||||||
|
*/
|
||||||
|
public void assertSessionContinuable(LoginAccount account, BigInteger sessionId, BigInteger requestBotId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该会话");
|
||||||
|
}
|
||||||
|
if (requestBotId != null && summary.getAssistantId() != null && !Objects.equals(summary.getAssistantId(), requestBotId)) {
|
||||||
|
throw new BusinessException("当前会话与所选聊天助手不匹配");
|
||||||
|
}
|
||||||
|
AssistantAvailability availability = resolveAssistantAvailability(account, List.of(summary)).get(summary.getAssistantId());
|
||||||
|
if (availability == null || !availability.continuable()) {
|
||||||
|
throw new BusinessException(buildReadOnlyMessage(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatSessionSummary requireUserSession(LoginAccount account, BigInteger sessionId) {
|
||||||
|
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
|
||||||
|
if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||||
|
throw new BusinessException("会话不存在");
|
||||||
|
}
|
||||||
|
if (!Objects.equals(summary.getUserId(), account.getId())) {
|
||||||
|
throw new BusinessException("无权访问该会话");
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首屏优先从热态恢复最近消息,避免分析库延迟导致刚完成的回复不可见。
|
||||||
|
*
|
||||||
|
* @param summary 会话摘要
|
||||||
|
* @param query 分页参数
|
||||||
|
* @return 命中热态时返回恢复结果,否则返回 null 继续走历史库
|
||||||
|
*/
|
||||||
|
private ChatHistoryPage restoreRecentMessages(ChatSessionSummary summary, ChatPageQuery query) {
|
||||||
|
if (summary == null || query == null || query.getPageNumber() != 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<tech.easyflow.chatlog.domain.dto.ChatMessageRecord> records =
|
||||||
|
chatSessionQueryService.getRecentTail(summary.getId(), Math.toIntExact(query.getPageSize()));
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isRestoredTailReliable(records)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ChatHistoryPage page = new ChatHistoryPage();
|
||||||
|
page.setPageNumber(query.getPageNumber());
|
||||||
|
page.setPageSize(query.getPageSize());
|
||||||
|
page.setRecords(records);
|
||||||
|
long total = summary.getMessageCount() == null ? 0L : summary.getMessageCount();
|
||||||
|
page.setTotal(Math.max(total, records.size()));
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 Redis tail 是否仍符合当前主线版本语义。
|
||||||
|
*
|
||||||
|
* @param records Redis tail 消息
|
||||||
|
* @return true 表示可直接用于首屏恢复
|
||||||
|
*/
|
||||||
|
private boolean isRestoredTailReliable(List<ChatMessageRecord> records) {
|
||||||
|
Map<BigInteger, Integer> selectedVariantByRound = new LinkedHashMap<>();
|
||||||
|
Map<BigInteger, Set<Integer>> assistantVariantsByRound = new LinkedHashMap<>();
|
||||||
|
for (ChatMessageRecord record : records) {
|
||||||
|
if (record == null || record.getRoundId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer selectedVariantIndex = record.getSelectedVariantIndex();
|
||||||
|
if (selectedVariantIndex != null && selectedVariantIndex > 0) {
|
||||||
|
Integer previous = selectedVariantByRound.putIfAbsent(record.getRoundId(), selectedVariantIndex);
|
||||||
|
if (previous != null && !Objects.equals(previous, selectedVariantIndex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("assistant".equalsIgnoreCase(record.getSenderRole())
|
||||||
|
&& record.getVariantIndex() != null
|
||||||
|
&& record.getVariantIndex() > 0) {
|
||||||
|
assistantVariantsByRound
|
||||||
|
.computeIfAbsent(record.getRoundId(), key -> new LinkedHashSet<>())
|
||||||
|
.add(record.getVariantIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Map.Entry<BigInteger, Integer> entry : selectedVariantByRound.entrySet()) {
|
||||||
|
Set<Integer> visibleVariants = assistantVariantsByRound.get(entry.getKey());
|
||||||
|
if (visibleVariants != null && !visibleVariants.isEmpty() && !visibleVariants.contains(entry.getValue())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<BigInteger, AssistantAvailability> resolveAssistantAvailability(LoginAccount account, List<ChatSessionSummary> sessions) {
|
||||||
|
Map<BigInteger, AssistantAvailability> result = new LinkedHashMap<>();
|
||||||
|
if (sessions == null || sessions.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Set<BigInteger> assistantIds = new LinkedHashSet<>();
|
||||||
|
for (ChatSessionSummary session : sessions) {
|
||||||
|
if (session != null && session.getAssistantId() != null) {
|
||||||
|
assistantIds.add(session.getAssistantId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (assistantIds.isEmpty()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
List<Bot> bots = botService.list(QueryWrapper.create().where(BOT.ID.in(assistantIds)));
|
||||||
|
Map<BigInteger, Bot> botMap = new LinkedHashMap<>();
|
||||||
|
for (Bot bot : bots) {
|
||||||
|
botMap.put(bot.getId(), bot);
|
||||||
|
}
|
||||||
|
RoleCategoryAccessSnapshot accessSnapshot = categoryPermissionService.getAccess("BOT", account);
|
||||||
|
for (BigInteger assistantId : assistantIds) {
|
||||||
|
Bot currentBot = botMap.get(assistantId);
|
||||||
|
if (currentBot == null) {
|
||||||
|
result.put(assistantId, new AssistantAvailability(false, ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!accessSnapshot.canAccess(currentBot.getCreatedBy(), currentBot.getCategoryId())) {
|
||||||
|
result.put(assistantId, new AssistantAvailability(false, ChatWorkspaceReadOnlyReason.NO_PERMISSION, null));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Bot displayBot = botService.toPublishedView(currentBot);
|
||||||
|
boolean online = Integer.valueOf(1).equals(currentBot.getStatus())
|
||||||
|
&& PublishStatus.from(currentBot.getPublishStatus()) == PublishStatus.PUBLISHED;
|
||||||
|
result.put(assistantId, new AssistantAvailability(
|
||||||
|
online,
|
||||||
|
online ? null : ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE,
|
||||||
|
displayBot
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceSessionView toSessionView(ChatSessionSummary summary, AssistantAvailability availability) {
|
||||||
|
ChatWorkspaceSessionView view = new ChatWorkspaceSessionView();
|
||||||
|
fillSessionView(view, summary, availability);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillSessionView(ChatWorkspaceSessionView view, ChatSessionSummary summary, AssistantAvailability availability) {
|
||||||
|
view.setSessionId(summary.getId());
|
||||||
|
view.setAssistantId(summary.getAssistantId());
|
||||||
|
view.setAssistantCode(summary.getAssistantCode());
|
||||||
|
view.setAssistantName(summary.getAssistantName());
|
||||||
|
view.setTitle(summary.getTitle());
|
||||||
|
view.setLastMessagePreview(summary.getLastMessagePreview());
|
||||||
|
view.setMessageCount(summary.getMessageCount());
|
||||||
|
view.setAccessAt(summary.getAccessAt());
|
||||||
|
view.setLastMessageAt(summary.getLastMessageAt());
|
||||||
|
view.setContinuable(availability != null && availability.continuable());
|
||||||
|
view.setReadOnlyReason(availability == null ? ChatWorkspaceReadOnlyReason.ASSISTANT_DELETED : availability.reason());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceAssistantView toAssistantView(Bot bot, ChatSessionSummary summary) {
|
||||||
|
ChatWorkspaceAssistantView view = new ChatWorkspaceAssistantView();
|
||||||
|
if (bot != null) {
|
||||||
|
view.setId(bot.getId());
|
||||||
|
view.setAlias(bot.getAlias());
|
||||||
|
view.setTitle(bot.getTitle());
|
||||||
|
view.setDescription(bot.getDescription());
|
||||||
|
view.setIcon(bot.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
view.setId(summary == null ? null : summary.getAssistantId());
|
||||||
|
view.setAlias(summary == null ? null : summary.getAssistantCode());
|
||||||
|
view.setTitle(summary == null ? null : summary.getAssistantName());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChatWorkspaceKnowledgeView> resolveBoundKnowledges(Bot displayBot) {
|
||||||
|
if (displayBot == null || displayBot.getPublishedSnapshotJson() == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
Object rawBindings = displayBot.getPublishedSnapshotJson().get("knowledgeBindings");
|
||||||
|
if (!(rawBindings instanceof List<?> bindings) || bindings.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<BigInteger> knowledgeIds = new ArrayList<>();
|
||||||
|
for (Object binding : bindings) {
|
||||||
|
if (!(binding instanceof Map<?, ?> bindingMap) || bindingMap.get("knowledgeId") == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
knowledgeIds.add(new BigInteger(String.valueOf(bindingMap.get("knowledgeId"))));
|
||||||
|
}
|
||||||
|
return resolveVisibleKnowledgeViews(knowledgeIds).validKnowledges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveExtraKnowledges(ChatSessionSummary summary) {
|
||||||
|
ChatSessionExtPayload payload = chatJsonSupport.fromJson(summary.getExtJson(), ChatSessionExtPayload.class);
|
||||||
|
List<BigInteger> extraKnowledgeIds = payload == null ? List.of() : payload.getExtraKnowledgeIds();
|
||||||
|
return resolveVisibleKnowledgeViews(extraKnowledgeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExtraKnowledgeResolution resolveVisibleKnowledgeViews(List<BigInteger> knowledgeIds) {
|
||||||
|
if (knowledgeIds == null || knowledgeIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<BigInteger> normalizedIds = new ArrayList<>();
|
||||||
|
for (BigInteger knowledgeId : knowledgeIds) {
|
||||||
|
if (knowledgeId != null && !normalizedIds.contains(knowledgeId)) {
|
||||||
|
normalizedIds.add(knowledgeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalizedIds.isEmpty()) {
|
||||||
|
return new ExtraKnowledgeResolution(List.of(), List.of(), List.of(), false);
|
||||||
|
}
|
||||||
|
List<DocumentCollection> collections = documentCollectionService.listByIds(normalizedIds);
|
||||||
|
Map<BigInteger, DocumentCollection> collectionMap = new LinkedHashMap<>();
|
||||||
|
for (DocumentCollection collection : collections) {
|
||||||
|
collectionMap.put(collection.getId(), collection);
|
||||||
|
}
|
||||||
|
KnowledgeReadAccessSnapshot accessSnapshot = knowledgeVisibilityQueryHelper.getCurrentReadSnapshot();
|
||||||
|
List<ChatWorkspaceKnowledgeView> validKnowledges = new ArrayList<>();
|
||||||
|
List<BigInteger> validKnowledgeIds = new ArrayList<>();
|
||||||
|
List<String> removedNames = new ArrayList<>();
|
||||||
|
boolean changed = false;
|
||||||
|
for (BigInteger knowledgeId : normalizedIds) {
|
||||||
|
DocumentCollection current = collectionMap.get(knowledgeId);
|
||||||
|
if (current == null) {
|
||||||
|
removedNames.add("知识库#" + knowledgeId);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (PublishStatus.from(current.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!knowledgeVisibilityQueryHelper.canRead(current, accessSnapshot)) {
|
||||||
|
removedNames.add(current.getTitle());
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
validKnowledges.add(toKnowledgeView(documentCollectionService.toPublishedView(current)));
|
||||||
|
validKnowledgeIds.add(current.getId());
|
||||||
|
}
|
||||||
|
if (!Objects.equals(normalizedIds, validKnowledgeIds)) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return new ExtraKnowledgeResolution(validKnowledges, validKnowledgeIds, removedNames, changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChatWorkspaceKnowledgeView toKnowledgeView(DocumentCollection collection) {
|
||||||
|
ChatWorkspaceKnowledgeView view = new ChatWorkspaceKnowledgeView();
|
||||||
|
view.setId(collection.getId());
|
||||||
|
view.setAlias(collection.getAlias());
|
||||||
|
view.setTitle(collection.getTitle());
|
||||||
|
view.setDescription(collection.getDescription());
|
||||||
|
view.setIcon(collection.getIcon());
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSessionExtraKnowledges(ChatSessionSummary summary, List<BigInteger> validKnowledgeIds, BigInteger operatorId) {
|
||||||
|
ChatSessionExtPayload payload = new ChatSessionExtPayload();
|
||||||
|
payload.setExtraKnowledgeIds(validKnowledgeIds);
|
||||||
|
ChatSessionUpsertCommand command = new ChatSessionUpsertCommand();
|
||||||
|
command.setSessionId(summary.getId());
|
||||||
|
command.setTenantId(summary.getTenantId());
|
||||||
|
command.setDeptId(summary.getDeptId());
|
||||||
|
command.setUserId(summary.getUserId());
|
||||||
|
command.setUserAccount(summary.getUserAccount());
|
||||||
|
command.setAssistantId(summary.getAssistantId());
|
||||||
|
command.setAssistantCode(summary.getAssistantCode());
|
||||||
|
command.setAssistantName(summary.getAssistantName());
|
||||||
|
command.setTitle(summary.getTitle());
|
||||||
|
command.setExtJson(chatJsonSupport.toJson(payload));
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
command.setOperateAt(new Date());
|
||||||
|
chatSessionCommandService.createOrTouchSession(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildReadOnlyMessage(ChatWorkspaceReadOnlyReason reason) {
|
||||||
|
if (reason == ChatWorkspaceReadOnlyReason.NO_PERMISSION) {
|
||||||
|
return "当前会话对应的聊天助手已无权限访问,仅支持查看历史记录";
|
||||||
|
}
|
||||||
|
if (reason == ChatWorkspaceReadOnlyReason.ASSISTANT_OFFLINE) {
|
||||||
|
return "当前会话对应的聊天助手已下架,无法继续聊天";
|
||||||
|
}
|
||||||
|
return "当前会话对应的聊天助手已删除,无法继续聊天";
|
||||||
|
}
|
||||||
|
|
||||||
|
private record AssistantAvailability(boolean continuable,
|
||||||
|
ChatWorkspaceReadOnlyReason reason,
|
||||||
|
Bot displayBot) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExtraKnowledgeResolution(List<ChatWorkspaceKnowledgeView> validKnowledges,
|
||||||
|
List<BigInteger> validKnowledgeIds,
|
||||||
|
List<String> removedNames,
|
||||||
|
boolean shouldSync) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,42 @@ package tech.easyflow.admin.service.dashboard;
|
|||||||
|
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工作台统计服务。
|
* 工作台统计服务。
|
||||||
*/
|
*/
|
||||||
public interface DashboardService {
|
public interface DashboardService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作台总览。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 工作台总览
|
||||||
|
*/
|
||||||
DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query);
|
DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 用户活跃榜
|
||||||
|
*/
|
||||||
|
List<DashboardUserRankItemVo> getUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @param outputStream 输出流
|
||||||
|
*/
|
||||||
|
void exportUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query, OutputStream outputStream);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,45 @@
|
|||||||
package tech.easyflow.admin.service.dashboard.impl;
|
package tech.easyflow.admin.service.dashboard.impl;
|
||||||
|
|
||||||
import com.easyagents.flow.core.chain.ChainStatus;
|
import cn.idev.excel.EasyExcel;
|
||||||
|
import cn.idev.excel.write.style.column.SimpleColumnWidthStyleStrategy;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import com.mybatisflex.core.row.Db;
|
import com.mybatisflex.core.row.Db;
|
||||||
import com.mybatisflex.core.row.Row;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardChatStatusVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendPointVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||||
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
import tech.easyflow.admin.service.dashboard.DashboardService;
|
import tech.easyflow.admin.service.dashboard.DashboardService;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||||
|
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||||
import tech.easyflow.common.constant.Constants;
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
import tech.easyflow.system.entity.SysAccountRole;
|
import tech.easyflow.system.entity.SysAccountRole;
|
||||||
import tech.easyflow.system.entity.SysRole;
|
import tech.easyflow.system.entity.SysRole;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.service.SysAccountRoleService;
|
import tech.easyflow.system.service.SysAccountRoleService;
|
||||||
import tech.easyflow.system.service.SysRoleService;
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -30,6 +49,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -40,32 +60,98 @@ import java.util.stream.Collectors;
|
|||||||
@Service
|
@Service
|
||||||
public class DashboardServiceImpl implements DashboardService {
|
public class DashboardServiceImpl implements DashboardService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class);
|
||||||
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
|
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
|
||||||
|
private static final String CHAT_UNAVAILABLE_MESSAGE = "聊天数据不可用";
|
||||||
|
private static final int DEFAULT_ASSISTANT_RANK_LIMIT = 5;
|
||||||
|
private static final int DEFAULT_ASSISTANT_TREND_LIMIT = 8;
|
||||||
|
private static final int DEFAULT_USER_RANK_LIMIT = 5;
|
||||||
|
private static final int USER_RANK_EXPORT_COLUMN_WIDTH = 20;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SysAccountRoleService sysAccountRoleService;
|
private SysAccountRoleService sysAccountRoleService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SysRoleService sysRoleService;
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SysAccountService sysAccountService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ChatDashboardQueryService chatDashboardQueryService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private BotService botService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作台总览信息。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 工作台总览
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
public DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||||
DashboardQueryContext context = buildContext(loginAccount, query);
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
|
||||||
DashboardSummaryVo summary = buildSummary(context);
|
DashboardSummaryVo summary = buildSummary(context);
|
||||||
List<DashboardTrendItemVo> trends = buildTrends(context);
|
ChatDashboardPayload chatPayload = buildChatPayload(context, summary);
|
||||||
List<DashboardDistributionItemVo> distribution = buildDistribution(context, summary);
|
|
||||||
|
|
||||||
DashboardOverviewVo result = new DashboardOverviewVo();
|
DashboardOverviewVo result = new DashboardOverviewVo();
|
||||||
result.setSummary(summary);
|
result.setSummary(summary);
|
||||||
result.setTrends(trends);
|
result.setChatStatus(chatPayload.chatStatus);
|
||||||
result.setDistribution(distribution);
|
result.setTrends(chatPayload.trends);
|
||||||
|
result.setAssistantTrends(chatPayload.assistantTrends);
|
||||||
|
result.setDistribution(chatPayload.distribution);
|
||||||
|
|
||||||
DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
|
DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
|
||||||
normalizedQuery.setRange(context.range);
|
normalizedQuery.setRange(context.range);
|
||||||
|
normalizedQuery.setStartDate(context.queryStartDate);
|
||||||
|
normalizedQuery.setEndDate(context.queryEndDate);
|
||||||
result.setQuery(normalizedQuery);
|
result.setQuery(normalizedQuery);
|
||||||
result.setUpdatedAt(new Date());
|
result.setUpdatedAt(new Date());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 用户活跃榜
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<DashboardUserRankItemVo> getUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query) {
|
||||||
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
return queryUserRanks(context, DEFAULT_USER_RANK_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出用户活跃榜。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @param outputStream 输出流
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void exportUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query, OutputStream outputStream) {
|
||||||
|
DashboardQueryContext context = buildContext(loginAccount, query);
|
||||||
|
List<DashboardUserRankItemVo> userRanks = queryUserRanks(context, null);
|
||||||
|
EasyExcel.write(outputStream)
|
||||||
|
.head(buildUserRankExportHead())
|
||||||
|
.registerWriteHandler(new SimpleColumnWidthStyleStrategy(USER_RANK_EXPORT_COLUMN_WIDTH))
|
||||||
|
.sheet("用户活跃榜")
|
||||||
|
.doWrite(buildUserRankExportRows(userRanks));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建顶部汇总卡片。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 汇总结果
|
||||||
|
*/
|
||||||
private DashboardSummaryVo buildSummary(DashboardQueryContext context) {
|
private DashboardSummaryVo buildSummary(DashboardQueryContext context) {
|
||||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
|
summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
|
||||||
@@ -73,72 +159,304 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
|
summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
|
||||||
summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
|
summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
|
||||||
summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
|
summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DashboardTrendItemVo> buildTrends(DashboardQueryContext context) {
|
/**
|
||||||
List<TimeBucket> buckets = buildBuckets(context.range);
|
* 构建聊天统计载荷。
|
||||||
String bucketFormat = "today".equals(context.range) ? "%Y-%m-%d %H:00:00" : "%Y-%m-%d";
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @param summary 汇总结果
|
||||||
|
* @return 聊天统计载荷
|
||||||
|
*/
|
||||||
|
private ChatDashboardPayload buildChatPayload(DashboardQueryContext context, DashboardSummaryVo summary) {
|
||||||
|
DashboardChatStatusVo chatStatus = new DashboardChatStatusVo();
|
||||||
|
chatStatus.setAvailable(Boolean.TRUE);
|
||||||
|
chatStatus.setMessage("");
|
||||||
|
|
||||||
Map<String, Long> activeUserMap = queryActiveUserTrend(context, bucketFormat);
|
if (!chatDashboardQueryService.available()) {
|
||||||
|
chatStatus.setAvailable(Boolean.FALSE);
|
||||||
|
chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
|
return new ChatDashboardPayload(
|
||||||
|
chatStatus,
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate startDate = context.startTime.toLocalDate();
|
||||||
|
LocalDate endDate = context.endTime.toLocalDate();
|
||||||
|
try {
|
||||||
|
ChatDashboardSummary chatSummary = chatDashboardQueryService.querySummary(startDate, endDate, context.tenantFilterId);
|
||||||
|
summary.setChatMessageTotal(chatSummary.messageTotal());
|
||||||
|
summary.setChatSessionTotal(chatSummary.sessionTotal());
|
||||||
|
summary.setActiveAssistantTotal(chatSummary.activeAssistantTotal());
|
||||||
|
summary.setChatActiveUserTotal(chatSummary.chatActiveUserTotal());
|
||||||
|
|
||||||
|
List<ChatDashboardTrend> rawTrends = useHourlyBuckets(context)
|
||||||
|
? chatDashboardQueryService.queryHourlyTrends(context.startTime, context.endTime, context.tenantFilterId)
|
||||||
|
: chatDashboardQueryService.queryTrends(startDate, endDate, context.tenantFilterId);
|
||||||
|
List<DashboardTrendItemVo> trends = buildTrendItems(context, rawTrends);
|
||||||
|
|
||||||
|
List<ChatAssistantUsageRank> rawRanks = chatDashboardQueryService.queryAssistantUsageRanks(
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
context.tenantFilterId,
|
||||||
|
DEFAULT_ASSISTANT_TREND_LIMIT
|
||||||
|
);
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends = buildAssistantTrendSeries(
|
||||||
|
context,
|
||||||
|
rawRanks
|
||||||
|
);
|
||||||
|
List<DashboardDistributionItemVo> distribution = buildAssistantDistribution(
|
||||||
|
rawRanks.subList(0, Math.min(DEFAULT_ASSISTANT_RANK_LIMIT, rawRanks.size()))
|
||||||
|
);
|
||||||
|
return new ChatDashboardPayload(chatStatus, trends, assistantTrends, distribution);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("加载工作台聊天统计失败,已降级为不可用状态,range={}, tenantId={}",
|
||||||
|
context.range,
|
||||||
|
context.tenantFilterId,
|
||||||
|
ex);
|
||||||
|
chatStatus.setAvailable(Boolean.FALSE);
|
||||||
|
chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
|
||||||
|
summary.setChatMessageTotal(0L);
|
||||||
|
summary.setChatSessionTotal(0L);
|
||||||
|
summary.setActiveAssistantTotal(0L);
|
||||||
|
summary.setChatActiveUserTotal(0L);
|
||||||
|
return new ChatDashboardPayload(
|
||||||
|
chatStatus,
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>(),
|
||||||
|
new ArrayList<>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建聊天趋势项,缺失日期补 0。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param rawTrends 原始趋势
|
||||||
|
* @return 趋势项
|
||||||
|
*/
|
||||||
|
private List<DashboardTrendItemVo> buildTrendItems(DashboardQueryContext context, List<ChatDashboardTrend> rawTrends) {
|
||||||
|
List<TimeBucket> buckets = buildBuckets(
|
||||||
|
context.range,
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate().minusDays(1)
|
||||||
|
);
|
||||||
|
Map<String, ChatDashboardTrend> trendMap = new HashMap<>();
|
||||||
|
for (ChatDashboardTrend rawTrend : rawTrends) {
|
||||||
|
trendMap.put(rawTrend.bucketKey(), rawTrend);
|
||||||
|
}
|
||||||
|
|
||||||
List<DashboardTrendItemVo> items = new ArrayList<>(buckets.size());
|
List<DashboardTrendItemVo> items = new ArrayList<>(buckets.size());
|
||||||
for (TimeBucket bucket : buckets) {
|
for (TimeBucket bucket : buckets) {
|
||||||
long activeUserTotal = activeUserMap.getOrDefault(bucket.key, 0L);
|
ChatDashboardTrend trend = trendMap.get(bucket.key);
|
||||||
|
|
||||||
DashboardTrendItemVo item = new DashboardTrendItemVo();
|
DashboardTrendItemVo item = new DashboardTrendItemVo();
|
||||||
item.setKey(bucket.key);
|
item.setKey(bucket.key);
|
||||||
item.setLabel(bucket.label);
|
item.setLabel(bucket.label);
|
||||||
item.setActiveUserTotal(activeUserTotal);
|
item.setActiveUserTotal(trend == null ? 0L : trend.activeUserTotal());
|
||||||
|
item.setChatMessageTotal(trend == null ? 0L : trend.messageTotal());
|
||||||
|
item.setChatSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DashboardDistributionItemVo> buildDistribution(DashboardQueryContext context, DashboardSummaryVo summary) {
|
/**
|
||||||
return buildResourceDistribution(summary);
|
* 构建智能体使用排行。
|
||||||
}
|
*
|
||||||
|
* @param ranks 原始排行数据
|
||||||
private List<DashboardDistributionItemVo> buildResourceDistribution(DashboardSummaryVo summary) {
|
* @return 页面排行项
|
||||||
List<DashboardDistributionItemVo> items = new ArrayList<>();
|
*/
|
||||||
items.add(buildPlatformItem("userTotal", "用户总量", summary.getUserTotal()));
|
private List<DashboardDistributionItemVo> buildAssistantDistribution(List<ChatAssistantUsageRank> ranks) {
|
||||||
items.add(buildPlatformItem("activeUserTotal", "活跃用户", summary.getActiveUserTotal()));
|
List<DashboardDistributionItemVo> items = new ArrayList<>(ranks.size());
|
||||||
items.add(buildPlatformItem("botTotal", "助手数量", summary.getBotTotal()));
|
for (ChatAssistantUsageRank rank : ranks) {
|
||||||
items.add(buildPlatformItem("workflowTotal", "工作流数量", summary.getWorkflowTotal()));
|
DashboardDistributionItemVo item = new DashboardDistributionItemVo();
|
||||||
items.add(buildPlatformItem("knowledgeBaseTotal", "知识库数量", summary.getKnowledgeBaseTotal()));
|
item.setKey(rank.assistantId() == null ? "" : rank.assistantId().toString());
|
||||||
|
item.setAssistantId(rank.assistantId());
|
||||||
|
item.setLabel(resolveAssistantLabel(rank.assistantId(), rank.assistantName()));
|
||||||
|
item.setUserTotal(rank.userTotal());
|
||||||
|
item.setMessageTotal(rank.messageTotal());
|
||||||
|
item.setSessionTotal(rank.sessionTotal());
|
||||||
|
item.setAvgMessagePerSession(calculateAvg(rank.messageTotal(), rank.sessionTotal()));
|
||||||
|
item.setAvgSessionPerUser(calculateAvg(rank.sessionTotal(), rank.userTotal()));
|
||||||
|
item.setValue(rank.sessionTotal());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardDistributionItemVo buildPlatformItem(String key, String label, Long value) {
|
/**
|
||||||
DashboardDistributionItemVo item = new DashboardDistributionItemVo();
|
* 构建智能体活跃趋势序列。
|
||||||
item.setKey(key);
|
*
|
||||||
item.setLabel(label);
|
* @param context 查询上下文
|
||||||
item.setValue(defaultLong(value));
|
* @param ranks 智能体排行
|
||||||
return item;
|
* @return 趋势序列
|
||||||
}
|
*/
|
||||||
|
private List<DashboardAssistantTrendSeriesVo> buildAssistantTrendSeries(DashboardQueryContext context,
|
||||||
private Map<String, Long> queryActiveUserTrend(DashboardQueryContext context, String bucketFormat) {
|
List<ChatAssistantUsageRank> ranks) {
|
||||||
StringBuilder sql = new StringBuilder();
|
if (ranks == null || ranks.isEmpty()) {
|
||||||
List<Object> params = new ArrayList<>();
|
return new ArrayList<>();
|
||||||
|
|
||||||
sql.append("SELECT DATE_FORMAT(l.created, '").append(bucketFormat).append("') AS bucket_key, ")
|
|
||||||
.append("COUNT(DISTINCT l.account_id) AS total ")
|
|
||||||
.append("FROM tb_sys_log l ")
|
|
||||||
.append("INNER JOIN tb_sys_account a ON a.id = l.account_id AND a.is_deleted IS NULL ")
|
|
||||||
.append("WHERE l.created >= ? AND l.created < ? ");
|
|
||||||
params.add(toDate(context.startTime));
|
|
||||||
params.add(toDate(context.endTime));
|
|
||||||
appendOptionalTenantFilter(sql, params, context.tenantFilterId, "a.tenant_id");
|
|
||||||
appendOptionalDeptFilter(sql, params, context.deptFilterId, "a.dept_id");
|
|
||||||
sql.append("GROUP BY bucket_key ORDER BY bucket_key ASC");
|
|
||||||
|
|
||||||
Map<String, Long> data = new HashMap<>();
|
|
||||||
for (Row row : Db.selectListBySql(sql.toString(), params.toArray())) {
|
|
||||||
data.put(asString(row.get("bucket_key")), asLong(row.get("total")));
|
|
||||||
}
|
}
|
||||||
return data;
|
List<TimeBucket> buckets = buildBuckets(
|
||||||
|
context.range,
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate().minusDays(1)
|
||||||
|
);
|
||||||
|
Map<BigInteger, ChatAssistantUsageRank> rankMap = new LinkedHashMap<>();
|
||||||
|
for (ChatAssistantUsageRank rank : ranks) {
|
||||||
|
rankMap.putIfAbsent(rank.assistantId(), rank);
|
||||||
|
}
|
||||||
|
List<BigInteger> assistantIds = new ArrayList<>(rankMap.keySet());
|
||||||
|
List<ChatAssistantSessionTrend> rawAssistantTrends = useHourlyBuckets(context)
|
||||||
|
? chatDashboardQueryService.queryAssistantHourlyTrends(
|
||||||
|
context.startTime,
|
||||||
|
context.endTime,
|
||||||
|
context.tenantFilterId,
|
||||||
|
assistantIds
|
||||||
|
)
|
||||||
|
: chatDashboardQueryService.queryAssistantTrends(
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate(),
|
||||||
|
context.tenantFilterId,
|
||||||
|
assistantIds
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<BigInteger, Map<String, ChatAssistantSessionTrend>> trendMap = new HashMap<>();
|
||||||
|
for (ChatAssistantSessionTrend rawTrend : rawAssistantTrends) {
|
||||||
|
trendMap.computeIfAbsent(rawTrend.assistantId(), key -> new HashMap<>())
|
||||||
|
.put(rawTrend.bucketKey(), rawTrend);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendSeriesVo> seriesList = new ArrayList<>(rankMap.size());
|
||||||
|
for (ChatAssistantUsageRank rank : rankMap.values()) {
|
||||||
|
BigInteger assistantId = rank.assistantId();
|
||||||
|
DashboardAssistantTrendSeriesVo series = new DashboardAssistantTrendSeriesVo();
|
||||||
|
series.setAssistantId(assistantId);
|
||||||
|
series.setLabel(resolveAssistantLabel(assistantId, rank.assistantName()));
|
||||||
|
series.setTotalSessionCount(rank.sessionTotal());
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendPointVo> points = new ArrayList<>(buckets.size());
|
||||||
|
Map<String, ChatAssistantSessionTrend> assistantTrendMap =
|
||||||
|
trendMap.getOrDefault(assistantId, new HashMap<>());
|
||||||
|
for (TimeBucket bucket : buckets) {
|
||||||
|
ChatAssistantSessionTrend trend = assistantTrendMap.get(bucket.key);
|
||||||
|
DashboardAssistantTrendPointVo point = new DashboardAssistantTrendPointVo();
|
||||||
|
point.setKey(bucket.key);
|
||||||
|
point.setLabel(bucket.label);
|
||||||
|
point.setSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||||
|
points.add(point);
|
||||||
|
}
|
||||||
|
series.setPoints(points);
|
||||||
|
seriesList.add(series);
|
||||||
|
}
|
||||||
|
return seriesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询用户活跃排行。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @param limit 返回条数,空表示全部
|
||||||
|
* @return 用户活跃排行
|
||||||
|
*/
|
||||||
|
private List<DashboardUserRankItemVo> queryUserRanks(DashboardQueryContext context, Integer limit) {
|
||||||
|
if (!chatDashboardQueryService.available()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<ChatActiveUserRank> rawUserRanks = chatDashboardQueryService.queryActiveUserRanks(
|
||||||
|
context.startTime.toLocalDate(),
|
||||||
|
context.endTime.toLocalDate(),
|
||||||
|
context.tenantFilterId,
|
||||||
|
context.assistantId,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
return buildUserRanks(rawUserRanks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户活跃排行。
|
||||||
|
*
|
||||||
|
* @param ranks 原始排行数据
|
||||||
|
* @return 页面排行项
|
||||||
|
*/
|
||||||
|
private List<DashboardUserRankItemVo> buildUserRanks(List<ChatActiveUserRank> ranks) {
|
||||||
|
if (ranks == null || ranks.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<DashboardUserRankItemVo> items = new ArrayList<>(ranks.size());
|
||||||
|
Map<BigInteger, AccountIdentitySnapshot> identityMap = resolveAccountIdentityMap(ranks);
|
||||||
|
for (ChatActiveUserRank rank : ranks) {
|
||||||
|
ResolvedUserIdentity identity = resolveUserIdentity(
|
||||||
|
rank.userId(),
|
||||||
|
rank.userAccount(),
|
||||||
|
identityMap.get(rank.userId())
|
||||||
|
);
|
||||||
|
DashboardUserRankItemVo item = new DashboardUserRankItemVo();
|
||||||
|
item.setUserId(rank.userId());
|
||||||
|
item.setLabel(identity.label);
|
||||||
|
item.setLoginName(identity.loginName);
|
||||||
|
item.setNickname(identity.nickname);
|
||||||
|
item.setSessionTotal(rank.sessionTotal());
|
||||||
|
item.setMessageTotal(rank.messageTotal());
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出表头。
|
||||||
|
*
|
||||||
|
* @return 表头
|
||||||
|
*/
|
||||||
|
private List<List<String>> buildUserRankExportHead() {
|
||||||
|
List<List<String>> head = new ArrayList<>(4);
|
||||||
|
head.add(List.of("登录账号"));
|
||||||
|
head.add(List.of("昵称"));
|
||||||
|
head.add(List.of("会话数"));
|
||||||
|
head.add(List.of("消息数"));
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出数据行。
|
||||||
|
*
|
||||||
|
* @param userRanks 用户排行
|
||||||
|
* @return 数据行
|
||||||
|
*/
|
||||||
|
private List<List<String>> buildUserRankExportRows(List<DashboardUserRankItemVo> userRanks) {
|
||||||
|
List<List<String>> rows = new ArrayList<>(userRanks.size());
|
||||||
|
for (DashboardUserRankItemVo item : userRanks) {
|
||||||
|
List<String> row = new ArrayList<>(4);
|
||||||
|
row.add(defaultIfBlank(item.getLoginName()));
|
||||||
|
row.add(defaultIfBlank(item.getNickname()));
|
||||||
|
row.add(String.valueOf(item.getSessionTotal() == null ? 0L : item.getSessionTotal()));
|
||||||
|
row.add(String.valueOf(item.getMessageTotal() == null ? 0L : item.getMessageTotal()));
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按租户统计平台资源数量。
|
||||||
|
*
|
||||||
|
* @param tableName 表名
|
||||||
|
* @param alias 别名
|
||||||
|
* @param containsLogicDelete 是否包含逻辑删除条件
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 统计值
|
||||||
|
*/
|
||||||
private long countScopedTable(String tableName, String alias, boolean containsLogicDelete, DashboardQueryContext context) {
|
private long countScopedTable(String tableName, String alias, boolean containsLogicDelete, DashboardQueryContext context) {
|
||||||
StringBuilder sql = new StringBuilder();
|
StringBuilder sql = new StringBuilder();
|
||||||
List<Object> params = new ArrayList<>();
|
List<Object> params = new ArrayList<>();
|
||||||
@@ -152,6 +470,12 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return queryForLong(sql.toString(), params);
|
return queryForLong(sql.toString(), params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计当前时间范围内活跃用户数。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return 活跃用户数
|
||||||
|
*/
|
||||||
private long countActiveUsers(DashboardQueryContext context) {
|
private long countActiveUsers(DashboardQueryContext context) {
|
||||||
StringBuilder sql = new StringBuilder();
|
StringBuilder sql = new StringBuilder();
|
||||||
List<Object> params = new ArrayList<>();
|
List<Object> params = new ArrayList<>();
|
||||||
@@ -167,11 +491,26 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return queryForLong(sql.toString(), params);
|
return queryForLong(sql.toString(), params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 count SQL 并返回 long 值。
|
||||||
|
*
|
||||||
|
* @param sql SQL
|
||||||
|
* @param params 参数
|
||||||
|
* @return long 值
|
||||||
|
*/
|
||||||
private long queryForLong(String sql, List<Object> params) {
|
private long queryForLong(String sql, List<Object> params) {
|
||||||
Object result = Db.selectObject(sql, params.toArray());
|
Object result = Db.selectObject(sql, params.toArray());
|
||||||
return asLong(result);
|
return asLong(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加租户过滤。
|
||||||
|
*
|
||||||
|
* @param sql SQL 构造器
|
||||||
|
* @param params 参数列表
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @param columnName 列名
|
||||||
|
*/
|
||||||
private void appendOptionalTenantFilter(StringBuilder sql, List<Object> params, BigInteger tenantId, String columnName) {
|
private void appendOptionalTenantFilter(StringBuilder sql, List<Object> params, BigInteger tenantId, String columnName) {
|
||||||
if (tenantId != null) {
|
if (tenantId != null) {
|
||||||
sql.append(" AND ").append(columnName).append(" = ? ");
|
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||||
@@ -179,6 +518,14 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加部门过滤。
|
||||||
|
*
|
||||||
|
* @param sql SQL 构造器
|
||||||
|
* @param params 参数列表
|
||||||
|
* @param deptId 部门 ID
|
||||||
|
* @param columnName 列名
|
||||||
|
*/
|
||||||
private void appendOptionalDeptFilter(StringBuilder sql, List<Object> params, BigInteger deptId, String columnName) {
|
private void appendOptionalDeptFilter(StringBuilder sql, List<Object> params, BigInteger deptId, String columnName) {
|
||||||
if (deptId != null) {
|
if (deptId != null) {
|
||||||
sql.append(" AND ").append(columnName).append(" = ? ");
|
sql.append(" AND ").append(columnName).append(" = ? ");
|
||||||
@@ -186,28 +533,95 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardOverviewQuery query) {
|
||||||
|
return buildContext(
|
||||||
|
loginAccount,
|
||||||
|
query == null ? null : query.getRange(),
|
||||||
|
query == null ? null : query.getStartDate(),
|
||||||
|
query == null ? null : query.getEndDate()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建用户榜查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param query 用户榜查询
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
|
private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardUserRankQuery query) {
|
||||||
|
DashboardQueryContext context = buildContext(
|
||||||
|
loginAccount,
|
||||||
|
query == null ? null : query.getRange(),
|
||||||
|
query == null ? null : query.getStartDate(),
|
||||||
|
query == null ? null : query.getEndDate()
|
||||||
|
);
|
||||||
|
context.assistantId = validateAssistantId(loginAccount, query == null ? null : query.getAssistantId());
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建基础查询上下文。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return 查询上下文
|
||||||
|
*/
|
||||||
|
private DashboardQueryContext buildContext(LoginAccount loginAccount,
|
||||||
|
String range,
|
||||||
|
String startDate,
|
||||||
|
String endDate) {
|
||||||
DashboardQueryContext context = new DashboardQueryContext();
|
DashboardQueryContext context = new DashboardQueryContext();
|
||||||
context.range = normalizeRange(query == null ? null : query.getRange());
|
context.range = normalizeRange(range);
|
||||||
context.superAdmin = isSuperAdmin(loginAccount);
|
context.superAdmin = isSuperAdmin(loginAccount);
|
||||||
|
|
||||||
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||||
if ("today".equals(context.range)) {
|
if ("today".equals(context.range)) {
|
||||||
context.startTime = LocalDateTime.of(today, LocalTime.MIN);
|
context.startTime = LocalDateTime.of(today, LocalTime.MIN);
|
||||||
context.endTime = context.startTime.plusDays(1);
|
context.endTime = context.startTime.plusDays(1);
|
||||||
|
context.queryStartDate = today.toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
} else if ("7d".equals(context.range)) {
|
} else if ("7d".equals(context.range)) {
|
||||||
context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
|
context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
|
||||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||||
} else {
|
context.queryStartDate = today.minusDays(6).toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
|
} else if ("30d".equals(context.range)) {
|
||||||
context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
|
context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
|
||||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||||
|
context.queryStartDate = today.minusDays(29).toString();
|
||||||
|
context.queryEndDate = today.toString();
|
||||||
|
} else {
|
||||||
|
LocalDate customStartDate = parseRequiredDate(startDate, "开始日期不能为空");
|
||||||
|
LocalDate customEndDate = parseRequiredDate(endDate, "结束日期不能为空");
|
||||||
|
if (customStartDate.isAfter(customEndDate)) {
|
||||||
|
throw new BusinessException("开始日期不能晚于结束日期");
|
||||||
|
}
|
||||||
|
context.startTime = LocalDateTime.of(customStartDate, LocalTime.MIN);
|
||||||
|
context.endTime = LocalDateTime.of(customEndDate.plusDays(1), LocalTime.MIN);
|
||||||
|
context.queryStartDate = customStartDate.toString();
|
||||||
|
context.queryEndDate = customEndDate.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
context.tenantFilterId = context.superAdmin ? null : loginAccount.getTenantId();
|
context.tenantFilterId = context.superAdmin || loginAccount == null ? null : loginAccount.getTenantId();
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前登录账号是否为超管。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @return true 表示超管
|
||||||
|
*/
|
||||||
private boolean isSuperAdmin(LoginAccount loginAccount) {
|
private boolean isSuperAdmin(LoginAccount loginAccount) {
|
||||||
if (loginAccount == null || loginAccount.getId() == null) {
|
if (loginAccount == null || loginAccount.getId() == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -228,24 +642,41 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return sysRoleService.count(roleWrapper) > 0;
|
return sysRoleService.count(roleWrapper) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化时间范围参数。
|
||||||
|
*
|
||||||
|
* @param range 原始时间范围
|
||||||
|
* @return 规范化后的时间范围
|
||||||
|
*/
|
||||||
private String normalizeRange(String range) {
|
private String normalizeRange(String range) {
|
||||||
if (!StringUtils.hasText(range)) {
|
if (!StringUtils.hasText(range)) {
|
||||||
return "7d";
|
return "7d";
|
||||||
}
|
}
|
||||||
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range)) {
|
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range) || "custom".equals(range)) {
|
||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
throw new BusinessException("不支持的时间范围: " + range);
|
throw new BusinessException("不支持的时间范围: " + range);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<TimeBucket> buildBuckets(String range) {
|
/**
|
||||||
|
* 构建时间桶。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @return 时间桶列表
|
||||||
|
*/
|
||||||
|
private List<TimeBucket> buildBuckets(String range, LocalDate customStartDate, LocalDate customEndDate) {
|
||||||
List<TimeBucket> buckets = new ArrayList<>();
|
List<TimeBucket> buckets = new ArrayList<>();
|
||||||
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||||
|
boolean hourlyBucket = "today".equals(range)
|
||||||
if ("today".equals(range)) {
|
|| ("custom".equals(range)
|
||||||
|
&& customStartDate != null
|
||||||
|
&& customEndDate != null
|
||||||
|
&& customStartDate.equals(customEndDate));
|
||||||
|
if (hourlyBucket) {
|
||||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
|
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
|
||||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
|
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
|
||||||
LocalDateTime start = LocalDateTime.of(today, LocalTime.MIN);
|
LocalDate bucketDate = "today".equals(range) ? today : customStartDate;
|
||||||
|
LocalDateTime start = LocalDateTime.of(bucketDate, LocalTime.MIN);
|
||||||
for (int hour = 0; hour < 24; hour++) {
|
for (int hour = 0; hour < 24; hour++) {
|
||||||
LocalDateTime current = start.plusHours(hour);
|
LocalDateTime current = start.plusHours(hour);
|
||||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||||
@@ -253,10 +684,20 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return buckets;
|
return buckets;
|
||||||
}
|
}
|
||||||
|
|
||||||
int days = "7d".equals(range) ? 7 : 30;
|
|
||||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
|
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||||
LocalDate start = today.minusDays(days - 1L);
|
int days;
|
||||||
|
LocalDate start;
|
||||||
|
if ("7d".equals(range)) {
|
||||||
|
days = 7;
|
||||||
|
start = today.minusDays(6);
|
||||||
|
} else if ("30d".equals(range)) {
|
||||||
|
days = 30;
|
||||||
|
start = today.minusDays(29);
|
||||||
|
} else {
|
||||||
|
start = customStartDate;
|
||||||
|
days = (int) java.time.temporal.ChronoUnit.DAYS.between(customStartDate, customEndDate) + 1;
|
||||||
|
}
|
||||||
for (int i = 0; i < days; i++) {
|
for (int i = 0; i < days; i++) {
|
||||||
LocalDate current = start.plusDays(i);
|
LocalDate current = start.plusDays(i);
|
||||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||||
@@ -264,18 +705,54 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return buckets;
|
return buckets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前上下文是否按小时构建趋势。
|
||||||
|
*
|
||||||
|
* @param context 查询上下文
|
||||||
|
* @return true 表示按小时
|
||||||
|
*/
|
||||||
|
private boolean useHourlyBuckets(DashboardQueryContext context) {
|
||||||
|
return "today".equals(context.range)
|
||||||
|
|| ("custom".equals(context.range)
|
||||||
|
&& context.startTime != null
|
||||||
|
&& context.endTime != null
|
||||||
|
&& context.startTime.toLocalDate().equals(context.endTime.toLocalDate().minusDays(1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析必填日期参数。
|
||||||
|
*
|
||||||
|
* @param dateText 日期文本
|
||||||
|
* @param errorMessage 错误信息
|
||||||
|
* @return 日期
|
||||||
|
*/
|
||||||
|
private LocalDate parseRequiredDate(String dateText, String errorMessage) {
|
||||||
|
if (!StringUtils.hasText(dateText)) {
|
||||||
|
throw new BusinessException(errorMessage);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return LocalDate.parse(dateText.trim());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new BusinessException("日期格式不正确: " + dateText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 LocalDateTime 转换为 Date。
|
||||||
|
*
|
||||||
|
* @param dateTime 时间
|
||||||
|
* @return Date
|
||||||
|
*/
|
||||||
private Date toDate(LocalDateTime dateTime) {
|
private Date toDate(LocalDateTime dateTime) {
|
||||||
return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
|
return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
|
||||||
}
|
}
|
||||||
|
|
||||||
private long defaultLong(Long value) {
|
/**
|
||||||
return value == null ? 0L : value;
|
* 解析对象为 long 值。
|
||||||
}
|
*
|
||||||
|
* @param value 原始对象
|
||||||
private String asString(Object value) {
|
* @return long 值
|
||||||
return value == null ? "" : String.valueOf(value);
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
private long asLong(Object value) {
|
private long asLong(Object value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 0L;
|
return 0L;
|
||||||
@@ -286,15 +763,163 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
return Long.parseLong(String.valueOf(value));
|
return Long.parseLong(String.valueOf(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算平均每会话消息数。
|
||||||
|
*
|
||||||
|
* @param messageTotal 消息总数
|
||||||
|
* @param sessionTotal 会话总数
|
||||||
|
* @return 平均值
|
||||||
|
*/
|
||||||
|
private double calculateAvg(long messageTotal, long sessionTotal) {
|
||||||
|
if (sessionTotal <= 0) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
return (double) messageTotal / (double) sessionTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量解析用户展示名称映射。
|
||||||
|
*
|
||||||
|
* @param ranks 活跃排行
|
||||||
|
* @return 名称映射
|
||||||
|
*/
|
||||||
|
private Map<BigInteger, AccountIdentitySnapshot> resolveAccountIdentityMap(List<ChatActiveUserRank> ranks) {
|
||||||
|
List<BigInteger> userIds = ranks.stream()
|
||||||
|
.map(ChatActiveUserRank::userId)
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (userIds.isEmpty()) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
|
.select(SysAccount::getId, SysAccount::getLoginName, SysAccount::getNickname)
|
||||||
|
.in(SysAccount::getId, userIds);
|
||||||
|
List<SysAccount> accounts = sysAccountService.list(queryWrapper);
|
||||||
|
Map<BigInteger, AccountIdentitySnapshot> identityMap = new HashMap<>(userIds.size());
|
||||||
|
if (accounts == null) {
|
||||||
|
return identityMap;
|
||||||
|
}
|
||||||
|
for (SysAccount account : accounts) {
|
||||||
|
if (account == null || account.getId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
identityMap.put(
|
||||||
|
account.getId(),
|
||||||
|
new AccountIdentitySnapshot(trimToNull(account.getLoginName()), trimToNull(account.getNickname()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return identityMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析智能体展示名称。
|
||||||
|
*
|
||||||
|
* @param assistantId 智能体 ID
|
||||||
|
* @param assistantName 智能体名称
|
||||||
|
* @return 展示名称
|
||||||
|
*/
|
||||||
|
private String resolveAssistantLabel(BigInteger assistantId, String assistantName) {
|
||||||
|
if (StringUtils.hasText(assistantName)) {
|
||||||
|
return assistantName.trim();
|
||||||
|
}
|
||||||
|
return assistantId == null ? "智能体-未知" : "智能体-" + assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析用户展示名称。
|
||||||
|
*
|
||||||
|
* @param userId 用户 ID
|
||||||
|
* @param userAccount 聊天侧账号快照
|
||||||
|
* @param snapshot 系统账号快照
|
||||||
|
* @return 用户身份
|
||||||
|
*/
|
||||||
|
private ResolvedUserIdentity resolveUserIdentity(BigInteger userId,
|
||||||
|
String userAccount,
|
||||||
|
AccountIdentitySnapshot snapshot) {
|
||||||
|
String loginName = snapshot == null ? null : snapshot.loginName;
|
||||||
|
String nickname = snapshot == null ? null : snapshot.nickname;
|
||||||
|
String trimmedUserAccount = trimToNull(userAccount);
|
||||||
|
if (StringUtils.hasText(loginName)) {
|
||||||
|
if (StringUtils.hasText(nickname)) {
|
||||||
|
return new ResolvedUserIdentity(loginName, nickname, loginName + "(" + nickname + ")");
|
||||||
|
}
|
||||||
|
return new ResolvedUserIdentity(loginName, nickname, loginName);
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(trimmedUserAccount)) {
|
||||||
|
return new ResolvedUserIdentity(trimmedUserAccount, nickname, trimmedUserAccount);
|
||||||
|
}
|
||||||
|
return new ResolvedUserIdentity(null, nickname, userId == null ? "用户-未知" : "用户-" + userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验智能体筛选条件。
|
||||||
|
*
|
||||||
|
* @param loginAccount 当前登录账号
|
||||||
|
* @param assistantId 智能体 ID
|
||||||
|
* @return 规范化后的智能体 ID
|
||||||
|
*/
|
||||||
|
private BigInteger validateAssistantId(LoginAccount loginAccount, BigInteger assistantId) {
|
||||||
|
if (assistantId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Bot bot = botService.getById(assistantId);
|
||||||
|
if (bot == null || !Integer.valueOf(1).equals(bot.getStatus())) {
|
||||||
|
throw new BusinessException("聊天助手不存在或未启用");
|
||||||
|
}
|
||||||
|
boolean visible = categoryPermissionService.canAccessCategory(
|
||||||
|
loginAccount,
|
||||||
|
"BOT",
|
||||||
|
bot.getCreatedBy(),
|
||||||
|
bot.getCategoryId()
|
||||||
|
);
|
||||||
|
if (!visible) {
|
||||||
|
throw new BusinessException("聊天助手不存在或未启用");
|
||||||
|
}
|
||||||
|
return assistantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化文本。
|
||||||
|
*
|
||||||
|
* @param value 原始文本
|
||||||
|
* @return 去空白后的文本
|
||||||
|
*/
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把空文本转为空串。
|
||||||
|
*
|
||||||
|
* @param value 原始文本
|
||||||
|
* @return 输出文本
|
||||||
|
*/
|
||||||
|
private String defaultIfBlank(String value) {
|
||||||
|
return StringUtils.hasText(value) ? value.trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作台查询上下文。
|
||||||
|
*/
|
||||||
private static class DashboardQueryContext {
|
private static class DashboardQueryContext {
|
||||||
private String range;
|
private String range;
|
||||||
private BigInteger tenantFilterId;
|
private BigInteger tenantFilterId;
|
||||||
private BigInteger deptFilterId;
|
private BigInteger deptFilterId;
|
||||||
|
private BigInteger assistantId;
|
||||||
private boolean superAdmin;
|
private boolean superAdmin;
|
||||||
private LocalDateTime startTime;
|
private LocalDateTime startTime;
|
||||||
private LocalDateTime endTime;
|
private LocalDateTime endTime;
|
||||||
|
private String queryStartDate;
|
||||||
|
private String queryEndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间桶。
|
||||||
|
*/
|
||||||
private static class TimeBucket {
|
private static class TimeBucket {
|
||||||
private final String key;
|
private final String key;
|
||||||
private final String label;
|
private final String label;
|
||||||
@@ -304,4 +929,43 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
this.label = label;
|
this.label = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统账号身份快照。
|
||||||
|
*/
|
||||||
|
private static class AccountIdentitySnapshot {
|
||||||
|
private final String loginName;
|
||||||
|
private final String nickname;
|
||||||
|
|
||||||
|
private AccountIdentitySnapshot(String loginName, String nickname) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户展示身份。
|
||||||
|
*/
|
||||||
|
private static class ResolvedUserIdentity {
|
||||||
|
private final String loginName;
|
||||||
|
private final String nickname;
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
private ResolvedUserIdentity(String loginName, String nickname, String label) {
|
||||||
|
this.loginName = loginName;
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天统计页面载荷。
|
||||||
|
*/
|
||||||
|
private record ChatDashboardPayload(
|
||||||
|
DashboardChatStatusVo chatStatus,
|
||||||
|
List<DashboardTrendItemVo> trends,
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends,
|
||||||
|
List<DashboardDistributionItemVo> distribution
|
||||||
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,626 @@
|
|||||||
|
package tech.easyflow.admin.service.dashboard.impl;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
import org.testng.Assert;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
import tech.easyflow.ai.entity.Bot;
|
||||||
|
import tech.easyflow.ai.service.BotService;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||||
|
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||||
|
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysAccountRoleService;
|
||||||
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DashboardServiceImpl} 测试。
|
||||||
|
*/
|
||||||
|
public class DashboardServiceImplTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证分析库不可用时返回明确不可用状态,且趋势与排行为空。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldReturnUnavailableChatPayloadWhenAnalyticalDbIsDisabled() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(false);
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("7d", null);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
Object chatStatus = readField(payload, "chatStatus");
|
||||||
|
List<?> trends = (List<?>) readField(payload, "trends");
|
||||||
|
List<?> distribution = (List<?>) readField(payload, "distribution");
|
||||||
|
|
||||||
|
Assert.assertFalse(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||||
|
Assert.assertEquals(invokeGetter(chatStatus, "getMessage"), "聊天数据不可用");
|
||||||
|
Assert.assertTrue(trends.isEmpty());
|
||||||
|
Assert.assertTrue(distribution.isEmpty());
|
||||||
|
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 today 返回 24 个小时点位,且 overview 不再触发用户榜查询。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildHourlyTrendForToday() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
String currentHourKey = LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(3L, 9L, 1L, 2L));
|
||||||
|
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L, 2L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 2L, 3L, 9L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantHourlyTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatAssistantSessionTrend(BigInteger.ONE, "", currentHourKey, 3L)));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("today", BigInteger.valueOf(9));
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
Object chatStatus = readField(payload, "chatStatus");
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
List<DashboardDistributionItemVo> distribution = (List<DashboardDistributionItemVo>) readField(payload, "distribution");
|
||||||
|
|
||||||
|
Assert.assertTrue(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||||
|
Assert.assertEquals(trends.size(), 24);
|
||||||
|
Assert.assertEquals(trends.get(0).getLabel(), "00:00");
|
||||||
|
Assert.assertEquals(trends.get(10).getKey(), currentHourKey);
|
||||||
|
Assert.assertEquals(trends.get(10).getLabel(), "10:00");
|
||||||
|
Assert.assertEquals(trends.get(10).getChatMessageTotal(), Long.valueOf(9L));
|
||||||
|
Assert.assertEquals(trends.get(10).getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(trends.get(10).getActiveUserTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(trends.get(11).getChatMessageTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(trends.get(11).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(9L));
|
||||||
|
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(1L));
|
||||||
|
Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 1);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getLabel(), "智能体-1");
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getTotalSessionCount(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 24);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(10).getSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(11).getSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(distribution.get(0).getLabel(), "智能体-1");
|
||||||
|
Assert.assertEquals(distribution.get(0).getAvgMessagePerSession(), Double.valueOf(3D));
|
||||||
|
Assert.assertEquals(distribution.get(0).getAvgSessionPerUser(), Double.valueOf(1.5D));
|
||||||
|
verify(chatDashboardQueryService, never()).queryActiveUserRanks(any(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证当系统账号名称仅回退为纯 ID 时,仍优先继续回退到聊天侧账号。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldFallbackToUserAccountWhenSystemDisplayNameIsOnlyId() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(9), "chat-user", 1L, 1L, 1L)));
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(9L, "", "仅昵称")));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
|
||||||
|
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLabel(), "chat-user");
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLoginName(), "chat-user");
|
||||||
|
Assert.assertEquals(userRanks.get(0).getNickname(), "仅昵称");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证日趋势会保留 assistantId 为空的排行项,并补齐 7 天点位。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildDailyAssistantTrendSeriesForRankedAssistants() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(4L, 12L, 2L, 3L));
|
||||||
|
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantUsageRank(BigInteger.ONE, "助手-A", 3L, 4L, 12L),
|
||||||
|
new ChatAssistantUsageRank(null, "未知助手", 1L, 2L, 4L)
|
||||||
|
));
|
||||||
|
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().minusDays(6).toString(), 2L),
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().toString(), 4L),
|
||||||
|
new ChatAssistantSessionTrend(null, "未知助手", LocalDate.now().minusDays(3).toString(), 2L)
|
||||||
|
));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("7d", BigInteger.ONE);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 2);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getLabel(), "助手-A");
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 7);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(0).getSessionTotal(), Long.valueOf(2L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(6).getSessionTotal(), Long.valueOf(4L));
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().get(1).getSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertNull(assistantTrends.get(1).getAssistantId());
|
||||||
|
Assert.assertEquals(assistantTrends.get(1).getLabel(), "未知助手");
|
||||||
|
Assert.assertEquals(assistantTrends.get(1).getPoints().get(3).getSessionTotal(), Long.valueOf(2L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证自定义单天范围按小时桶构建。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildHourlyTrendForCustomSingleDayRange() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
LocalDate customDate = LocalDate.now().minusDays(2);
|
||||||
|
String currentHourKey = LocalDateTime.of(customDate, LocalTime.of(8, 0))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(2L, 6L, 1L, 1L));
|
||||||
|
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 2L, 6L, 1L)));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||||
|
.thenReturn(List.of());
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext(
|
||||||
|
"custom",
|
||||||
|
BigInteger.ONE,
|
||||||
|
LocalDateTime.of(customDate, LocalTime.MIN),
|
||||||
|
LocalDateTime.of(customDate.plusDays(1), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
Assert.assertEquals(trends.size(), 24);
|
||||||
|
Assert.assertEquals(trends.get(8).getKey(), currentHourKey);
|
||||||
|
Assert.assertEquals(trends.get(8).getActiveUserTotal(), Long.valueOf(1L));
|
||||||
|
Assert.assertEquals(trends.get(9).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证自定义多天范围按天桶构建,并保留查询日期。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldBuildDailyBucketsForCustomMultiDayRangeContext() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
DashboardOverviewQuery query = new DashboardOverviewQuery();
|
||||||
|
query.setRange("custom");
|
||||||
|
query.setStartDate("2026-05-01");
|
||||||
|
query.setEndDate("2026-05-03");
|
||||||
|
|
||||||
|
Object context = invokeBuildContext(service, query);
|
||||||
|
Assert.assertEquals(readField(context, "range"), "custom");
|
||||||
|
Assert.assertEquals(
|
||||||
|
readField(context, "startTime"),
|
||||||
|
LocalDateTime.of(LocalDate.of(2026, 5, 1), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
Assert.assertEquals(
|
||||||
|
readField(context, "endTime"),
|
||||||
|
LocalDateTime.of(LocalDate.of(2026, 5, 4), LocalTime.MIN)
|
||||||
|
);
|
||||||
|
Assert.assertEquals(readField(context, "queryStartDate"), "2026-05-01");
|
||||||
|
Assert.assertEquals(readField(context, "queryEndDate"), "2026-05-03");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证近 30 天趋势补齐完整 30 个桶,并按 Top 8 请求智能体活跃排行。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void shouldBuildThirtyDayBucketsAndRequestTopEightAssistantRanks() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
LocalDate startDate = LocalDate.now().minusDays(29);
|
||||||
|
LocalDate endDate = LocalDate.now();
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||||
|
.thenReturn(new ChatDashboardSummary(10L, 20L, 8L, 4L));
|
||||||
|
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatDashboardTrend(startDate.toString(), 3L, 6L, 2L),
|
||||||
|
new ChatDashboardTrend(endDate.toString(), 7L, 14L, 4L)
|
||||||
|
));
|
||||||
|
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), eq(8)))
|
||||||
|
.thenReturn(IntStream.rangeClosed(1, 8)
|
||||||
|
.mapToObj(index -> new ChatAssistantUsageRank(
|
||||||
|
BigInteger.valueOf(index),
|
||||||
|
"助手-" + index,
|
||||||
|
index,
|
||||||
|
index * 2L,
|
||||||
|
index * 4L
|
||||||
|
))
|
||||||
|
.toList());
|
||||||
|
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||||
|
.thenReturn(List.of(
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", startDate.toString(), 2L),
|
||||||
|
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", endDate.toString(), 4L)
|
||||||
|
));
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
|
||||||
|
Object context = newContext("30d", BigInteger.ONE);
|
||||||
|
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||||
|
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||||
|
|
||||||
|
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||||
|
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||||
|
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||||
|
Assert.assertEquals(trends.size(), 30);
|
||||||
|
Assert.assertEquals(trends.get(0).getKey(), startDate.toString());
|
||||||
|
Assert.assertEquals(trends.get(0).getChatSessionTotal(), Long.valueOf(3L));
|
||||||
|
Assert.assertEquals(trends.get(29).getKey(), endDate.toString());
|
||||||
|
Assert.assertEquals(trends.get(29).getChatMessageTotal(), Long.valueOf(14L));
|
||||||
|
Assert.assertEquals(trends.get(1).getChatSessionTotal(), Long.valueOf(0L));
|
||||||
|
Assert.assertEquals(assistantTrends.size(), 8);
|
||||||
|
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 30);
|
||||||
|
verify(chatDashboardQueryService).queryAssistantUsageRanks(any(), any(), any(), eq(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户榜筛选会透传 assistantId。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
|
||||||
|
SysRoleService sysRoleService = mock(SysRoleService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(1);
|
||||||
|
bot.setCreatedBy(BigInteger.ONE);
|
||||||
|
bot.setCategoryId(BigInteger.valueOf(8));
|
||||||
|
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
|
||||||
|
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "botService", botService);
|
||||||
|
setField(service, "categoryPermissionService", categoryPermissionService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
setField(service, "sysAccountRoleService", sysAccountRoleService);
|
||||||
|
setField(service, "sysRoleService", sysRoleService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
|
||||||
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
|
loginAccount.setId(BigInteger.valueOf(12));
|
||||||
|
loginAccount.setTenantId(BigInteger.valueOf(33));
|
||||||
|
|
||||||
|
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(loginAccount, query);
|
||||||
|
Assert.assertEquals(userRanks.size(), 1);
|
||||||
|
Assert.assertEquals(userRanks.get(0).getLabel(), "demo-user(演示用户)");
|
||||||
|
verify(chatDashboardQueryService).queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证未启用智能体会被拒绝。
|
||||||
|
*/
|
||||||
|
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||||
|
public void shouldRejectDisabledAssistantFilter() {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(0);
|
||||||
|
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
|
||||||
|
setFieldSilently(service, "botService", botService);
|
||||||
|
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
|
||||||
|
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
service.getUserRanks(new LoginAccount(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证当前作用域不可见的智能体会被拒绝。
|
||||||
|
*/
|
||||||
|
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||||
|
public void shouldRejectInvisibleAssistantFilter() {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
BotService botService = mock(BotService.class);
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||||
|
|
||||||
|
Bot bot = new Bot();
|
||||||
|
bot.setId(BigInteger.TEN);
|
||||||
|
bot.setStatus(1);
|
||||||
|
bot.setCreatedBy(BigInteger.ONE);
|
||||||
|
bot.setCategoryId(BigInteger.valueOf(8));
|
||||||
|
|
||||||
|
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||||
|
.thenReturn(false);
|
||||||
|
|
||||||
|
setFieldSilently(service, "botService", botService);
|
||||||
|
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
|
||||||
|
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
query.setAssistantId(BigInteger.TEN);
|
||||||
|
service.getUserRanks(new LoginAccount(), query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证导出结果包含表头与账号昵称列。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldExportDashboardUserRanksWithSeparatedIdentityColumns() throws Exception {
|
||||||
|
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||||
|
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||||
|
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||||
|
|
||||||
|
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||||
|
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), isNull(), isNull()))
|
||||||
|
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(7), "export-user", 3L, 6L, 1L)));
|
||||||
|
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(7L, "export-user", "导出演示")));
|
||||||
|
|
||||||
|
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||||
|
setField(service, "sysAccountService", sysAccountService);
|
||||||
|
|
||||||
|
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||||
|
query.setRange("7d");
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
service.exportUserRanks(new LoginAccount(), query, outputStream);
|
||||||
|
|
||||||
|
try (org.apache.poi.ss.usermodel.Workbook workbook =
|
||||||
|
WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) {
|
||||||
|
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(0).getStringCellValue(), "登录账号");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(1).getStringCellValue(), "昵称");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(2).getStringCellValue(), "会话数");
|
||||||
|
Assert.assertEquals(sheet.getRow(0).getCell(3).getStringCellValue(), "消息数");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(0).getStringCellValue(), "export-user");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(1).getStringCellValue(), "导出演示");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(2).getStringCellValue(), "3");
|
||||||
|
Assert.assertEquals(sheet.getRow(1).getCell(3).getStringCellValue(), "6");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造查询上下文。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @return 查询上下文实例
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object newContext(String range, BigInteger tenantId) throws Exception {
|
||||||
|
return newContext(
|
||||||
|
range,
|
||||||
|
tenantId,
|
||||||
|
LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MIN),
|
||||||
|
LocalDateTime.of(LocalDate.now().plusDays(1), java.time.LocalTime.MIN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造查询上下文。
|
||||||
|
*
|
||||||
|
* @param range 时间范围
|
||||||
|
* @param tenantId 租户 ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 查询上下文实例
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object newContext(String range,
|
||||||
|
BigInteger tenantId,
|
||||||
|
LocalDateTime startTime,
|
||||||
|
LocalDateTime endTime) throws Exception {
|
||||||
|
Class<?> contextClass = Class.forName(
|
||||||
|
"tech.easyflow.admin.service.dashboard.impl.DashboardServiceImpl$DashboardQueryContext"
|
||||||
|
);
|
||||||
|
Constructor<?> constructor = contextClass.getDeclaredConstructor();
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
Object context = constructor.newInstance();
|
||||||
|
setField(context, "range", range);
|
||||||
|
setField(context, "tenantFilterId", tenantId);
|
||||||
|
setField(context, "startTime", startTime);
|
||||||
|
setField(context, "endTime", endTime);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用私有上下文构建方法。
|
||||||
|
*
|
||||||
|
* @param service service
|
||||||
|
* @param query 查询参数
|
||||||
|
* @return 上下文
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeBuildContext(DashboardServiceImpl service, DashboardOverviewQuery query) throws Exception {
|
||||||
|
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||||
|
"buildContext",
|
||||||
|
LoginAccount.class,
|
||||||
|
DashboardOverviewQuery.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(service, null, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用私有聊天载荷组装方法。
|
||||||
|
*
|
||||||
|
* @param service service
|
||||||
|
* @param context 上下文
|
||||||
|
* @param summary 汇总对象
|
||||||
|
* @return 载荷
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeBuildChatPayload(DashboardServiceImpl service, Object context, DashboardSummaryVo summary)
|
||||||
|
throws Exception {
|
||||||
|
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||||
|
"buildChatPayload",
|
||||||
|
context.getClass(),
|
||||||
|
DashboardSummaryVo.class
|
||||||
|
);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method.invoke(service, context, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取对象字段。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @return 字段值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object readField(Object target, String fieldName) throws Exception {
|
||||||
|
Field field = target.getClass().getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.get(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 getter。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param methodName 方法名
|
||||||
|
* @return 返回值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private Object invokeGetter(Object target, String methodName) throws Exception {
|
||||||
|
Method method = target.getClass().getMethod(methodName);
|
||||||
|
return method.invoke(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过反射设置字段值。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
* @throws Exception 反射失败
|
||||||
|
*/
|
||||||
|
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Class<?> current = target.getClass();
|
||||||
|
while (current != null) {
|
||||||
|
try {
|
||||||
|
Field field = current.getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
current = current.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFieldSilently(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
setField(target, fieldName, value);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SysAccount buildSysAccount(long id, String loginName, String nickname) {
|
||||||
|
SysAccount account = new SysAccount();
|
||||||
|
account.setId(BigInteger.valueOf(id));
|
||||||
|
account.setLoginName(loginName);
|
||||||
|
account.setNickname(nickname);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import tech.easyflow.ai.service.KnowledgeShareAuditService;
|
|||||||
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
import tech.easyflow.ai.service.KnowledgeSharePermissionService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
import tech.easyflow.ai.service.impl.KnowledgeSharePermissionServiceImpl;
|
import tech.easyflow.ai.service.impl.KnowledgeSharePermissionServiceImpl;
|
||||||
|
import tech.easyflow.ai.support.DocumentStoreLifecycleSupport;
|
||||||
import tech.easyflow.ai.vo.FaqImportResultVo;
|
import tech.easyflow.ai.vo.FaqImportResultVo;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.filestorage.FileStorageService;
|
import tech.easyflow.common.filestorage.FileStorageService;
|
||||||
@@ -342,18 +343,22 @@ public class PublicKnowledgeShareController {
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
try {
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
||||||
|
documentStore.setEmbeddingModel(embeddingModel);
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
|
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
||||||
|
doc.setId(current.getId());
|
||||||
|
StoreResult result = documentStore.update(doc, options);
|
||||||
|
audit(apiKey, "API更新文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", documentChunk.getId()));
|
||||||
|
return Result.ok(result);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
EmbeddingModel embeddingModel = model.toEmbeddingModel();
|
|
||||||
documentStore.setEmbeddingModel(embeddingModel);
|
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
|
||||||
com.easyagents.core.document.Document doc = com.easyagents.core.document.Document.of(documentChunk.getContent());
|
|
||||||
doc.setId(current.getId());
|
|
||||||
StoreResult result = documentStore.update(doc, options);
|
|
||||||
audit(apiKey, "API更新文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", documentChunk.getId()));
|
|
||||||
return Result.ok(result);
|
|
||||||
}
|
}
|
||||||
return Result.ok(false);
|
return Result.ok(false);
|
||||||
}
|
}
|
||||||
@@ -376,16 +381,20 @@ public class PublicKnowledgeShareController {
|
|||||||
if (documentStore == null) {
|
if (documentStore == null) {
|
||||||
return Result.fail(2, "知识库没有配置向量库");
|
return Result.fail(2, "知识库没有配置向量库");
|
||||||
}
|
}
|
||||||
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
try {
|
||||||
if (model == null) {
|
Model model = modelService.getModelInstance(knowledge.getVectorEmbedModelId());
|
||||||
return Result.fail(3, "知识库没有配置向量模型");
|
if (model == null) {
|
||||||
|
return Result.fail(3, "知识库没有配置向量模型");
|
||||||
|
}
|
||||||
|
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||||
|
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
||||||
|
documentStore.delete(Collections.singletonList(chunkId), options);
|
||||||
|
documentChunkService.removeById(chunkId);
|
||||||
|
audit(apiKey, "API删除文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", chunkId));
|
||||||
|
return Result.ok(true);
|
||||||
|
} finally {
|
||||||
|
DocumentStoreLifecycleSupport.closeQuietly(documentStore);
|
||||||
}
|
}
|
||||||
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
|
||||||
StoreOptions options = StoreOptions.ofCollectionName(knowledge.getVectorStoreCollection());
|
|
||||||
documentStore.delete(Collections.singletonList(chunkId), options);
|
|
||||||
documentChunkService.removeById(chunkId);
|
|
||||||
audit(apiKey, "API删除文档 Chunk", "KNOWLEDGE_API_SHARE_WRITE", request.getRequestURI(), Map.of("knowledgeId", knowledgeId, "chunkId", chunkId));
|
|
||||||
return Result.ok(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -638,6 +647,10 @@ public class PublicKnowledgeShareController {
|
|||||||
for (com.easyagents.core.document.Document document : documents) {
|
for (com.easyagents.core.document.Document document : documents) {
|
||||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||||
item.setContent(document.getContent());
|
item.setContent(document.getContent());
|
||||||
|
Object renderMarkdown = document.getMetadata("renderMarkdown");
|
||||||
|
item.setRenderMarkdown(renderMarkdown == null ? null : String.valueOf(renderMarkdown));
|
||||||
|
Object sourceFileName = document.getMetadata("sourceFileName");
|
||||||
|
item.setSourceFileName(sourceFileName == null ? null : String.valueOf(sourceFileName));
|
||||||
item.setScore(document.getScore());
|
item.setScore(document.getScore());
|
||||||
Object hitSource = document.getMetadata("hitSource");
|
Object hitSource = document.getMetadata("hitSource");
|
||||||
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
item.setHitSource(hitSource == null ? null : String.valueOf(hitSource));
|
||||||
|
|||||||
@@ -2,27 +2,33 @@ package tech.easyflow.publicapi.controller;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
import tech.easyflow.approval.annotation.RequirePublishedAccess;
|
||||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.entity.WorkflowExecResult;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.service.WorkflowExecResultService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowApiPermissionService;
|
||||||
|
import tech.easyflow.ai.utils.WorkFlowUtil;
|
||||||
import tech.easyflow.common.constant.Constants;
|
import tech.easyflow.common.constant.Constants;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
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.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -41,13 +47,15 @@ public class PublicWorkflowController {
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
@Resource
|
||||||
|
private WorkflowApiPermissionService workflowApiPermissionService;
|
||||||
|
@Resource
|
||||||
|
private WorkflowExecResultService workflowExecResultService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过id或别名获取工作流详情
|
* 通过id或别名获取工作流详情
|
||||||
@@ -59,8 +67,11 @@ public class PublicWorkflowController {
|
|||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#key", denyMessage = "工作流尚未发布")
|
||||||
public Result<Workflow> getByIdOrAlias(
|
public Result<Workflow> getByIdOrAlias(
|
||||||
@RequestParam
|
@RequestParam
|
||||||
@NotBlank(message = "key不能为空") String key) {
|
@NotBlank(message = "key不能为空") String key,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
Workflow workflow = workflowService.getPublishedDetail(key);
|
Workflow workflow = workflowService.getPublishedDetail(key);
|
||||||
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.ok(workflow);
|
return Result.ok(workflow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +92,7 @@ public class PublicWorkflowController {
|
|||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -92,18 +104,20 @@ public class PublicWorkflowController {
|
|||||||
* 运行工作流 - v2
|
* 运行工作流 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/runAsync")
|
@PostMapping("/runAsync")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
|
||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
public Result<String> runAsync(@JsonBody(value = "id", required = true) BigInteger id,
|
||||||
@JsonBody("variables") Map<String, Object> variables) {
|
@JsonBody("variables") Map<String, Object> variables,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
Workflow workflow = workflowService.getPublishedById(id);
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
throw new RuntimeException("工作流不存在");
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
|
variables.put(Constants.LOGIN_USER_KEY, buildApiKeyLoginAccount(apiKey));
|
||||||
|
variables.put(WorkFlowUtil.CREATED_KEY_MEMORY_KEY, WorkFlowUtil.API_KEY);
|
||||||
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
String executeId = chainExecutor.executeAsync(PublishedWorkflowDefinitionIds.published(id.toString()), variables);
|
||||||
return Result.ok(executeId);
|
return Result.ok(executeId);
|
||||||
}
|
}
|
||||||
@@ -113,7 +127,10 @@ public class PublicWorkflowController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/getChainStatus")
|
@PostMapping("/getChainStatus")
|
||||||
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
public Result<ChainInfo> getChainStatus(@JsonBody(value = "executeId") String executeId,
|
||||||
@JsonBody("nodes") List<NodeInfo> nodes) {
|
@JsonBody("nodes") List<NodeInfo> nodes,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
ChainInfo res = tinyFlowService.getChainStatus(executeId, nodes);
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
@@ -122,34 +139,96 @@ public class PublicWorkflowController {
|
|||||||
* 恢复工作流运行 - v2
|
* 恢复工作流运行 - v2
|
||||||
*/
|
*/
|
||||||
@PostMapping("/resume")
|
@PostMapping("/resume")
|
||||||
@SaCheckPermission("/api/v1/workflow/save")
|
|
||||||
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
public Result<Void> resume(@JsonBody(value = "executeId", required = true) String executeId,
|
||||||
@JsonBody("confirmParams") Map<String, Object> confirmParams) {
|
@JsonBody("confirmParams") Map<String, Object> confirmParams,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
SysApiKey apiKey = workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
|
WorkflowExecResult execResult = assertApiKeyExecutionOwnership(apiKey, executeId);
|
||||||
|
assertWorkflowExecutionResumable(execResult);
|
||||||
chainExecutor.resumeAsync(executeId, confirmParams);
|
chainExecutor.resumeAsync(executeId, confirmParams);
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("getRunningParameters")
|
@GetMapping("getRunningParameters")
|
||||||
@SaCheckPermission("/api/v1/workflow/query")
|
|
||||||
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
@RequirePublishedAccess(resourceType = "WORKFLOW", idExpr = "#id", denyMessage = "工作流尚未发布")
|
||||||
public Result<?> getRunningParameters(@RequestParam BigInteger id) {
|
public Result<?> getRunningParameters(@RequestParam BigInteger id, HttpServletRequest request) {
|
||||||
|
workflowApiPermissionService.assertWorkflowApi(request.getHeader("ApiKey"), request.getRequestURI());
|
||||||
Workflow workflow = workflowService.getPublishedById(id);
|
Workflow workflow = workflowService.getPublishedById(id);
|
||||||
|
|
||||||
if (workflow == null) {
|
assertStrictPublishedWorkflow(workflow);
|
||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
|
||||||
}
|
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
return Result.fail(2, "节点配置错误,请检查! ");
|
||||||
}
|
}
|
||||||
List<Parameter> chainParameters = definition.getStartParameters();
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
|
||||||
res.put("parameters", chainParameters);
|
|
||||||
res.put("title", workflow.getTitle());
|
|
||||||
res.put("description", workflow.getDescription());
|
|
||||||
res.put("icon", workflow.getIcon());
|
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 API Key 调用方的运行身份。
|
||||||
|
*
|
||||||
|
* @param apiKey 访问令牌
|
||||||
|
* @return 工作流运行身份
|
||||||
|
*/
|
||||||
|
private LoginAccount buildApiKeyLoginAccount(SysApiKey apiKey) {
|
||||||
|
LoginAccount account = new LoginAccount();
|
||||||
|
account.setId(apiKey.getId());
|
||||||
|
account.setDeptId(apiKey.getDeptId() == null ? BigInteger.ZERO : apiKey.getDeptId());
|
||||||
|
account.setTenantId(apiKey.getTenantId() == null ? BigInteger.ZERO : apiKey.getTenantId());
|
||||||
|
account.setLoginName("apikey:" + apiKey.getId());
|
||||||
|
account.setNickname("API 调用方");
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验工作流 Public API 只能访问严格已发布且存在发布快照的工作流。
|
||||||
|
*
|
||||||
|
* @param workflow 工作流发布视图
|
||||||
|
*/
|
||||||
|
private void assertStrictPublishedWorkflow(Workflow workflow) {
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流尚未发布");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 Public Workflow API 后续操作只能作用于当前 API Key 发起的执行实例。
|
||||||
|
*
|
||||||
|
* @param apiKey 当前 API Key
|
||||||
|
* @param executeId 执行 ID
|
||||||
|
* @return 已通过归属校验的执行记录
|
||||||
|
*/
|
||||||
|
private WorkflowExecResult assertApiKeyExecutionOwnership(SysApiKey apiKey, String executeId) {
|
||||||
|
if (executeId == null || executeId.isBlank()) {
|
||||||
|
throw new BusinessException("执行ID不能为空");
|
||||||
|
}
|
||||||
|
WorkflowExecResult execResult = workflowExecResultService.getByExecKey(executeId);
|
||||||
|
if (execResult == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
if (!WorkFlowUtil.API_KEY.equals(execResult.getCreatedKey())
|
||||||
|
|| apiKey == null
|
||||||
|
|| apiKey.getId() == null
|
||||||
|
|| !String.valueOf(apiKey.getId()).equals(execResult.getCreatedBy())) {
|
||||||
|
throw new BusinessException("无权限访问当前工作流执行记录");
|
||||||
|
}
|
||||||
|
return execResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验当前执行实例是否仍允许恢复。
|
||||||
|
*
|
||||||
|
* @param execResult 执行记录
|
||||||
|
*/
|
||||||
|
private void assertWorkflowExecutionResumable(WorkflowExecResult execResult) {
|
||||||
|
if (execResult == null || execResult.getWorkflowId() == null) {
|
||||||
|
throw new BusinessException("工作流执行记录不存在,请稍后重试");
|
||||||
|
}
|
||||||
|
Workflow workflow = workflowService.getById(execResult.getWorkflowId());
|
||||||
|
if (workflow == null || !PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
|
||||||
|
|| workflow.getPublishedSnapshotJson() == null || workflow.getPublishedSnapshotJson().isEmpty()) {
|
||||||
|
throw new BusinessException("工作流已下线或不可恢复执行");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.service.*;
|
import tech.easyflow.ai.service.*;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
@@ -28,7 +29,9 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.chatlog.service.ChatRoundOperateService;
|
||||||
import tech.easyflow.core.runtime.ChatChannel;
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
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;
|
||||||
@@ -66,6 +69,8 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
private Cache<String, Object> cache;
|
private Cache<String, Object> cache;
|
||||||
@Resource
|
@Resource
|
||||||
private AudioServiceManager audioServiceManager;
|
private AudioServiceManager audioServiceManager;
|
||||||
|
@Resource
|
||||||
|
private ChatRoundOperateService chatRoundOperateService;
|
||||||
|
|
||||||
public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
public UcBotController(BotService service, ModelService modelService, BotWorkflowService botWorkflowService,
|
||||||
BotDocumentCollectionService botDocumentCollectionService) {
|
BotDocumentCollectionService botDocumentCollectionService) {
|
||||||
@@ -152,13 +157,17 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@JsonBody(value = "botId", required = true) BigInteger botId,
|
@JsonBody(value = "botId", required = true) BigInteger botId,
|
||||||
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
@JsonBody(value = "conversationId", required = true) BigInteger conversationId,
|
||||||
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
@JsonBody(value = "messages") List<Map<String, String>> messages,
|
||||||
@JsonBody(value = "attachments") List<String> attachments
|
@JsonBody(value = "attachments") List<String> attachments,
|
||||||
|
@JsonBody(value = "regenerateRoundId") BigInteger regenerateRoundId
|
||||||
|
|
||||||
) {
|
) {
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
chatRoundOperateService.requireRegeneratableRound(conversationId, regenerateRoundId);
|
||||||
|
}
|
||||||
|
|
||||||
// 前置校验:失败则直接返回错误SseEmitter
|
// 前置校验:失败则直接返回错误SseEmitter
|
||||||
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult);
|
SseEmitter errorEmitter = botService.checkChatBeforeStart(botId, prompt, conversationId.toString(), chatCheckResult, regenerateRoundId);
|
||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
return errorEmitter;
|
||||||
}
|
}
|
||||||
@@ -170,7 +179,7 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
messages,
|
messages,
|
||||||
chatCheckResult,
|
chatCheckResult,
|
||||||
attachments,
|
attachments,
|
||||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments)
|
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments, regenerateRoundId)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -286,25 +295,38 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments,
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
BigInteger regenerateRoundId) {
|
||||||
|
LoginAccount account = requireCurrentLoginAccount();
|
||||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||||
context.setChannel(ChatChannel.USER_CENTER);
|
context.setChannel(ChatChannel.USER_CENTER);
|
||||||
context.setSessionId(conversationId);
|
context.setSessionId(conversationId);
|
||||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
context.setTenantId(account.getTenantId());
|
||||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
context.setDeptId(account.getDeptId());
|
||||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
context.setUserId(account.getId());
|
||||||
context.setUserAccount(account == null ? "anonymous" : account.getLoginName());
|
context.setUserAccount(account.getLoginName());
|
||||||
context.setUserName(account == null ? "匿名用户" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
context.setUserName(StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName());
|
||||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||||
context.setAnonymous(account == null || BigInteger.ZERO.equals(account.getId()));
|
context.setAnonymous(false);
|
||||||
context.setAttachments(attachments);
|
context.setAttachments(attachments);
|
||||||
|
if (regenerateRoundId != null) {
|
||||||
|
context.getExt().put(ChatRuntimeExtKeys.REGENERATE_ROUND_ID, regenerateRoundId);
|
||||||
|
}
|
||||||
|
ChatTimeToolAvailabilityContext.bindLoggedInSnapshot(context, account, bot);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoginAccount requireCurrentLoginAccount() {
|
||||||
|
try {
|
||||||
|
return SaTokenUtil.getLoginAccount();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> getDefaultLlmOptions() {
|
private Map<String, Object> getDefaultLlmOptions() {
|
||||||
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
||||||
defaultLlmOptions.put("temperature", 0.7);
|
defaultLlmOptions.put("temperature", 0.7);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
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.chatlog.domain.dto.ChatHistoryPage;
|
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||||
|
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||||
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
|
||||||
@@ -17,6 +18,7 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/userCenter/chatHistory")
|
@RequestMapping("/userCenter/chatHistory")
|
||||||
@@ -61,4 +63,19 @@ public class UcChatHistoryController {
|
|||||||
chatHistoryManageService.deleteUserSession(account.getId(), sessionId, account.getId());
|
chatHistoryManageService.deleteUserSession(account.getId(), sessionId, account.getId());
|
||||||
return Result.ok();
|
return Result.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sessions/{sessionId}/rounds/{roundId}/variants")
|
||||||
|
public Result<List<ChatMessageRecord>> listRoundVariants(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.listUserRoundVariants(account.getId(), sessionId, roundId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sessions/{sessionId}/rounds/{roundId}/selectVariant")
|
||||||
|
public Result<ChatMessageRecord> selectRoundVariant(@PathVariable BigInteger sessionId,
|
||||||
|
@PathVariable BigInteger roundId,
|
||||||
|
@JsonBody(value = "variantIndex", required = true) Integer variantIndex) {
|
||||||
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
|
return Result.ok(chatHistoryManageService.selectUserRoundVariant(account.getId(), sessionId, roundId, variantIndex, account.getId()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,15 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
|||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
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 com.easyagents.flow.core.chain.ChainDefinition;
|
|
||||||
import com.easyagents.flow.core.chain.Parameter;
|
|
||||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||||
import com.easyagents.flow.core.parser.ChainParser;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
import tech.easyflow.ai.permission.WorkflowVisibilityQueryHelper;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.ChainInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
import tech.easyflow.ai.easyagentsflow.entity.NodeInfo;
|
||||||
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowDatacenterContentService;
|
|
||||||
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
import tech.easyflow.ai.easyagentsflow.service.TinyFlowService;
|
||||||
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
import tech.easyflow.ai.entity.Workflow;
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
@@ -48,13 +45,11 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
@Resource
|
@Resource
|
||||||
private ChainExecutor chainExecutor;
|
private ChainExecutor chainExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
private ChainParser chainParser;
|
|
||||||
@Resource
|
|
||||||
private TinyFlowService tinyFlowService;
|
private TinyFlowService tinyFlowService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowCheckService workflowCheckService;
|
private WorkflowCheckService workflowCheckService;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowDatacenterContentService workflowDatacenterContentService;
|
private WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
@Resource
|
@Resource
|
||||||
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
private WorkflowVisibilityQueryHelper workflowVisibilityQueryHelper;
|
||||||
|
|
||||||
@@ -86,6 +81,7 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
if (variables == null) {
|
if (variables == null) {
|
||||||
variables = new HashMap<>();
|
variables = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -115,6 +111,7 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
throw new RuntimeException("工作流不存在");
|
throw new RuntimeException("工作流不存在");
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
variables = workflowRunningParameterResolver.normalizeRuntimeVariables(workflow.getContent(), variables);
|
||||||
if (StpUtil.isLogin()) {
|
if (StpUtil.isLogin()) {
|
||||||
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
variables.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
@@ -176,17 +173,10 @@ public class UcWorkflowController extends BaseCurdController<WorkflowService, Wo
|
|||||||
return Result.fail(1, "can not find the workflow by id: " + id);
|
return Result.fail(1, "can not find the workflow by id: " + id);
|
||||||
}
|
}
|
||||||
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
workflowCheckService.checkOrThrow(workflow.getContent(), WorkflowCheckStage.PRE_EXECUTE, workflow.getId());
|
||||||
|
Map<String, Object> res = workflowRunningParameterResolver.buildRunningParametersView(workflow);
|
||||||
ChainDefinition definition = chainParser.parse(workflowDatacenterContentService.prepareContent(workflow.getContent()));
|
if (res == null) {
|
||||||
if (definition == null) {
|
|
||||||
return Result.fail(2, "节点配置错误,请检查! ");
|
return Result.fail(2, "节点配置错误,请检查! ");
|
||||||
}
|
}
|
||||||
List<Parameter> chainParameters = definition.getStartParameters();
|
|
||||||
Map<String, Object> res = new HashMap<>();
|
|
||||||
res.put("parameters", chainParameters);
|
|
||||||
res.put("title", workflow.getTitle());
|
|
||||||
res.put("description", workflow.getDescription());
|
|
||||||
res.put("icon", workflow.getIcon());
|
|
||||||
return Result.ok(res);
|
return Result.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,418 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
# EasyFlow Chat Protocol Specification v1.1
|
|
||||||
|
|
||||||
* **Protocol Name:** `easyflow-chat`
|
|
||||||
* **Version:** `1.1`
|
|
||||||
* **Status:** Draft / Recommended
|
|
||||||
* **Transport:** Server-Sent Events (SSE)
|
|
||||||
* **Encoding:** UTF-8
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1. 设计背景与目标
|
|
||||||
|
|
||||||
本协议用于描述 **EasyFlow 对话系统中的服务端事件流通信规范**,支持:
|
|
||||||
|
|
||||||
* AI 对话的 **流式输出**
|
|
||||||
* 模型 **思考过程(Thinking)**
|
|
||||||
* **工具调用(Tool Calling)**
|
|
||||||
* **系统 / 业务错误**
|
|
||||||
* **工作流 / Agent 状态**
|
|
||||||
* **对话中的用户交互(表单、确认等)**
|
|
||||||
* **中断与恢复(Suspend / Resume)**
|
|
||||||
|
|
||||||
设计目标:
|
|
||||||
|
|
||||||
* 前后端解耦
|
|
||||||
* 协议长期可扩展
|
|
||||||
* 不绑定具体模型厂商
|
|
||||||
* 易于与 Workflow / Agent / Chain 架构集成
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 2. 传输层规范(Transport)
|
|
||||||
|
|
||||||
* 使用 HTTP + SSE(支持未来扩展为其他协议,比如 WebSocket 等)
|
|
||||||
* Response Header:
|
|
||||||
|
|
||||||
```http
|
|
||||||
Content-Type: text/event-stream
|
|
||||||
Cache-Control: no-cache
|
|
||||||
Connection: keep-alive
|
|
||||||
```
|
|
||||||
* 通信方向:**Server → Client**
|
|
||||||
* 所有业务数据通过 `data` 字段传输,格式为 **JSON 字符串**
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 3. SSE Event 级别规范
|
|
||||||
|
|
||||||
### 3.1 Event Name(固定)
|
|
||||||
|
|
||||||
| event | 含义 |
|
|
||||||
| - |-------|
|
|
||||||
| message | 正常业务事件 |
|
|
||||||
| error | 错误事件 |
|
|
||||||
| done | 流结束事件 |
|
|
||||||
|
|
||||||
> ⚠️ **禁止在 event name 中承载业务语义**
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 4. 统一 Envelope 结构(核心)
|
|
||||||
|
|
||||||
### 4.1 基本结构
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"protocol": "easyflow-chat",
|
|
||||||
"version": "1.1",
|
|
||||||
"domain": "llm | tool | system | business | workflow | interaction | debug",
|
|
||||||
"type": "string",
|
|
||||||
"conversation_id": "string",
|
|
||||||
"message_id": "string",
|
|
||||||
"index": 0,
|
|
||||||
"payload": {},
|
|
||||||
"meta": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 4.2 字段说明
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 说明 |
|
|
||||||
| -- |---------| -- |------------------------|
|
|
||||||
| protocol | string | ✔ | 固定值 `easyflow-chat` |
|
|
||||||
| version | string | ✔ | 协议版本 |
|
|
||||||
| domain | string | ✔ | 事件所属领域 |
|
|
||||||
| type | string | ✔ | 领域内事件类型 |
|
|
||||||
| conversation_id | string | ✔ | 会话唯一标识 |
|
|
||||||
| message_id | string | ✖ | assistant 消息 ID |
|
|
||||||
| index | number | ✖ | 流式输出序号 |
|
|
||||||
| payload | object | ✔ | 事件数据 |
|
|
||||||
| meta | object | ✖ | 元信息(token、耗时等) |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 5. Domain 定义
|
|
||||||
|
|
||||||
| Domain | 说明 |
|
|
||||||
| -- | -- |
|
|
||||||
| llm | 模型语义输出 |
|
|
||||||
| tool | 工具调用与结果 |
|
|
||||||
| system | 系统级事件 |
|
|
||||||
| business | 业务规则 |
|
|
||||||
| workflow | 工作流 / Agent 状态 |
|
|
||||||
| interaction | 用户交互(表单等) |
|
|
||||||
| debug | 调试信息 |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 6. llm Domain
|
|
||||||
|
|
||||||
### 6.1 thinking
|
|
||||||
|
|
||||||
表示模型的思考过程。
|
|
||||||
|
|
||||||
#### 流式输出(delta)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "llm",
|
|
||||||
"type": "thinking",
|
|
||||||
"payload": {
|
|
||||||
"delta": "分析用户需求"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 完整输出(可选)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "llm",
|
|
||||||
"type": "message",
|
|
||||||
"payload": {
|
|
||||||
"content": "这是一个完整的回答"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 6.2 message
|
|
||||||
|
|
||||||
#### 流式输出(delta)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "llm",
|
|
||||||
"type": "message",
|
|
||||||
"index": 12,
|
|
||||||
"payload": {
|
|
||||||
"delta": "这是一个"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 完整输出(可选)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "llm",
|
|
||||||
"type": "message",
|
|
||||||
"payload": {
|
|
||||||
"content": "这是一个完整的回答"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 7. tool Domain
|
|
||||||
|
|
||||||
### 7.1 tool_call
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "tool",
|
|
||||||
"type": "tool_call",
|
|
||||||
"payload": {
|
|
||||||
"tool_call_id": "call_1",
|
|
||||||
"name": "search",
|
|
||||||
"arguments": {
|
|
||||||
"query": "SSE 协议设计"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 7.2 tool_result
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "tool",
|
|
||||||
"type": "tool_result",
|
|
||||||
"payload": {
|
|
||||||
"tool_call_id": "call_1",
|
|
||||||
"status": "success | error",
|
|
||||||
"result": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 8. system Domain
|
|
||||||
|
|
||||||
### 8.1 error
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "system",
|
|
||||||
"type": "error",
|
|
||||||
"payload": {
|
|
||||||
"code": "MODEL_CONFIG_INVALID",
|
|
||||||
"message": "模型配置错误",
|
|
||||||
"retryable": false,
|
|
||||||
"detail": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 8.2 status
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "system",
|
|
||||||
"type": "status",
|
|
||||||
"payload": {
|
|
||||||
"state": "initializing | running | suspended | resumed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 9. business Domain
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "business",
|
|
||||||
"type": "error",
|
|
||||||
"payload": {
|
|
||||||
"code": "QUOTA_EXCEEDED",
|
|
||||||
"message": "配额不足"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 10. workflow Domain
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "workflow",
|
|
||||||
"type": "status",
|
|
||||||
"payload": {
|
|
||||||
"node_id": "node_1",
|
|
||||||
"state": "start | suspend | resume | end",
|
|
||||||
"reason": "interaction"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 11. interaction Domain(对话内交互)
|
|
||||||
|
|
||||||
### 11.1 form_request
|
|
||||||
|
|
||||||
表示请求用户填写表单,对话进入挂起状态。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "interaction",
|
|
||||||
"type": "form_request",
|
|
||||||
"payload": {
|
|
||||||
"form_id": "user_info_form",
|
|
||||||
"title": "补充信息",
|
|
||||||
"description": "请填写以下信息以继续",
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["age", "email"],
|
|
||||||
"properties": {
|
|
||||||
"age": {
|
|
||||||
"type": "number",
|
|
||||||
"title": "年龄"
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "邮箱",
|
|
||||||
"format": "email"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"submit_text": "继续",
|
|
||||||
"cancel_text": "取消"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> 表单 schema **符合 JSON Schema 标准**
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 11.2 form_cancel
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "interaction",
|
|
||||||
"type": "form_cancel",
|
|
||||||
"payload": {
|
|
||||||
"form_id": "user_info_form"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 12. 表单提交与恢复(非 SSE)
|
|
||||||
|
|
||||||
表单提交通过 **普通 HTTP / WebSocket 请求**:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"conversation_id": "conv_1",
|
|
||||||
"form_id": "user_info_form",
|
|
||||||
"values": {
|
|
||||||
"age": 30,
|
|
||||||
"email": "a@b.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功后服务端恢复 SSE 流。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 13. done 事件(流结束)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"domain": "system",
|
|
||||||
"type": "done",
|
|
||||||
"meta": {
|
|
||||||
"prompt_tokens": 1234,
|
|
||||||
"completion_tokens": 456,
|
|
||||||
"latency_ms": 2300
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 14. 错误处理规则
|
|
||||||
|
|
||||||
* 收到 `event: error` 后客户端应终止流
|
|
||||||
* 错误语义由:
|
|
||||||
|
|
||||||
```
|
|
||||||
domain + type + payload.code
|
|
||||||
```
|
|
||||||
|
|
||||||
共同决定
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 15. 状态机视角(推荐)
|
|
||||||
|
|
||||||
```text
|
|
||||||
RUNNING
|
|
||||||
↓
|
|
||||||
LLM_OUTPUT
|
|
||||||
↓
|
|
||||||
INTERACTION_REQUESTED
|
|
||||||
↓
|
|
||||||
SUSPENDED
|
|
||||||
↓
|
|
||||||
FORM_SUBMITTED
|
|
||||||
↓
|
|
||||||
RESUMED
|
|
||||||
↓
|
|
||||||
RUNNING
|
|
||||||
↓
|
|
||||||
DONE
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 16. 扩展与兼容规则
|
|
||||||
|
|
||||||
1. 可新增 domain
|
|
||||||
2. 可新增 type
|
|
||||||
3. 不允许删除已有字段
|
|
||||||
4. payload 可自由扩展
|
|
||||||
5. 1.x 版本保持向后兼容
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 17. 设计原则
|
|
||||||
|
|
||||||
> * SSE 只负责事件流
|
|
||||||
> * domain 定义责任边界
|
|
||||||
> * type 定义语义动作
|
|
||||||
> * payload 定义数据结构
|
|
||||||
> * 前端不依赖 event name 判断业务,不依赖协议本身,支持其他协议的扩展
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -25,6 +25,11 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-actuator</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.clickhouse</groupId>
|
<groupId>com.clickhouse</groupId>
|
||||||
<artifactId>clickhouse-jdbc</artifactId>
|
<artifactId>clickhouse-jdbc</artifactId>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package tech.easyflow.common.analyticaldb.support;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.health.Health;
|
||||||
|
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析数据库健康检查。
|
||||||
|
*/
|
||||||
|
@Component("analyticalDbHealthIndicator")
|
||||||
|
public class AnalyticalDBHealthIndicator implements HealthIndicator {
|
||||||
|
|
||||||
|
private final AnalyticalDBHealthSupport healthSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建分析数据库健康检查器。
|
||||||
|
*
|
||||||
|
* @param healthSupport 分析数据库健康检查支持
|
||||||
|
*/
|
||||||
|
public AnalyticalDBHealthIndicator(AnalyticalDBHealthSupport healthSupport) {
|
||||||
|
this.healthSupport = healthSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查分析数据库是否可用。
|
||||||
|
*
|
||||||
|
* @return 健康状态
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Health health() {
|
||||||
|
if (!healthSupport.enabled()) {
|
||||||
|
return Health.up().withDetail("enabled", false).build();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
healthSupport.selfCheck();
|
||||||
|
return Health.up().withDetail("enabled", true).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Health.down(e).withDetail("enabled", true).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package tech.easyflow.common.audio.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音频模块线程池配置。
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "easyflow.thread-pool.scheduler")
|
||||||
|
public class AudioThreadPoolProperties {
|
||||||
|
|
||||||
|
private int poolSize = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调度线程池大小。
|
||||||
|
*
|
||||||
|
* @return 调度线程池大小
|
||||||
|
*/
|
||||||
|
public int getPoolSize() {
|
||||||
|
return poolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置调度线程池大小。
|
||||||
|
*
|
||||||
|
* @param poolSize 调度线程池大小
|
||||||
|
*/
|
||||||
|
public void setPoolSize(int poolSize) {
|
||||||
|
this.poolSize = poolSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,38 @@
|
|||||||
package tech.easyflow.common.audio.socket;
|
package tech.easyflow.common.audio.socket;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.scheduling.TaskScheduler;
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
import tech.easyflow.common.audio.config.AudioThreadPoolProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@EnableConfigurationProperties(AudioThreadPoolProperties.class)
|
||||||
public class SchedulingConfig {
|
public class SchedulingConfig {
|
||||||
|
|
||||||
|
private final AudioThreadPoolProperties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建音频调度配置。
|
||||||
|
*
|
||||||
|
* @param properties 音频调度线程池配置
|
||||||
|
*/
|
||||||
|
public SchedulingConfig(AudioThreadPoolProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建调度线程池。
|
||||||
|
*
|
||||||
|
* @return 调度线程池
|
||||||
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
public TaskScheduler taskScheduler() {
|
public TaskScheduler taskScheduler() {
|
||||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
scheduler.setPoolSize(10);
|
scheduler.setPoolSize(properties.getPoolSize());
|
||||||
scheduler.setThreadNamePrefix("scheduled-task-");
|
scheduler.setThreadNamePrefix("scheduled-task-");
|
||||||
scheduler.setDaemon(true);
|
scheduler.setDaemon(true);
|
||||||
scheduler.initialize();
|
scheduler.initialize();
|
||||||
|
|||||||
@@ -39,7 +39,23 @@
|
|||||||
<artifactId>fastjson</artifactId>
|
<artifactId>fastjson</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.12.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package tech.easyflow.common.cache;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring 定时任务 Redis 分布式锁。
|
||||||
|
*/
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface DistributedScheduledLock {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取锁使用的 Redis key。
|
||||||
|
*
|
||||||
|
* @return Redis 锁 key
|
||||||
|
*/
|
||||||
|
String key();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待锁的秒数。
|
||||||
|
*
|
||||||
|
* @return 等待锁的秒数
|
||||||
|
*/
|
||||||
|
long waitSeconds() default 0L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁租约秒数。
|
||||||
|
*
|
||||||
|
* @return 锁租约秒数
|
||||||
|
*/
|
||||||
|
long leaseSeconds() default 300L;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package tech.easyflow.common.cache;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务分布式锁切面。
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class DistributedScheduledLockAspect {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DistributedScheduledLockAspect.class);
|
||||||
|
|
||||||
|
private final RedisLockExecutor redisLockExecutor;
|
||||||
|
private final ScheduledExecutorService renewExecutor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时任务分布式锁切面。
|
||||||
|
*
|
||||||
|
* @param redisLockExecutor Redis 分布式锁执行器
|
||||||
|
*/
|
||||||
|
public DistributedScheduledLockAspect(RedisLockExecutor redisLockExecutor) {
|
||||||
|
this.redisLockExecutor = redisLockExecutor;
|
||||||
|
this.renewExecutor = Executors.newScheduledThreadPool(
|
||||||
|
1,
|
||||||
|
new DistributedScheduledLockThreadFactory()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截带分布式调度锁的定时任务。
|
||||||
|
*
|
||||||
|
* @param joinPoint 切点
|
||||||
|
* @param lock 锁注解
|
||||||
|
* @return 原方法返回值;未抢到锁时返回 null
|
||||||
|
* @throws Throwable 原方法执行异常或 Redis 访问异常
|
||||||
|
*/
|
||||||
|
@Around("@annotation(lock)")
|
||||||
|
public Object around(ProceedingJoinPoint joinPoint, DistributedScheduledLock lock) throws Throwable {
|
||||||
|
Duration waitTimeout = Duration.ofSeconds(Math.max(lock.waitSeconds(), 0L));
|
||||||
|
Duration leaseTimeout = Duration.ofSeconds(Math.max(lock.leaseSeconds(), 1L));
|
||||||
|
RedisLockExecutor.LockHandle handle = redisLockExecutor.tryAcquire(lock.key(), waitTimeout, leaseTimeout);
|
||||||
|
if (handle == null) {
|
||||||
|
LOG.info("定时任务分布式锁已被其他实例持有,跳过本轮执行: lockKey={}, method={}",
|
||||||
|
lock.key(), joinPoint.getSignature().toShortString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ScheduledFuture<?> renewTask = scheduleRenew(lock.key(), handle, leaseTimeout);
|
||||||
|
try {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
} finally {
|
||||||
|
renewTask.cancel(false);
|
||||||
|
handle.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScheduledFuture<?> scheduleRenew(String lockKey,
|
||||||
|
RedisLockExecutor.LockHandle handle,
|
||||||
|
Duration leaseTimeout) {
|
||||||
|
long renewIntervalMillis = Math.max(leaseTimeout.toMillis() / 3L, 1000L);
|
||||||
|
return renewExecutor.scheduleWithFixedDelay(() -> {
|
||||||
|
if (!handle.renew()) {
|
||||||
|
LOG.warn("定时任务分布式锁续期失败: lockKey={}", lockKey);
|
||||||
|
}
|
||||||
|
}, renewIntervalMillis, renewIntervalMillis, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭调度锁续期线程池。
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
renewExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调度锁续期线程工厂。
|
||||||
|
*/
|
||||||
|
private static final class DistributedScheduledLockThreadFactory implements ThreadFactory {
|
||||||
|
|
||||||
|
private final AtomicInteger index = new AtomicInteger(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建续期线程。
|
||||||
|
*
|
||||||
|
* @param runnable 线程任务
|
||||||
|
* @return 续期线程
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable runnable) {
|
||||||
|
Thread thread = new Thread(runnable);
|
||||||
|
thread.setName("distributed-scheduled-lock-renew-" + index.getAndIncrement());
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ import java.util.Collections;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 分布式锁执行器。
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class RedisLockExecutor {
|
public class RedisLockExecutor {
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ public class RedisLockExecutor {
|
|||||||
private static final long RETRY_INTERVAL_MILLIS = 50L;
|
private static final long RETRY_INTERVAL_MILLIS = 50L;
|
||||||
|
|
||||||
private static final DefaultRedisScript<Long> RELEASE_LOCK_SCRIPT;
|
private static final DefaultRedisScript<Long> RELEASE_LOCK_SCRIPT;
|
||||||
|
private static final DefaultRedisScript<Long> RENEW_LOCK_SCRIPT;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
RELEASE_LOCK_SCRIPT = new DefaultRedisScript<>();
|
RELEASE_LOCK_SCRIPT = new DefaultRedisScript<>();
|
||||||
@@ -29,11 +33,26 @@ public class RedisLockExecutor {
|
|||||||
"else return 0 end"
|
"else return 0 end"
|
||||||
);
|
);
|
||||||
RELEASE_LOCK_SCRIPT.setResultType(Long.class);
|
RELEASE_LOCK_SCRIPT.setResultType(Long.class);
|
||||||
|
RENEW_LOCK_SCRIPT = new DefaultRedisScript<>();
|
||||||
|
RENEW_LOCK_SCRIPT.setScriptText(
|
||||||
|
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
|
||||||
|
"return redis.call('pexpire', KEYS[1], ARGV[2]) " +
|
||||||
|
"else return 0 end"
|
||||||
|
);
|
||||||
|
RENEW_LOCK_SCRIPT.setResultType(Long.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在分布式锁保护下执行无返回任务。
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param waitTimeout 等待锁的最大时间
|
||||||
|
* @param leaseTimeout 锁租约时间
|
||||||
|
* @param task 业务任务
|
||||||
|
*/
|
||||||
public void executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Runnable task) {
|
public void executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Runnable task) {
|
||||||
executeWithLock(lockKey, waitTimeout, leaseTimeout, () -> {
|
executeWithLock(lockKey, waitTimeout, leaseTimeout, () -> {
|
||||||
task.run();
|
task.run();
|
||||||
@@ -41,40 +60,156 @@ public class RedisLockExecutor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在分布式锁保护下执行有返回任务。
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param waitTimeout 等待锁的最大时间
|
||||||
|
* @param leaseTimeout 锁租约时间
|
||||||
|
* @param task 业务任务
|
||||||
|
* @param <T> 返回类型
|
||||||
|
* @return 任务返回值
|
||||||
|
*/
|
||||||
public <T> T executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Supplier<T> task) {
|
public <T> T executeWithLock(String lockKey, Duration waitTimeout, Duration leaseTimeout, Supplier<T> task) {
|
||||||
|
LockHandle handle = acquire(lockKey, waitTimeout, leaseTimeout);
|
||||||
|
try {
|
||||||
|
return task.get();
|
||||||
|
} finally {
|
||||||
|
handle.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取显式释放的分布式锁句柄。
|
||||||
|
*
|
||||||
|
* <p>长连接、SSE 或异步任务不能使用 callback 型锁,否则 callback 返回后锁会被提前释放。
|
||||||
|
* 该方法返回 owner token 绑定的句柄,由调用方在运行完成、失败或取消时显式释放。</p>
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param waitTimeout 等待时间
|
||||||
|
* @param leaseTimeout 租约时间
|
||||||
|
* @return 锁句柄
|
||||||
|
*/
|
||||||
|
public LockHandle acquire(String lockKey, Duration waitTimeout, Duration leaseTimeout) {
|
||||||
|
LockHandle handle = tryAcquire(lockKey, waitTimeout, leaseTimeout);
|
||||||
|
if (handle == null) {
|
||||||
|
throw new IllegalStateException("获取分布式锁失败,请稍后重试,lockKey=" + lockKey);
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试获取显式释放的分布式锁句柄。
|
||||||
|
*
|
||||||
|
* <p>返回 {@code null} 表示锁当前被其他节点持有。Redis 访问失败或等待过程被中断仍会抛出异常,
|
||||||
|
* 调用方可据此区分“正常跳过”和“基础设施异常”。</p>
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param waitTimeout 等待时间
|
||||||
|
* @param leaseTimeout 租约时间
|
||||||
|
* @return 获取成功时返回锁句柄,否则返回 null
|
||||||
|
*/
|
||||||
|
public LockHandle tryAcquire(String lockKey, Duration waitTimeout, Duration leaseTimeout) {
|
||||||
String lockValue = UUID.randomUUID().toString();
|
String lockValue = UUID.randomUUID().toString();
|
||||||
boolean acquired = false;
|
boolean acquired = false;
|
||||||
long deadline = System.nanoTime() + waitTimeout.toNanos();
|
long deadline = System.nanoTime() + waitTimeout.toNanos();
|
||||||
try {
|
try {
|
||||||
while (System.nanoTime() <= deadline) {
|
do {
|
||||||
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, leaseTimeout);
|
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, leaseTimeout);
|
||||||
if (Boolean.TRUE.equals(success)) {
|
if (Boolean.TRUE.equals(success)) {
|
||||||
acquired = true;
|
acquired = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (System.nanoTime() >= deadline) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
Thread.sleep(RETRY_INTERVAL_MILLIS);
|
Thread.sleep(RETRY_INTERVAL_MILLIS);
|
||||||
}
|
} while (System.nanoTime() <= deadline);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new IllegalStateException("等待分布式锁被中断,lockKey=" + lockKey, e);
|
throw new IllegalStateException("等待分布式锁被中断,lockKey=" + lockKey, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acquired) {
|
if (!acquired) {
|
||||||
throw new IllegalStateException("获取分布式锁失败,请稍后重试,lockKey=" + lockKey);
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return task.get();
|
|
||||||
} finally {
|
|
||||||
releaseLock(lockKey, lockValue);
|
|
||||||
}
|
}
|
||||||
|
return new LockHandle(lockKey, lockValue, leaseTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseLock(String lockKey, String lockValue) {
|
/**
|
||||||
|
* 按 owner token 释放锁。
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param lockValue owner token
|
||||||
|
*/
|
||||||
|
public void releaseLock(String lockKey, String lockValue) {
|
||||||
try {
|
try {
|
||||||
stringRedisTemplate.execute(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), lockValue);
|
stringRedisTemplate.execute(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey), lockValue);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("释放分布式锁失败,lockKey={}", lockKey, e);
|
log.warn("释放分布式锁失败,lockKey={}", lockKey, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 owner token 续期锁。
|
||||||
|
*
|
||||||
|
* @param lockKey 锁 key
|
||||||
|
* @param lockValue owner token
|
||||||
|
* @param leaseTimeout 新租约时间
|
||||||
|
* @return 续期成功时为 true
|
||||||
|
*/
|
||||||
|
public boolean renewLock(String lockKey, String lockValue, Duration leaseTimeout) {
|
||||||
|
try {
|
||||||
|
Long result = stringRedisTemplate.execute(RENEW_LOCK_SCRIPT, Collections.singletonList(lockKey),
|
||||||
|
lockValue, String.valueOf(leaseTimeout.toMillis()));
|
||||||
|
return Long.valueOf(1L).equals(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("续期分布式锁失败,lockKey={}", lockKey, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显式分布式锁句柄。
|
||||||
|
*/
|
||||||
|
public final class LockHandle implements AutoCloseable {
|
||||||
|
|
||||||
|
private final String lockKey;
|
||||||
|
private final String lockValue;
|
||||||
|
private final Duration leaseTimeout;
|
||||||
|
private volatile boolean released;
|
||||||
|
|
||||||
|
private LockHandle(String lockKey, String lockValue, Duration leaseTimeout) {
|
||||||
|
this.lockKey = lockKey;
|
||||||
|
this.lockValue = lockValue;
|
||||||
|
this.leaseTimeout = leaseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续期当前锁。
|
||||||
|
*
|
||||||
|
* @return 续期成功时为 true
|
||||||
|
*/
|
||||||
|
public boolean renew() {
|
||||||
|
if (released) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return renewLock(lockKey, lockValue, leaseTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放当前锁。
|
||||||
|
*/
|
||||||
|
public void release() {
|
||||||
|
if (released) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
released = true;
|
||||||
|
releaseLock(lockKey, lockValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package tech.easyflow.common.cache;
|
||||||
|
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.Signature;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
import org.springframework.data.redis.core.script.RedisScript;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link DistributedScheduledLockAspect} 回归测试。
|
||||||
|
*/
|
||||||
|
public class DistributedScheduledLockAspectTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证未抢到调度锁时跳过原方法。
|
||||||
|
*
|
||||||
|
* @throws Throwable 切面执行异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void aroundShouldSkipTaskWhenLockIsHeld() throws Throwable {
|
||||||
|
RedisLockExecutor executor = createExecutor(false);
|
||||||
|
DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
|
||||||
|
AtomicInteger proceedCount = new AtomicInteger();
|
||||||
|
|
||||||
|
Object result = aspect.around(
|
||||||
|
mockJoinPoint(proceedCount),
|
||||||
|
annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertNull(result);
|
||||||
|
Assert.assertEquals(0, proceedCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证抢到调度锁时执行原方法并释放锁。
|
||||||
|
*
|
||||||
|
* @throws Throwable 切面执行异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void aroundShouldProceedAndReleaseWhenLockAcquired() throws Throwable {
|
||||||
|
RedisLockExecutor executor = createExecutor(true);
|
||||||
|
DistributedScheduledLockAspect aspect = new DistributedScheduledLockAspect(executor);
|
||||||
|
AtomicInteger proceedCount = new AtomicInteger();
|
||||||
|
|
||||||
|
Object result = aspect.around(
|
||||||
|
mockJoinPoint(proceedCount),
|
||||||
|
annotatedMethod("lockedTask").getAnnotation(DistributedScheduledLock.class)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertEquals("ok", result);
|
||||||
|
Assert.assertEquals(1, proceedCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DistributedScheduledLock(key = "easyflow:test:scheduled", leaseSeconds = 30L)
|
||||||
|
private void lockedTask() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Method annotatedMethod(String methodName) throws NoSuchMethodException {
|
||||||
|
Method method = DistributedScheduledLockAspectTest.class.getDeclaredMethod(methodName);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProceedingJoinPoint mockJoinPoint(AtomicInteger proceedCount) throws Throwable {
|
||||||
|
ProceedingJoinPoint joinPoint = Mockito.mock(ProceedingJoinPoint.class);
|
||||||
|
Signature signature = Mockito.mock(Signature.class);
|
||||||
|
Mockito.when(signature.toShortString()).thenReturn("lockedTask()");
|
||||||
|
Mockito.when(joinPoint.getSignature()).thenReturn(signature);
|
||||||
|
Mockito.when(joinPoint.proceed()).thenAnswer(invocation -> {
|
||||||
|
proceedCount.incrementAndGet();
|
||||||
|
return "ok";
|
||||||
|
});
|
||||||
|
return joinPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private RedisLockExecutor createExecutor(boolean acquired) throws Exception {
|
||||||
|
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||||
|
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
|
||||||
|
Mockito.when(valueOperations.setIfAbsent(
|
||||||
|
ArgumentMatchers.anyString(),
|
||||||
|
ArgumentMatchers.anyString(),
|
||||||
|
ArgumentMatchers.any(Duration.class)
|
||||||
|
)).thenReturn(acquired);
|
||||||
|
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
Mockito.when(redisTemplate.execute(
|
||||||
|
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||||
|
ArgumentMatchers.<List<String>>any(),
|
||||||
|
ArgumentMatchers.<Object[]>any()
|
||||||
|
)).thenReturn(1L);
|
||||||
|
|
||||||
|
RedisLockExecutor executor = new RedisLockExecutor();
|
||||||
|
Field field = RedisLockExecutor.class.getDeclaredField("stringRedisTemplate");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(executor, redisTemplate);
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package tech.easyflow.common.cache;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.ValueOperations;
|
||||||
|
import org.springframework.data.redis.core.script.RedisScript;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RedisLockExecutor} 回归测试。
|
||||||
|
*/
|
||||||
|
public class RedisLockExecutorTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证锁被占用时返回 null,便于调度任务跳过本轮执行。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void tryAcquireShouldReturnNullWhenLockIsHeld() throws Exception {
|
||||||
|
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||||
|
ValueOperations<String, String> valueOperations = mockValueOperations(false);
|
||||||
|
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
|
||||||
|
RedisLockExecutor executor = new RedisLockExecutor();
|
||||||
|
setRedisTemplate(executor, redisTemplate);
|
||||||
|
|
||||||
|
RedisLockExecutor.LockHandle handle = executor.tryAcquire(
|
||||||
|
"easyflow:test:lock",
|
||||||
|
Duration.ZERO,
|
||||||
|
Duration.ofSeconds(30)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertNull(handle);
|
||||||
|
Mockito.verify(valueOperations).setIfAbsent(
|
||||||
|
ArgumentMatchers.eq("easyflow:test:lock"),
|
||||||
|
ArgumentMatchers.anyString(),
|
||||||
|
ArgumentMatchers.eq(Duration.ofSeconds(30))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证锁获取成功后释放会执行 owner token 校验脚本。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void acquiredHandleShouldReleaseLockWithOwnerToken() throws Exception {
|
||||||
|
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||||
|
ValueOperations<String, String> valueOperations = mockValueOperations(true);
|
||||||
|
Mockito.when(redisTemplate.opsForValue()).thenReturn(valueOperations);
|
||||||
|
Mockito.when(redisTemplate.execute(
|
||||||
|
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||||
|
ArgumentMatchers.<List<String>>any(),
|
||||||
|
ArgumentMatchers.<Object[]>any()
|
||||||
|
)).thenReturn(1L);
|
||||||
|
|
||||||
|
RedisLockExecutor executor = new RedisLockExecutor();
|
||||||
|
setRedisTemplate(executor, redisTemplate);
|
||||||
|
|
||||||
|
RedisLockExecutor.LockHandle handle = executor.tryAcquire(
|
||||||
|
"easyflow:test:lock",
|
||||||
|
Duration.ZERO,
|
||||||
|
Duration.ofSeconds(30)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertNotNull(handle);
|
||||||
|
handle.release();
|
||||||
|
Mockito.verify(redisTemplate).execute(
|
||||||
|
ArgumentMatchers.<RedisScript<Long>>any(),
|
||||||
|
ArgumentMatchers.eq(List.of("easyflow:test:lock")),
|
||||||
|
ArgumentMatchers.<Object[]>any()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private ValueOperations<String, String> mockValueOperations(boolean acquired) {
|
||||||
|
ValueOperations<String, String> valueOperations = Mockito.mock(ValueOperations.class);
|
||||||
|
Mockito.when(valueOperations.setIfAbsent(
|
||||||
|
ArgumentMatchers.anyString(),
|
||||||
|
ArgumentMatchers.anyString(),
|
||||||
|
ArgumentMatchers.any(Duration.class)
|
||||||
|
)).thenReturn(acquired);
|
||||||
|
return valueOperations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRedisTemplate(RedisLockExecutor executor, StringRedisTemplate redisTemplate) throws Exception {
|
||||||
|
Field field = RedisLockExecutor.class.getDeclaredField("stringRedisTemplate");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(executor, redisTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ public enum ChatType {
|
|||||||
TOOL_CALL,
|
TOOL_CALL,
|
||||||
TOOL_RESULT,
|
TOOL_RESULT,
|
||||||
STATUS,
|
STATUS,
|
||||||
|
CITATIONS,
|
||||||
|
SESSION_CREATED,
|
||||||
ERROR,
|
ERROR,
|
||||||
FORM_REQUEST,
|
FORM_REQUEST,
|
||||||
FORM_CANCEL,
|
FORM_CANCEL,
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import tech.easyflow.common.util.StringUtil;
|
|
||||||
import tech.easyflow.common.util.SpringContextUtil;
|
import tech.easyflow.common.util.SpringContextUtil;
|
||||||
|
import tech.easyflow.common.util.StringUtil;
|
||||||
import tech.easyflow.core.chat.protocol.ChatEnvelope;
|
import tech.easyflow.core.chat.protocol.ChatEnvelope;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -90,15 +91,21 @@ public class ChatSseEmitter {
|
|||||||
.data(json)
|
.data(json)
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (IllegalStateException e) {
|
} catch (AsyncRequestNotUsableException e) {
|
||||||
closed.compareAndSet(false, true);
|
markDisconnected(event, e);
|
||||||
LOG.error("ChatSseEmitter send failed(event={}), message={}, exception={}", event, e.getMessage(), e.toString(), e);
|
|
||||||
return false;
|
return false;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("ChatSseEmitter send io failed(event={}), message={}, exception={}", event, e.getMessage(), e.toString(), e);
|
markDisconnected(event, e);
|
||||||
safeCompleteWithError(e);
|
return false;
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
closed.compareAndSet(false, true);
|
||||||
|
LOG.warn("ChatSseEmitter send failed(event={}), message={}, exception={}", event, e.getMessage(), e.toString());
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (isClientDisconnected(e)) {
|
||||||
|
markDisconnected(event, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
LOG.error("ChatSseEmitter send unexpected failed(event={}), message={}, exception={}", event, e.getMessage(), e.toString(), e);
|
LOG.error("ChatSseEmitter send unexpected failed(event={}), message={}, exception={}", event, e.getMessage(), e.toString(), e);
|
||||||
safeCompleteWithError(e);
|
safeCompleteWithError(e);
|
||||||
return false;
|
return false;
|
||||||
@@ -165,4 +172,31 @@ public class ChatSseEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void markDisconnected(String event, Throwable ex) {
|
||||||
|
closed.compareAndSet(false, true);
|
||||||
|
LOG.warn("ChatSseEmitter client disconnected(event={}), message={}, exception={}",
|
||||||
|
event, ex == null ? null : ex.getMessage(), ex == null ? null : ex.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isClientDisconnected(Throwable ex) {
|
||||||
|
Throwable current = ex;
|
||||||
|
while (current != null) {
|
||||||
|
if (current instanceof AsyncRequestNotUsableException || current instanceof IOException) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String message = current.getMessage();
|
||||||
|
if (message != null) {
|
||||||
|
String lowerMessage = message.toLowerCase();
|
||||||
|
if (lowerMessage.contains("broken pipe")
|
||||||
|
|| lowerMessage.contains("connection reset")
|
||||||
|
|| lowerMessage.contains("disconnected client")
|
||||||
|
|| lowerMessage.contains("response not usable")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current = current.getCause();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,55 +5,163 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 负责聚合单轮聊天中的 assistant thinking、tool 调用和最终回答,
|
||||||
|
* 并产出可用于历史回放的结构化 payload。
|
||||||
|
*/
|
||||||
public class ChatAssistantAccumulator {
|
public class ChatAssistantAccumulator {
|
||||||
|
|
||||||
private final StringBuilder content = new StringBuilder();
|
private final StringBuilder content = new StringBuilder();
|
||||||
private final StringBuilder reasoning = new StringBuilder();
|
private final StringBuilder reasoning = new StringBuilder();
|
||||||
|
private final StringBuilder displayReasoning = new StringBuilder();
|
||||||
private final List<Map<String, Object>> chains = new ArrayList<>();
|
private final List<Map<String, Object>> chains = new ArrayList<>();
|
||||||
|
private final List<Map<String, Object>> messageChain = new ArrayList<>();
|
||||||
|
private final List<Map<String, Object>> toolMessages = new ArrayList<>();
|
||||||
|
private Map<String, Object> latestToolCallAssistant;
|
||||||
|
private boolean toolCallBatchOpen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加当前 assistant 片段的文本内容。
|
||||||
|
*
|
||||||
|
* @param delta 内容增量
|
||||||
|
*/
|
||||||
public void appendContent(String delta) {
|
public void appendContent(String delta) {
|
||||||
if (delta != null && !delta.isEmpty()) {
|
if (delta != null && !delta.isEmpty()) {
|
||||||
content.append(delta);
|
content.append(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加当前 assistant 片段的 reasoning 内容。
|
||||||
|
*
|
||||||
|
* @param delta reasoning 增量
|
||||||
|
*/
|
||||||
public void appendReasoning(String delta) {
|
public void appendReasoning(String delta) {
|
||||||
if (delta != null && !delta.isEmpty()) {
|
if (delta != null && !delta.isEmpty()) {
|
||||||
reasoning.append(delta);
|
reasoning.append(delta);
|
||||||
|
displayReasoning.append(delta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 tool call,同时把当前 assistant 片段固化为一条结构化 assistant 消息。
|
||||||
|
*
|
||||||
|
* @param id tool call id
|
||||||
|
* @param name tool 名称
|
||||||
|
* @param arguments tool 参数
|
||||||
|
*/
|
||||||
public void appendToolCall(String id, String name, Object arguments) {
|
public void appendToolCall(String id, String name, Object arguments) {
|
||||||
Map<String, Object> chain = findToolChain(id, name);
|
appendToolCall(id, name, null, arguments);
|
||||||
chain.put("status", "TOOL_CALL");
|
|
||||||
chain.put("result", arguments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 tool call,同时保留面向前端展示的工具名称。
|
||||||
|
*
|
||||||
|
* @param id tool call id
|
||||||
|
* @param name tool 名称
|
||||||
|
* @param displayName tool 展示名称
|
||||||
|
* @param arguments tool 参数
|
||||||
|
*/
|
||||||
|
public void appendToolCall(String id, String name, String displayName, Object arguments) {
|
||||||
|
Map<String, Object> chain = findToolChain(id, name);
|
||||||
|
chain.put("status", "TOOL_CALL");
|
||||||
|
chain.put("arguments", arguments);
|
||||||
|
putIfNotBlank(chain, "toolDisplayName", displayName);
|
||||||
|
|
||||||
|
Map<String, Object> assistantMessage = ensureToolCallAssistantMessage();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> toolCalls = (List<Map<String, Object>>) assistantMessage.computeIfAbsent("toolCalls",
|
||||||
|
key -> new ArrayList<Map<String, Object>>());
|
||||||
|
Map<String, Object> toolCall = new LinkedHashMap<>();
|
||||||
|
toolCall.put("id", id);
|
||||||
|
toolCall.put("name", name);
|
||||||
|
toolCall.put("arguments", arguments == null ? null : String.valueOf(arguments));
|
||||||
|
putIfNotBlank(toolCall, "toolDisplayName", displayName);
|
||||||
|
toolCalls.add(toolCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 tool result,并附加到结构化消息链中。
|
||||||
|
*
|
||||||
|
* @param id tool call id
|
||||||
|
* @param name tool 名称
|
||||||
|
* @param result tool 结果
|
||||||
|
*/
|
||||||
public void appendToolResult(String id, String name, Object result) {
|
public void appendToolResult(String id, String name, Object result) {
|
||||||
|
appendToolResult(id, name, null, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 tool result,并保留面向前端展示的工具名称。
|
||||||
|
*
|
||||||
|
* @param id tool call id
|
||||||
|
* @param name tool 名称
|
||||||
|
* @param displayName tool 展示名称
|
||||||
|
* @param result tool 结果
|
||||||
|
*/
|
||||||
|
public void appendToolResult(String id, String name, String displayName, Object result) {
|
||||||
Map<String, Object> chain = findToolChain(id, name);
|
Map<String, Object> chain = findToolChain(id, name);
|
||||||
chain.put("status", "TOOL_RESULT");
|
chain.put("status", "TOOL_RESULT");
|
||||||
chain.put("result", result);
|
chain.put("result", result);
|
||||||
|
putIfNotBlank(chain, "toolDisplayName", displayName);
|
||||||
|
Map<String, Object> toolMessage = ChatRuntimeHistoryPayloadHelper.toolMessage(
|
||||||
|
id,
|
||||||
|
result == null ? null : String.valueOf(result)
|
||||||
|
);
|
||||||
|
toolMessages.add(toolMessage);
|
||||||
|
messageChain.add(ChatRuntimeHistoryPayloadHelper.deepCopyMap(toolMessage));
|
||||||
|
toolCallBatchOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 assistant 片段的文本内容。
|
||||||
|
*
|
||||||
|
* @return 文本内容
|
||||||
|
*/
|
||||||
public String getContent() {
|
public String getContent() {
|
||||||
return content.toString();
|
return content.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> buildPayload() {
|
/**
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
* 获取最近一次 tool-call assistant 的 reasoning 内容,供实时内存消息回写复用。
|
||||||
|
*
|
||||||
|
* @return reasoning 内容
|
||||||
|
*/
|
||||||
|
public String getLatestToolCallReasoning() {
|
||||||
|
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("reasoningContent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近一次 tool-call assistant 的内容,供实时内存消息回写复用。
|
||||||
|
*
|
||||||
|
* @return 内容
|
||||||
|
*/
|
||||||
|
public String getLatestToolCallContent() {
|
||||||
|
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("content"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 产出结构化 payload。
|
||||||
|
*
|
||||||
|
* @param finalContent 最终 assistant 文本
|
||||||
|
* @return payload
|
||||||
|
*/
|
||||||
|
public Map<String, Object> buildPayload(String finalContent) {
|
||||||
List<Map<String, Object>> payloadChains = new ArrayList<>();
|
List<Map<String, Object>> payloadChains = new ArrayList<>();
|
||||||
if (reasoning.length() > 0) {
|
if (displayReasoning.length() > 0) {
|
||||||
Map<String, Object> think = new LinkedHashMap<>();
|
Map<String, Object> think = new LinkedHashMap<>();
|
||||||
think.put("reasoning_content", reasoning.toString());
|
think.put("reasoning_content", displayReasoning.toString());
|
||||||
think.put("thinkingStatus", "end");
|
think.put("thinkingStatus", "end");
|
||||||
think.put("thinlCollapse", Boolean.TRUE);
|
think.put("thinlCollapse", Boolean.TRUE);
|
||||||
payloadChains.add(think);
|
payloadChains.add(think);
|
||||||
}
|
}
|
||||||
payloadChains.addAll(chains);
|
payloadChains.addAll(chains);
|
||||||
if (!payloadChains.isEmpty()) {
|
List<Map<String, Object>> payloadMessageChain = ChatRuntimeHistoryPayloadHelper.deepCopyList(messageChain);
|
||||||
payload.put("chains", payloadChains);
|
Map<String, Object> finalAssistantMessage = buildFinalAssistantMessage(finalContent);
|
||||||
|
if (!finalAssistantMessage.isEmpty()) {
|
||||||
|
payloadMessageChain.add(finalAssistantMessage);
|
||||||
}
|
}
|
||||||
return payload;
|
return ChatRuntimeHistoryPayloadHelper.buildPayload(payloadMessageChain, toolMessages, payloadChains);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> findToolChain(String id, String name) {
|
private Map<String, Object> findToolChain(String id, String name) {
|
||||||
@@ -71,4 +179,49 @@ public class ChatAssistantAccumulator {
|
|||||||
chains.add(chain);
|
chains.add(chain);
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> ensureToolCallAssistantMessage() {
|
||||||
|
if (toolCallBatchOpen && latestToolCallAssistant != null && !hasPendingAssistantContent()) {
|
||||||
|
return latestToolCallAssistant;
|
||||||
|
}
|
||||||
|
latestToolCallAssistant = ChatRuntimeHistoryPayloadHelper.assistantMessage(
|
||||||
|
content.length() == 0 ? null : content.toString(),
|
||||||
|
reasoning.length() == 0 ? null : reasoning.toString(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
messageChain.add(latestToolCallAssistant);
|
||||||
|
content.setLength(0);
|
||||||
|
reasoning.setLength(0);
|
||||||
|
toolCallBatchOpen = true;
|
||||||
|
return latestToolCallAssistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildFinalAssistantMessage(String finalContent) {
|
||||||
|
String assistantContent = finalContent;
|
||||||
|
if ((assistantContent == null || assistantContent.isEmpty()) && content.length() > 0) {
|
||||||
|
assistantContent = content.toString();
|
||||||
|
}
|
||||||
|
if ((assistantContent == null || assistantContent.isEmpty()) && reasoning.length() == 0) {
|
||||||
|
return new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
return ChatRuntimeHistoryPayloadHelper.assistantMessage(
|
||||||
|
assistantContent,
|
||||||
|
reasoning.length() == 0 ? null : reasoning.toString(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPendingAssistantContent() {
|
||||||
|
return content.length() > 0 || reasoning.length() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stringValue(Object value) {
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfNotBlank(Map<String, Object> target, String key, String value) {
|
||||||
|
if (value != null && !value.isBlank()) {
|
||||||
|
target.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package tech.easyflow.core.runtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天运行时扩展字段键。
|
||||||
|
*/
|
||||||
|
public final class ChatRuntimeExtKeys {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话级额外知识库 ID 列表。
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_KNOWLEDGE_IDS = "extraKnowledgeIds";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求要重答的轮次 ID。
|
||||||
|
*/
|
||||||
|
public static final String REGENERATE_ROUND_ID = "regenerateRoundId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求归属的轮次 ID。
|
||||||
|
*/
|
||||||
|
public static final String CURRENT_ROUND_ID = "currentRoundId";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求归属的轮次序号。
|
||||||
|
*/
|
||||||
|
public static final String CURRENT_ROUND_NO = "currentRoundNo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前请求生成的答案版本序号。
|
||||||
|
*/
|
||||||
|
public static final String CURRENT_VARIANT_INDEX = "currentVariantIndex";
|
||||||
|
|
||||||
|
private ChatRuntimeExtKeys() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
package tech.easyflow.core.runtime;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天运行时历史 payload 的统一读写工具。
|
||||||
|
* <p>
|
||||||
|
* 该工具只处理 {@link Map} / {@link List} 结构,避免把 easy-agents 类型泄漏到通用协议模块。
|
||||||
|
*/
|
||||||
|
public final class ChatRuntimeHistoryPayloadHelper {
|
||||||
|
|
||||||
|
public static final String KEY_MESSAGE_CHAIN = "messageChain";
|
||||||
|
public static final String KEY_ASSISTANT_MESSAGE = "assistantMessage";
|
||||||
|
public static final String KEY_FINAL_ASSISTANT_MESSAGE = "finalAssistantMessage";
|
||||||
|
public static final String KEY_TOOL_MESSAGES = "toolMessages";
|
||||||
|
public static final String KEY_DISPLAY_CHAINS = "displayChains";
|
||||||
|
public static final String KEY_CHAINS = "chains";
|
||||||
|
|
||||||
|
private ChatRuntimeHistoryPayloadHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 assistant 历史消息结构。
|
||||||
|
*
|
||||||
|
* @param content assistant 内容
|
||||||
|
* @param reasoningContent assistant reasoning 内容
|
||||||
|
* @param toolCalls assistant tool calls
|
||||||
|
* @return assistant 历史消息结构
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> assistantMessage(String content,
|
||||||
|
String reasoningContent,
|
||||||
|
List<Map<String, Object>> toolCalls) {
|
||||||
|
Map<String, Object> message = new LinkedHashMap<>();
|
||||||
|
message.put("role", "assistant");
|
||||||
|
if (content != null) {
|
||||||
|
message.put("content", content);
|
||||||
|
}
|
||||||
|
if (reasoningContent != null && !reasoningContent.isEmpty()) {
|
||||||
|
message.put("reasoningContent", reasoningContent);
|
||||||
|
}
|
||||||
|
if (toolCalls != null && !toolCalls.isEmpty()) {
|
||||||
|
message.put("toolCalls", deepCopyList(toolCalls));
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 tool 历史消息结构。
|
||||||
|
*
|
||||||
|
* @param toolCallId tool call id
|
||||||
|
* @param content tool 结果内容
|
||||||
|
* @return tool 历史消息结构
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> toolMessage(String toolCallId, String content) {
|
||||||
|
Map<String, Object> message = new LinkedHashMap<>();
|
||||||
|
message.put("role", "tool");
|
||||||
|
message.put("toolCallId", toolCallId);
|
||||||
|
if (content != null) {
|
||||||
|
message.put("content", content);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造统一的历史 payload。
|
||||||
|
*
|
||||||
|
* @param messageChain 完整 assistant/tool 历史链
|
||||||
|
* @param toolMessages tool 历史列表
|
||||||
|
* @param displayChains 前端展示链
|
||||||
|
* @return payload
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> buildPayload(List<Map<String, Object>> messageChain,
|
||||||
|
List<Map<String, Object>> toolMessages,
|
||||||
|
List<Map<String, Object>> displayChains) {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
List<Map<String, Object>> safeMessageChain = deepCopyList(messageChain);
|
||||||
|
List<Map<String, Object>> safeToolMessages = deepCopyList(toolMessages);
|
||||||
|
List<Map<String, Object>> safeDisplayChains = deepCopyList(displayChains);
|
||||||
|
if (!safeMessageChain.isEmpty()) {
|
||||||
|
payload.put(KEY_MESSAGE_CHAIN, safeMessageChain);
|
||||||
|
Map<String, Object> firstAssistant = findAssistant(safeMessageChain, false);
|
||||||
|
if (!firstAssistant.isEmpty()) {
|
||||||
|
payload.put(KEY_ASSISTANT_MESSAGE, firstAssistant);
|
||||||
|
}
|
||||||
|
Map<String, Object> lastAssistant = findAssistant(safeMessageChain, true);
|
||||||
|
if (!lastAssistant.isEmpty() && !lastAssistant.equals(firstAssistant)) {
|
||||||
|
payload.put(KEY_FINAL_ASSISTANT_MESSAGE, lastAssistant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!safeToolMessages.isEmpty()) {
|
||||||
|
payload.put(KEY_TOOL_MESSAGES, safeToolMessages);
|
||||||
|
}
|
||||||
|
if (!safeDisplayChains.isEmpty()) {
|
||||||
|
payload.put(KEY_DISPLAY_CHAINS, safeDisplayChains);
|
||||||
|
payload.put(KEY_CHAINS, deepCopyList(safeDisplayChains));
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取结构化消息链。
|
||||||
|
*
|
||||||
|
* @param payload contentPayload
|
||||||
|
* @return 结构化消息链
|
||||||
|
*/
|
||||||
|
public static List<Map<String, Object>> getMessageChain(Map<String, Object> payload) {
|
||||||
|
return getMapList(payload == null ? null : payload.get(KEY_MESSAGE_CHAIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 assistant 历史消息。
|
||||||
|
*
|
||||||
|
* @param payload contentPayload
|
||||||
|
* @return assistant 历史消息
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> getAssistantMessage(Map<String, Object> payload) {
|
||||||
|
return getMap(payload == null ? null : payload.get(KEY_ASSISTANT_MESSAGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取最终 assistant 历史消息。
|
||||||
|
*
|
||||||
|
* @param payload contentPayload
|
||||||
|
* @return 最终 assistant 历史消息
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> getFinalAssistantMessage(Map<String, Object> payload) {
|
||||||
|
return getMap(payload == null ? null : payload.get(KEY_FINAL_ASSISTANT_MESSAGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取 tool 历史消息列表。
|
||||||
|
*
|
||||||
|
* @param payload contentPayload
|
||||||
|
* @return tool 历史消息列表
|
||||||
|
*/
|
||||||
|
public static List<Map<String, Object>> getToolMessages(Map<String, Object> payload) {
|
||||||
|
return getMapList(payload == null ? null : payload.get(KEY_TOOL_MESSAGES));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 payload 是否已经包含新结构化历史。
|
||||||
|
*
|
||||||
|
* @param payload contentPayload
|
||||||
|
* @return true 表示包含新结构
|
||||||
|
*/
|
||||||
|
public static boolean hasStructuredHistory(Map<String, Object> payload) {
|
||||||
|
return !getMessageChain(payload).isEmpty()
|
||||||
|
|| !getAssistantMessage(payload).isEmpty()
|
||||||
|
|| !getToolMessages(payload).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取对象为 Map。
|
||||||
|
*
|
||||||
|
* @param value 值
|
||||||
|
* @return Map 视图
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static Map<String, Object> getMap(Object value) {
|
||||||
|
if (!(value instanceof Map<?, ?> source)) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||||
|
if (entry.getKey() != null) {
|
||||||
|
result.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取对象为 Map 列表。
|
||||||
|
*
|
||||||
|
* @param value 值
|
||||||
|
* @return Map 列表
|
||||||
|
*/
|
||||||
|
public static List<Map<String, Object>> getMapList(Object value) {
|
||||||
|
if (!(value instanceof List<?> source)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>(source.size());
|
||||||
|
for (Object item : source) {
|
||||||
|
Map<String, Object> map = getMap(item);
|
||||||
|
if (!map.isEmpty()) {
|
||||||
|
result.add(map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝 Map 列表。
|
||||||
|
*
|
||||||
|
* @param source 原始列表
|
||||||
|
* @return 深拷贝后的列表
|
||||||
|
*/
|
||||||
|
public static List<Map<String, Object>> deepCopyList(List<Map<String, Object>> source) {
|
||||||
|
if (source == null || source.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> copy = new ArrayList<>(source.size());
|
||||||
|
for (Map<String, Object> item : source) {
|
||||||
|
copy.add(deepCopyMap(item));
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝 Map。
|
||||||
|
*
|
||||||
|
* @param source 原始 Map
|
||||||
|
* @return 深拷贝后的 Map
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static Map<String, Object> deepCopyMap(Map<String, Object> source) {
|
||||||
|
if (source == null || source.isEmpty()) {
|
||||||
|
return new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
Map<String, Object> copy = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof Map<?, ?> mapValue) {
|
||||||
|
copy.put(entry.getKey(), deepCopyMap((Map<String, Object>) mapValue));
|
||||||
|
} else if (value instanceof List<?> listValue) {
|
||||||
|
copy.put(entry.getKey(), deepCopyValueList((List<Object>) listValue));
|
||||||
|
} else {
|
||||||
|
copy.put(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Object> deepCopyValueList(List<Object> source) {
|
||||||
|
List<Object> copy = new ArrayList<>(source.size());
|
||||||
|
for (Object item : source) {
|
||||||
|
if (item instanceof Map<?, ?> mapItem) {
|
||||||
|
copy.add(deepCopyMap((Map<String, Object>) mapItem));
|
||||||
|
} else if (item instanceof List<?> listItem) {
|
||||||
|
copy.add(deepCopyValueList((List<Object>) listItem));
|
||||||
|
} else {
|
||||||
|
copy.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> findAssistant(List<Map<String, Object>> messageChain, boolean reverse) {
|
||||||
|
if (messageChain == null || messageChain.isEmpty()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
if (reverse) {
|
||||||
|
for (int i = messageChain.size() - 1; i >= 0; i--) {
|
||||||
|
Map<String, Object> item = messageChain.get(i);
|
||||||
|
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
|
||||||
|
return deepCopyMap(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
for (Map<String, Object> item : messageChain) {
|
||||||
|
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
|
||||||
|
return deepCopyMap(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ public class ChatRuntimeMessage implements Serializable {
|
|||||||
private Date createdAt = new Date();
|
private Date createdAt = new Date();
|
||||||
private BigInteger senderId;
|
private BigInteger senderId;
|
||||||
private String senderName;
|
private String senderName;
|
||||||
|
private BigInteger roundId;
|
||||||
|
private Integer roundNo;
|
||||||
|
private String messageKind;
|
||||||
|
private Integer variantIndex;
|
||||||
|
|
||||||
public BigInteger getMessageId() {
|
public BigInteger getMessageId() {
|
||||||
return messageId;
|
return messageId;
|
||||||
@@ -80,4 +84,36 @@ public class ChatRuntimeMessage implements Serializable {
|
|||||||
public void setSenderName(String senderName) {
|
public void setSenderName(String senderName) {
|
||||||
this.senderName = senderName;
|
this.senderName = senderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getRoundId() {
|
||||||
|
return roundId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoundId(BigInteger roundId) {
|
||||||
|
this.roundId = roundId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getRoundNo() {
|
||||||
|
return roundNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoundNo(Integer roundNo) {
|
||||||
|
this.roundNo = roundNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessageKind() {
|
||||||
|
return messageKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageKind(String messageKind) {
|
||||||
|
this.messageKind = messageKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getVariantIndex() {
|
||||||
|
return variantIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVariantIndex(Integer variantIndex) {
|
||||||
|
this.variantIndex = variantIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,22 @@
|
|||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
<version>${jackson.version}</version>
|
<version>${jackson.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-pool2</artifactId>
|
||||||
|
<version>2.11.1</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.12.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.connection.RedisPassword;
|
import org.springframework.data.redis.connection.RedisPassword;
|
||||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
|
||||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import tech.easyflow.common.mq.core.MQConsumerContainer;
|
import tech.easyflow.common.mq.core.MQConsumerContainer;
|
||||||
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||||
@@ -24,6 +26,10 @@ import tech.easyflow.common.mq.redis.RedisMQProducer;
|
|||||||
import tech.easyflow.common.mq.redis.RedisStreamKeySupport;
|
import tech.easyflow.common.mq.redis.RedisStreamKeySupport;
|
||||||
import tech.easyflow.common.mq.support.MQHealthSupport;
|
import tech.easyflow.common.mq.support.MQHealthSupport;
|
||||||
|
|
||||||
|
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||||
|
|
||||||
|
import io.lettuce.core.api.StatefulConnection;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -43,11 +49,27 @@ public class MQConfiguration {
|
|||||||
if (redisProperties.getPassword() != null) {
|
if (redisProperties.getPassword() != null) {
|
||||||
configuration.setPassword(RedisPassword.of(redisProperties.getPassword()));
|
configuration.setPassword(RedisPassword.of(redisProperties.getPassword()));
|
||||||
}
|
}
|
||||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration);
|
LettuceClientConfiguration clientConfiguration = createClientConfiguration(redisProperties, mqProperties);
|
||||||
|
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration, clientConfiguration);
|
||||||
connectionFactory.afterPropertiesSet();
|
connectionFactory.afterPropertiesSet();
|
||||||
return new MQRedisResources(connectionFactory, new StringRedisTemplate(connectionFactory));
|
return new MQRedisResources(connectionFactory, new StringRedisTemplate(connectionFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LettuceClientConfiguration createClientConfiguration(RedisProperties redisProperties,
|
||||||
|
MQProperties mqProperties) {
|
||||||
|
MQProperties.Redis.Pool pool = mqProperties.getRedis().getPool();
|
||||||
|
GenericObjectPoolConfig<StatefulConnection<?, ?>> poolConfig = new GenericObjectPoolConfig<>();
|
||||||
|
poolConfig.setMaxTotal(pool.getMaxActive());
|
||||||
|
poolConfig.setMaxIdle(pool.getMaxIdle());
|
||||||
|
poolConfig.setMinIdle(pool.getMinIdle());
|
||||||
|
LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder =
|
||||||
|
LettucePoolingClientConfiguration.builder().poolConfig(poolConfig);
|
||||||
|
if (redisProperties.getTimeout() != null) {
|
||||||
|
builder.commandTimeout(redisProperties.getTimeout());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "mqRedisConnectionFactory", autowireCandidate = false, defaultCandidate = false)
|
@Bean(name = "mqRedisConnectionFactory", autowireCandidate = false, defaultCandidate = false)
|
||||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
public LettuceConnectionFactory mqRedisConnectionFactory(MQRedisResources mqRedisResources) {
|
public LettuceConnectionFactory mqRedisConnectionFactory(MQRedisResources mqRedisResources) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package tech.easyflow.common.mq.config;
|
package tech.easyflow.common.mq.config;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EasyFlow MQ 配置。
|
||||||
|
*/
|
||||||
@ConfigurationProperties(prefix = "easyflow.mq")
|
@ConfigurationProperties(prefix = "easyflow.mq")
|
||||||
public class MQProperties {
|
public class MQProperties {
|
||||||
|
|
||||||
@@ -35,11 +39,14 @@ public class MQProperties {
|
|||||||
|
|
||||||
private int database = 1;
|
private int database = 1;
|
||||||
private String streamPrefix = "easyflow:mq";
|
private String streamPrefix = "easyflow:mq";
|
||||||
|
private String consumerInstanceId = defaultConsumerInstanceId();
|
||||||
private int chatPersistShardCount = 4;
|
private int chatPersistShardCount = 4;
|
||||||
private int consumerBatchSize = 200;
|
private int consumerBatchSize = 200;
|
||||||
private Duration consumerBlockTimeout = Duration.ofMillis(2000);
|
private Duration consumerBlockTimeout = Duration.ofMillis(2000);
|
||||||
private Duration pendingClaimIdle = Duration.ofMillis(60000);
|
private Duration pendingClaimIdle = Duration.ofMillis(60000);
|
||||||
private int maxRetry = 16;
|
private int maxRetry = 16;
|
||||||
|
private ConsumerExecutor consumerExecutor = new ConsumerExecutor();
|
||||||
|
private Pool pool = new Pool();
|
||||||
|
|
||||||
public int getDatabase() {
|
public int getDatabase() {
|
||||||
return database;
|
return database;
|
||||||
@@ -57,6 +64,26 @@ public class MQProperties {
|
|||||||
this.streamPrefix = streamPrefix;
|
this.streamPrefix = streamPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Redis Stream 消费实例 ID。
|
||||||
|
*
|
||||||
|
* @return 消费实例 ID
|
||||||
|
*/
|
||||||
|
public String getConsumerInstanceId() {
|
||||||
|
return consumerInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Redis Stream 消费实例 ID。
|
||||||
|
*
|
||||||
|
* @param consumerInstanceId 消费实例 ID
|
||||||
|
*/
|
||||||
|
public void setConsumerInstanceId(String consumerInstanceId) {
|
||||||
|
this.consumerInstanceId = StringUtils.hasText(consumerInstanceId)
|
||||||
|
? consumerInstanceId.trim()
|
||||||
|
: defaultConsumerInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
public int getChatPersistShardCount() {
|
public int getChatPersistShardCount() {
|
||||||
return chatPersistShardCount;
|
return chatPersistShardCount;
|
||||||
}
|
}
|
||||||
@@ -96,5 +123,106 @@ public class MQProperties {
|
|||||||
public void setMaxRetry(int maxRetry) {
|
public void setMaxRetry(int maxRetry) {
|
||||||
this.maxRetry = maxRetry;
|
this.maxRetry = maxRetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ConsumerExecutor getConsumerExecutor() {
|
||||||
|
return consumerExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConsumerExecutor(ConsumerExecutor consumerExecutor) {
|
||||||
|
this.consumerExecutor = consumerExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pool getPool() {
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPool(Pool pool) {
|
||||||
|
this.pool = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis MQ 消费线程池配置。
|
||||||
|
*/
|
||||||
|
public static class ConsumerExecutor {
|
||||||
|
|
||||||
|
private int coreSize = 4;
|
||||||
|
private int maxSize = 12;
|
||||||
|
private int queueCapacity = 64;
|
||||||
|
private int keepAliveSeconds = 60;
|
||||||
|
|
||||||
|
public int getCoreSize() {
|
||||||
|
return coreSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCoreSize(int coreSize) {
|
||||||
|
this.coreSize = coreSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxSize() {
|
||||||
|
return maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxSize(int maxSize) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getQueueCapacity() {
|
||||||
|
return queueCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQueueCapacity(int queueCapacity) {
|
||||||
|
this.queueCapacity = queueCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getKeepAliveSeconds() {
|
||||||
|
return keepAliveSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeepAliveSeconds(int keepAliveSeconds) {
|
||||||
|
this.keepAliveSeconds = keepAliveSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis MQ 连接池配置。
|
||||||
|
*/
|
||||||
|
public static class Pool {
|
||||||
|
|
||||||
|
private int maxActive = 12;
|
||||||
|
private int maxIdle = 8;
|
||||||
|
private int minIdle = 1;
|
||||||
|
|
||||||
|
public int getMaxActive() {
|
||||||
|
return maxActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxActive(int maxActive) {
|
||||||
|
this.maxActive = maxActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMaxIdle() {
|
||||||
|
return maxIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxIdle(int maxIdle) {
|
||||||
|
this.maxIdle = maxIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMinIdle() {
|
||||||
|
return minIdle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMinIdle(int minIdle) {
|
||||||
|
this.minIdle = minIdle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultConsumerInstanceId() {
|
||||||
|
String hostName = System.getenv("HOSTNAME");
|
||||||
|
if (StringUtils.hasText(hostName)) {
|
||||||
|
return hostName.trim();
|
||||||
|
}
|
||||||
|
return java.util.UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public class MQSubscription {
|
|||||||
private String topic;
|
private String topic;
|
||||||
private String consumerGroup;
|
private String consumerGroup;
|
||||||
private int shardCount;
|
private int shardCount;
|
||||||
|
private boolean batchEnabled = true;
|
||||||
|
|
||||||
public String getTopic() {
|
public String getTopic() {
|
||||||
return topic;
|
return topic;
|
||||||
@@ -29,4 +30,22 @@ public class MQSubscription {
|
|||||||
public void setShardCount(int shardCount) {
|
public void setShardCount(int shardCount) {
|
||||||
this.shardCount = shardCount;
|
this.shardCount = shardCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用批量消费。
|
||||||
|
*
|
||||||
|
* @return true 表示启用批量消费
|
||||||
|
*/
|
||||||
|
public boolean isBatchEnabled() {
|
||||||
|
return batchEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否启用批量消费。
|
||||||
|
*
|
||||||
|
* @param batchEnabled 是否启用批量消费
|
||||||
|
*/
|
||||||
|
public void setBatchEnabled(boolean batchEnabled) {
|
||||||
|
this.batchEnabled = batchEnabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,17 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(RedisMQConsumerContainer.class);
|
private static final Logger LOG = LoggerFactory.getLogger(RedisMQConsumerContainer.class);
|
||||||
|
private static final Pattern UNSAFE_CONSUMER_NAME_CHARS = Pattern.compile("[^A-Za-z0-9_.-]");
|
||||||
|
|
||||||
private final RedisConnectionFactory redisConnectionFactory;
|
private final RedisConnectionFactory redisConnectionFactory;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
@@ -45,7 +49,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
private final MQDeadLetterService deadLetterService;
|
private final MQDeadLetterService deadLetterService;
|
||||||
private final RedisStreamKeySupport keySupport;
|
private final RedisStreamKeySupport keySupport;
|
||||||
private final List<MQConsumerHandler> handlers;
|
private final List<MQConsumerHandler> handlers;
|
||||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
private final ExecutorService executorService;
|
||||||
|
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
this.deadLetterService = deadLetterService;
|
this.deadLetterService = deadLetterService;
|
||||||
this.keySupport = keySupport;
|
this.keySupport = keySupport;
|
||||||
this.handlers = handlers;
|
this.handlers = handlers;
|
||||||
|
this.executorService = createExecutor(properties, handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -77,7 +82,12 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
int currentShard = shard;
|
int currentShard = shard;
|
||||||
LOG.info("启动 MQ 消费线程: topic={}, group={}, shard={}, handler={}",
|
LOG.info("启动 MQ 消费线程: topic={}, group={}, shard={}, handler={}",
|
||||||
subscription.getTopic(), subscription.getConsumerGroup(), currentShard, handler.getClass().getSimpleName());
|
subscription.getTopic(), subscription.getConsumerGroup(), currentShard, handler.getClass().getSimpleName());
|
||||||
executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
|
try {
|
||||||
|
executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
running = false;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,15 +118,62 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ExecutorService createExecutor(MQProperties properties, List<MQConsumerHandler> handlers) {
|
||||||
|
MQProperties.Redis.ConsumerExecutor config = properties.getRedis().getConsumerExecutor();
|
||||||
|
int consumerTaskCount = handlers.stream()
|
||||||
|
.map(MQConsumerHandler::subscription)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.mapToInt(subscription -> Math.max(subscription.getShardCount(), 1))
|
||||||
|
.sum();
|
||||||
|
if (config.getCoreSize() > config.getMaxSize()) {
|
||||||
|
throw new IllegalStateException("Redis MQ 消费线程池配置错误:core-size 不能大于 max-size");
|
||||||
|
}
|
||||||
|
if (consumerTaskCount > config.getMaxSize()) {
|
||||||
|
throw new IllegalStateException("Redis MQ 消费线程池配置错误:max-size="
|
||||||
|
+ config.getMaxSize() + " 小于消费循环数 " + consumerTaskCount
|
||||||
|
+ ",请调大 easyflow.mq.redis.consumer-executor.max-size");
|
||||||
|
}
|
||||||
|
int coreSize = Math.max(config.getCoreSize(), consumerTaskCount);
|
||||||
|
int maxSize = config.getMaxSize();
|
||||||
|
AtomicInteger threadIndex = new AtomicInteger(1);
|
||||||
|
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||||
|
coreSize,
|
||||||
|
maxSize,
|
||||||
|
config.getKeepAliveSeconds(),
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new ArrayBlockingQueue<>(config.getQueueCapacity()),
|
||||||
|
task -> {
|
||||||
|
Thread thread = new Thread(task);
|
||||||
|
thread.setName("redis-mq-consumer-" + threadIndex.getAndIncrement());
|
||||||
|
thread.setDaemon(false);
|
||||||
|
return thread;
|
||||||
|
},
|
||||||
|
new ThreadPoolExecutor.AbortPolicy()
|
||||||
|
);
|
||||||
|
executor.allowCoreThreadTimeOut(true);
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
|
||||||
private void consumeLoop(MQConsumerHandler handler, MQSubscription subscription, int shard) {
|
private void consumeLoop(MQConsumerHandler handler, MQSubscription subscription, int shard) {
|
||||||
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
||||||
String consumerName = subscription.getConsumerGroup() + "-" + shard;
|
String consumerName = buildConsumerName(subscription.getConsumerGroup(), shard);
|
||||||
ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
|
ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
|
||||||
LOG.info("MQ 消费循环已启动: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
LOG.info("MQ 消费循环已启动: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
||||||
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, handler.getClass().getSimpleName());
|
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, handler.getClass().getSimpleName());
|
||||||
while (running) {
|
while (running) {
|
||||||
try {
|
try {
|
||||||
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
List<MapRecord<String, Object, Object>> pendingRecords =
|
||||||
|
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
||||||
|
if (!pendingRecords.isEmpty()) {
|
||||||
|
List<MQMessage> pendingMessages = toMessages(streamKey, pendingRecords);
|
||||||
|
if (!pendingMessages.isEmpty()) {
|
||||||
|
LOG.info("MQ 收到重领 pending 消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
|
||||||
|
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName,
|
||||||
|
streamKey, pendingMessages.size());
|
||||||
|
handleMessages(handler, subscription, streamKey, subscription.getConsumerGroup(), pendingMessages);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
|
List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
|
||||||
Consumer.from(subscription.getConsumerGroup(), consumerName),
|
Consumer.from(subscription.getConsumerGroup(), consumerName),
|
||||||
StreamReadOptions.empty()
|
StreamReadOptions.empty()
|
||||||
@@ -133,7 +190,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
LOG.info("MQ 收到消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
|
LOG.info("MQ 收到消息批次: topic={}, group={}, shard={}, consumer={}, streamKey={}, count={}",
|
||||||
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, messages.size());
|
subscription.getTopic(), subscription.getConsumerGroup(), shard, consumerName, streamKey, messages.size());
|
||||||
handleMessages(handler, streamKey, subscription.getConsumerGroup(), messages);
|
handleMessages(handler, subscription, streamKey, subscription.getConsumerGroup(), messages);
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
LOG.error("MQ 消费循环异常: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
LOG.error("MQ 消费循环异常: topic={}, group={}, shard={}, consumer={}, streamKey={}, handler={}",
|
||||||
subscription.getTopic(),
|
subscription.getTopic(),
|
||||||
@@ -148,7 +205,20 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reclaimPending(String streamKey, String group, String consumerName) {
|
/**
|
||||||
|
* 构建 Redis Stream consumer name。
|
||||||
|
*
|
||||||
|
* @param consumerGroup 消费组
|
||||||
|
* @param shard 分片序号
|
||||||
|
* @return consumer name
|
||||||
|
*/
|
||||||
|
String buildConsumerName(String consumerGroup, int shard) {
|
||||||
|
String instanceId = properties.getRedis().getConsumerInstanceId();
|
||||||
|
String safeInstanceId = UNSAFE_CONSUMER_NAME_CHARS.matcher(instanceId).replaceAll("-");
|
||||||
|
return consumerGroup + "-" + shard + "-" + safeInstanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MapRecord<String, Object, Object>> reclaimPending(String streamKey, String group, String consumerName) {
|
||||||
Duration idle = properties.getRedis().getPendingClaimIdle();
|
Duration idle = properties.getRedis().getPendingClaimIdle();
|
||||||
try (RedisConnection connection = redisConnectionFactory.getConnection()) {
|
try (RedisConnection connection = redisConnectionFactory.getConnection()) {
|
||||||
RedisStreamCommands.XPendingOptions options = RedisStreamCommands.XPendingOptions
|
RedisStreamCommands.XPendingOptions options = RedisStreamCommands.XPendingOptions
|
||||||
@@ -156,7 +226,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
var pendingMessages = connection.streamCommands()
|
var pendingMessages = connection.streamCommands()
|
||||||
.xPending(streamKey.getBytes(StandardCharsets.UTF_8), group, options);
|
.xPending(streamKey.getBytes(StandardCharsets.UTF_8), group, options);
|
||||||
if (pendingMessages == null || pendingMessages.isEmpty()) {
|
if (pendingMessages == null || pendingMessages.isEmpty()) {
|
||||||
return;
|
return List.of();
|
||||||
}
|
}
|
||||||
List<RecordId> ids = new ArrayList<>();
|
List<RecordId> ids = new ArrayList<>();
|
||||||
for (PendingMessage pendingMessage : pendingMessages) {
|
for (PendingMessage pendingMessage : pendingMessages) {
|
||||||
@@ -165,15 +235,16 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ids.isEmpty()) {
|
if (ids.isEmpty()) {
|
||||||
return;
|
return List.of();
|
||||||
}
|
}
|
||||||
stringRedisTemplate.opsForStream().claim(
|
List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().claim(
|
||||||
streamKey,
|
streamKey,
|
||||||
group,
|
group,
|
||||||
consumerName,
|
consumerName,
|
||||||
idle,
|
idle,
|
||||||
ids.toArray(new RecordId[0])
|
ids.toArray(new RecordId[0])
|
||||||
);
|
);
|
||||||
|
return records == null ? List.of() : records;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +260,7 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<MQMessage> toMessages(String streamKey, List<MapRecord<String, Object, Object>> records) {
|
List<MQMessage> toMessages(String streamKey, List<MapRecord<String, Object, Object>> records) {
|
||||||
List<MQMessage> messages = new ArrayList<>(records.size());
|
List<MQMessage> messages = new ArrayList<>(records.size());
|
||||||
for (MapRecord<String, Object, Object> record : records) {
|
for (MapRecord<String, Object, Object> record : records) {
|
||||||
Object payload = record.getValue().get("payload");
|
Object payload = record.getValue().get("payload");
|
||||||
@@ -225,7 +296,15 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMessages(MQConsumerHandler handler, String streamKey, String group, List<MQMessage> messages) throws Exception {
|
void handleMessages(MQConsumerHandler handler,
|
||||||
|
MQSubscription subscription,
|
||||||
|
String streamKey,
|
||||||
|
String group,
|
||||||
|
List<MQMessage> messages) throws Exception {
|
||||||
|
if (!subscription.isBatchEnabled()) {
|
||||||
|
handleMessagesIndividually(handler, streamKey, group, messages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
LOG.info("MQ 开始批量处理消息: group={}, streamKey={}, count={}, handler={}",
|
LOG.info("MQ 开始批量处理消息: group={}, streamKey={}, count={}, handler={}",
|
||||||
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
group, streamKey, messages.size(), handler.getClass().getSimpleName());
|
||||||
@@ -244,6 +323,13 @@ public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMessagesIndividually(handler, streamKey, group, messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMessagesIndividually(MQConsumerHandler handler,
|
||||||
|
String streamKey,
|
||||||
|
String group,
|
||||||
|
List<MQMessage> messages) {
|
||||||
for (MQMessage message : messages) {
|
for (MQMessage message : messages) {
|
||||||
try {
|
try {
|
||||||
LOG.info("MQ 开始单条处理消息: group={}, streamKey={}, messageId={}, handler={}",
|
LOG.info("MQ 开始单条处理消息: group={}, streamKey={}, messageId={}, handler={}",
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package tech.easyflow.common.mq.redis;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentMatchers;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnection;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.RedisStreamCommands;
|
||||||
|
import org.springframework.data.redis.connection.stream.Consumer;
|
||||||
|
import org.springframework.data.redis.connection.stream.MapRecord;
|
||||||
|
import org.springframework.data.redis.connection.stream.PendingMessage;
|
||||||
|
import org.springframework.data.redis.connection.stream.PendingMessages;
|
||||||
|
import org.springframework.data.redis.connection.stream.RecordId;
|
||||||
|
import org.springframework.data.redis.core.StreamOperations;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import tech.easyflow.common.mq.config.MQProperties;
|
||||||
|
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||||
|
import tech.easyflow.common.mq.core.MQDeadLetterService;
|
||||||
|
import tech.easyflow.common.mq.core.MQMessage;
|
||||||
|
import tech.easyflow.common.mq.core.MQMessageConverter;
|
||||||
|
import tech.easyflow.common.mq.core.MQSubscription;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link RedisMQConsumerContainer} 回归测试。
|
||||||
|
*/
|
||||||
|
public class RedisMQConsumerContainerTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 consumer name 包含稳定实例 ID,且消费组名称不被改变。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void buildConsumerNameShouldAppendSanitizedInstanceId() {
|
||||||
|
MQProperties properties = new MQProperties();
|
||||||
|
properties.getRedis().setConsumerInstanceId("node/a:1");
|
||||||
|
RedisMQConsumerContainer container = new RedisMQConsumerContainer(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
properties,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
|
||||||
|
String consumerName = container.buildConsumerName("chat-persist", 2);
|
||||||
|
|
||||||
|
Assert.assertEquals("chat-persist-2-node-a-1", consumerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证关闭批量消费后,容器按单条处理并独立确认消息。
|
||||||
|
*
|
||||||
|
* @throws Exception 消息处理异常
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void handleMessagesShouldProcessIndividuallyWhenBatchDisabled() throws Exception {
|
||||||
|
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
StreamOperations<String, Object, Object> streamOperations = Mockito.mock(StreamOperations.class);
|
||||||
|
Mockito.when(redisTemplate.opsForStream()).thenReturn(streamOperations);
|
||||||
|
RecordingHandler handler = new RecordingHandler();
|
||||||
|
MQSubscription subscription = new MQSubscription();
|
||||||
|
subscription.setBatchEnabled(false);
|
||||||
|
RedisMQConsumerContainer container = container(redisTemplate, null);
|
||||||
|
MQMessage first = message("message-1", "1-0");
|
||||||
|
MQMessage second = message("message-2", "2-0");
|
||||||
|
|
||||||
|
container.handleMessages(handler, subscription, "stream-1", "group-1", List.of(first, second));
|
||||||
|
|
||||||
|
Assert.assertEquals(List.of(List.of("message-1"), List.of("message-2")), handler.calls);
|
||||||
|
Mockito.verify(streamOperations).acknowledge("stream-1", "group-1", "1-0");
|
||||||
|
Mockito.verify(streamOperations).acknowledge("stream-1", "group-1", "2-0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证 pending 消息被 claim 后可以转换为 MQ 消息继续消费。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void reclaimPendingShouldReturnClaimedRecordsForConsumption() {
|
||||||
|
StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
StreamOperations<String, Object, Object> streamOperations = Mockito.mock(StreamOperations.class);
|
||||||
|
Mockito.when(redisTemplate.opsForStream()).thenReturn(streamOperations);
|
||||||
|
RedisConnectionFactory connectionFactory = Mockito.mock(RedisConnectionFactory.class);
|
||||||
|
RedisConnection connection = Mockito.mock(RedisConnection.class);
|
||||||
|
RedisStreamCommands streamCommands = Mockito.mock(RedisStreamCommands.class);
|
||||||
|
Mockito.when(connectionFactory.getConnection()).thenReturn(connection);
|
||||||
|
Mockito.when(connection.streamCommands()).thenReturn(streamCommands);
|
||||||
|
PendingMessage pendingMessage = new PendingMessage(
|
||||||
|
RecordId.of("1-0"), Consumer.from("group-1", "old-consumer"), Duration.ofMinutes(2), 1);
|
||||||
|
Mockito.when(streamCommands.xPending(
|
||||||
|
ArgumentMatchers.eq("stream-1".getBytes(java.nio.charset.StandardCharsets.UTF_8)),
|
||||||
|
ArgumentMatchers.eq("group-1"),
|
||||||
|
ArgumentMatchers.any(RedisStreamCommands.XPendingOptions.class)))
|
||||||
|
.thenReturn(new PendingMessages("group-1", List.of(pendingMessage)));
|
||||||
|
Map<Object, Object> payload = Map.of("payload", "message-1");
|
||||||
|
MapRecord<String, Object, Object> record = MapRecord
|
||||||
|
.create("stream-1", payload)
|
||||||
|
.withId(RecordId.of("1-0"));
|
||||||
|
Mockito.when(streamOperations.claim(
|
||||||
|
ArgumentMatchers.eq("stream-1"),
|
||||||
|
ArgumentMatchers.eq("group-1"),
|
||||||
|
ArgumentMatchers.eq("consumer-1"),
|
||||||
|
ArgumentMatchers.any(Duration.class),
|
||||||
|
ArgumentMatchers.any(RecordId[].class)))
|
||||||
|
.thenReturn(List.of(record));
|
||||||
|
RedisMQConsumerContainer container = container(redisTemplate, connectionFactory);
|
||||||
|
|
||||||
|
List<MapRecord<String, Object, Object>> records =
|
||||||
|
container.reclaimPending("stream-1", "group-1", "consumer-1");
|
||||||
|
List<MQMessage> messages = container.toMessages("stream-1", records);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, records.size());
|
||||||
|
Assert.assertEquals(1, messages.size());
|
||||||
|
Assert.assertEquals("message-1", messages.get(0).getMessageId());
|
||||||
|
Assert.assertEquals("1-0", messages.get(0).getStreamMessageId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private RedisMQConsumerContainer container(StringRedisTemplate redisTemplate,
|
||||||
|
RedisConnectionFactory connectionFactory) {
|
||||||
|
MQProperties properties = new MQProperties();
|
||||||
|
return new RedisMQConsumerContainer(
|
||||||
|
connectionFactory,
|
||||||
|
redisTemplate,
|
||||||
|
properties,
|
||||||
|
new PlainMessageConverter(),
|
||||||
|
Mockito.mock(MQDeadLetterService.class),
|
||||||
|
null,
|
||||||
|
List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MQMessage message(String messageId, String streamMessageId) {
|
||||||
|
MQMessage message = new MQMessage();
|
||||||
|
message.setMessageId(messageId);
|
||||||
|
message.setStreamMessageId(streamMessageId);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RecordingHandler implements MQConsumerHandler {
|
||||||
|
|
||||||
|
private final List<List<String>> calls = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MQSubscription subscription() {
|
||||||
|
return new MQSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(List<MQMessage> messages) {
|
||||||
|
calls.add(messages.stream().map(MQMessage::getMessageId).toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlainMessageConverter implements MQMessageConverter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String serialize(MQMessage message) {
|
||||||
|
return message.getMessageId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MQMessage deserialize(String payload) {
|
||||||
|
MQMessage message = new MQMessage();
|
||||||
|
message.setMessageId(payload);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
easyflow-modules/easyflow-module-agent/pom.xml
Normal file
77
easyflow-modules/easyflow-module-agent/pom.xml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-modules</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easyflow-module-agent</name>
|
||||||
|
<artifactId>easyflow-module-agent</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-approval</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-system</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-satoken</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mybatis-flex</groupId>
|
||||||
|
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<version>5.12.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package tech.easyflow.agent.config;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.ComponentScan;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 模块自动配置。
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
@MapperScan("tech.easyflow.agent.mapper")
|
||||||
|
@ComponentScan("tech.easyflow.agent")
|
||||||
|
@EnableConfigurationProperties(AgentRuntimeProperties.class)
|
||||||
|
public class AgentModuleConfig {
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
package tech.easyflow.agent.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态生产化配置。
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "easyflow.agent.runtime")
|
||||||
|
public class AgentRuntimeProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 热态 session 缓存 TTL。
|
||||||
|
*/
|
||||||
|
private Duration sessionCacheTtl = Duration.ofHours(24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前 Agent 运行实例 ID。
|
||||||
|
*/
|
||||||
|
private String instanceId = defaultInstanceId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行路由 TTL。
|
||||||
|
*/
|
||||||
|
private Duration routeTtl = Duration.ofHours(24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行命令 topic 前缀。
|
||||||
|
*/
|
||||||
|
private String commandTopicPrefix = "easyflow:agent-runtime-command";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行命令结果等待超时时间。
|
||||||
|
*/
|
||||||
|
private Duration commandResultTimeout = Duration.ofSeconds(5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行命令结果缓存 TTL。
|
||||||
|
*/
|
||||||
|
private Duration commandResultTtl = Duration.ofMinutes(5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前进程启动代 ID。
|
||||||
|
*/
|
||||||
|
private final String bootId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HITL pending 默认过期时间。
|
||||||
|
*/
|
||||||
|
private Duration hitlPendingTimeout = Duration.ofMinutes(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行锁等待时间。
|
||||||
|
*/
|
||||||
|
private Duration lockWaitTimeout = Duration.ofSeconds(2);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行锁租约时间。
|
||||||
|
*/
|
||||||
|
private Duration lockLeaseTimeout = Duration.ofMinutes(5);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行锁续期间隔。
|
||||||
|
*/
|
||||||
|
private Duration lockRenewInterval = Duration.ofMinutes(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 异步工具任务 Redis 运行态 TTL。
|
||||||
|
*/
|
||||||
|
private Duration asyncToolTaskTtl = Duration.ofHours(24);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Redis 热态 session 缓存 TTL。
|
||||||
|
*
|
||||||
|
* @return 缓存 TTL
|
||||||
|
*/
|
||||||
|
public Duration getSessionCacheTtl() {
|
||||||
|
return sessionCacheTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Redis 热态 session 缓存 TTL。
|
||||||
|
*
|
||||||
|
* @param sessionCacheTtl 缓存 TTL
|
||||||
|
*/
|
||||||
|
public void setSessionCacheTtl(Duration sessionCacheTtl) {
|
||||||
|
this.sessionCacheTtl = sessionCacheTtl == null ? Duration.ofHours(24) : sessionCacheTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前 Agent 运行实例 ID。
|
||||||
|
*
|
||||||
|
* @return 实例 ID
|
||||||
|
*/
|
||||||
|
public String getInstanceId() {
|
||||||
|
return instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前 Agent 运行实例 ID。
|
||||||
|
*
|
||||||
|
* @param instanceId 实例 ID
|
||||||
|
*/
|
||||||
|
public void setInstanceId(String instanceId) {
|
||||||
|
this.instanceId = StringUtils.hasText(instanceId) ? instanceId.trim() : defaultInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 运行路由 TTL。
|
||||||
|
*
|
||||||
|
* @return 路由 TTL
|
||||||
|
*/
|
||||||
|
public Duration getRouteTtl() {
|
||||||
|
return routeTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Agent 运行路由 TTL。
|
||||||
|
*
|
||||||
|
* @param routeTtl 路由 TTL
|
||||||
|
*/
|
||||||
|
public void setRouteTtl(Duration routeTtl) {
|
||||||
|
this.routeTtl = routeTtl == null ? Duration.ofHours(24) : routeTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 运行命令 topic 前缀。
|
||||||
|
*
|
||||||
|
* @return 命令 topic 前缀
|
||||||
|
*/
|
||||||
|
public String getCommandTopicPrefix() {
|
||||||
|
return commandTopicPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Agent 运行命令 topic 前缀。
|
||||||
|
*
|
||||||
|
* @param commandTopicPrefix 命令 topic 前缀
|
||||||
|
*/
|
||||||
|
public void setCommandTopicPrefix(String commandTopicPrefix) {
|
||||||
|
this.commandTopicPrefix = StringUtils.hasText(commandTopicPrefix)
|
||||||
|
? commandTopicPrefix.trim()
|
||||||
|
: "easyflow:agent-runtime-command";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 运行命令结果等待超时时间。
|
||||||
|
*
|
||||||
|
* @return 等待超时时间
|
||||||
|
*/
|
||||||
|
public Duration getCommandResultTimeout() {
|
||||||
|
return commandResultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Agent 运行命令结果等待超时时间。
|
||||||
|
*
|
||||||
|
* @param commandResultTimeout 等待超时时间
|
||||||
|
*/
|
||||||
|
public void setCommandResultTimeout(Duration commandResultTimeout) {
|
||||||
|
this.commandResultTimeout = commandResultTimeout == null ? Duration.ofSeconds(5) : commandResultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 运行命令结果缓存 TTL。
|
||||||
|
*
|
||||||
|
* @return 结果缓存 TTL
|
||||||
|
*/
|
||||||
|
public Duration getCommandResultTtl() {
|
||||||
|
return commandResultTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Agent 运行命令结果缓存 TTL。
|
||||||
|
*
|
||||||
|
* @param commandResultTtl 结果缓存 TTL
|
||||||
|
*/
|
||||||
|
public void setCommandResultTtl(Duration commandResultTtl) {
|
||||||
|
this.commandResultTtl = commandResultTtl == null ? Duration.ofMinutes(5) : commandResultTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前进程启动代 ID。
|
||||||
|
*
|
||||||
|
* @return 启动代 ID
|
||||||
|
*/
|
||||||
|
public String getBootId() {
|
||||||
|
return bootId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 HITL pending 默认过期时间。
|
||||||
|
*
|
||||||
|
* @return 过期时间
|
||||||
|
*/
|
||||||
|
public Duration getHitlPendingTimeout() {
|
||||||
|
return hitlPendingTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 HITL pending 默认过期时间。
|
||||||
|
*
|
||||||
|
* @param hitlPendingTimeout 过期时间
|
||||||
|
*/
|
||||||
|
public void setHitlPendingTimeout(Duration hitlPendingTimeout) {
|
||||||
|
this.hitlPendingTimeout = hitlPendingTimeout == null ? Duration.ofMinutes(30) : hitlPendingTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行锁等待时间。
|
||||||
|
*
|
||||||
|
* @return 等待时间
|
||||||
|
*/
|
||||||
|
public Duration getLockWaitTimeout() {
|
||||||
|
return lockWaitTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行锁等待时间。
|
||||||
|
*
|
||||||
|
* @param lockWaitTimeout 等待时间
|
||||||
|
*/
|
||||||
|
public void setLockWaitTimeout(Duration lockWaitTimeout) {
|
||||||
|
this.lockWaitTimeout = lockWaitTimeout == null ? Duration.ofSeconds(2) : lockWaitTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行锁租约时间。
|
||||||
|
*
|
||||||
|
* @return 租约时间
|
||||||
|
*/
|
||||||
|
public Duration getLockLeaseTimeout() {
|
||||||
|
return lockLeaseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行锁租约时间。
|
||||||
|
*
|
||||||
|
* @param lockLeaseTimeout 租约时间
|
||||||
|
*/
|
||||||
|
public void setLockLeaseTimeout(Duration lockLeaseTimeout) {
|
||||||
|
this.lockLeaseTimeout = lockLeaseTimeout == null ? Duration.ofMinutes(5) : lockLeaseTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行锁续期间隔。
|
||||||
|
*
|
||||||
|
* @return 续期间隔
|
||||||
|
*/
|
||||||
|
public Duration getLockRenewInterval() {
|
||||||
|
return lockRenewInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行锁续期间隔。
|
||||||
|
*
|
||||||
|
* @param lockRenewInterval 续期间隔
|
||||||
|
*/
|
||||||
|
public void setLockRenewInterval(Duration lockRenewInterval) {
|
||||||
|
this.lockRenewInterval = lockRenewInterval == null ? Duration.ofMinutes(1) : lockRenewInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Agent 异步工具任务 Redis 运行态 TTL。
|
||||||
|
*
|
||||||
|
* @return 任务 TTL
|
||||||
|
*/
|
||||||
|
public Duration getAsyncToolTaskTtl() {
|
||||||
|
return asyncToolTaskTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Agent 异步工具任务 Redis 运行态 TTL。
|
||||||
|
*
|
||||||
|
* @param asyncToolTaskTtl 任务 TTL
|
||||||
|
*/
|
||||||
|
public void setAsyncToolTaskTtl(Duration asyncToolTaskTtl) {
|
||||||
|
this.asyncToolTaskTtl = asyncToolTaskTtl == null ? Duration.ofHours(24) : asyncToolTaskTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String defaultInstanceId() {
|
||||||
|
String envInstanceId = System.getenv("EASYFLOW_INSTANCE_ID");
|
||||||
|
if (StringUtils.hasText(envInstanceId)) {
|
||||||
|
return envInstanceId.trim();
|
||||||
|
}
|
||||||
|
String hostName = System.getenv("HOSTNAME");
|
||||||
|
if (StringUtils.hasText(hostName)) {
|
||||||
|
return hostName.trim();
|
||||||
|
}
|
||||||
|
return UUID.randomUUID().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程命令动作。
|
||||||
|
*/
|
||||||
|
public enum AgentRuntimeCommandAction {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批准工具执行。
|
||||||
|
*/
|
||||||
|
APPROVE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拒绝工具执行。
|
||||||
|
*/
|
||||||
|
REJECT
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||||
|
import tech.easyflow.agent.runtime.AgentRunService;
|
||||||
|
import tech.easyflow.common.mq.config.MQProperties;
|
||||||
|
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||||
|
import tech.easyflow.common.mq.core.MQMessage;
|
||||||
|
import tech.easyflow.common.mq.core.MQSubscription;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程命令消费者。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AgentRuntimeCommandConsumer implements MQConsumerHandler {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AgentRuntimeCommandConsumer.class);
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final AgentRuntimeProperties properties;
|
||||||
|
private final MQProperties mqProperties;
|
||||||
|
private final AgentRunService agentRunService;
|
||||||
|
private final AgentRuntimeCommandResultRegistry resultRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 运行态远程命令消费者。
|
||||||
|
*
|
||||||
|
* @param objectMapper JSON 序列化器
|
||||||
|
* @param properties Agent 运行配置
|
||||||
|
* @param mqProperties MQ 配置
|
||||||
|
* @param agentRunService Agent 运行服务
|
||||||
|
* @param resultRegistry 远程命令结果注册表
|
||||||
|
*/
|
||||||
|
public AgentRuntimeCommandConsumer(ObjectMapper objectMapper,
|
||||||
|
AgentRuntimeProperties properties,
|
||||||
|
MQProperties mqProperties,
|
||||||
|
AgentRunService agentRunService,
|
||||||
|
AgentRuntimeCommandResultRegistry resultRegistry) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.properties = properties;
|
||||||
|
this.mqProperties = mqProperties;
|
||||||
|
this.agentRunService = agentRunService;
|
||||||
|
this.resultRegistry = resultRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MQSubscription subscription() {
|
||||||
|
MQSubscription subscription = new MQSubscription();
|
||||||
|
subscription.setTopic(commandTopic());
|
||||||
|
subscription.setConsumerGroup(commandTopic());
|
||||||
|
subscription.setShardCount(Math.max(mqProperties.getRedis().getChatPersistShardCount(), 1));
|
||||||
|
subscription.setBatchEnabled(false);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(List<MQMessage> messages) {
|
||||||
|
if (messages == null || messages.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (MQMessage message : messages) {
|
||||||
|
try {
|
||||||
|
handleCommand(message, objectMapper.readValue(message.getBody(), AgentRuntimeCommandMessage.class));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Agent 远程运行命令解析失败: messageId={}", message.getMessageId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCommand(MQMessage message, AgentRuntimeCommandMessage command) {
|
||||||
|
if (command == null || command.getAction() == null) {
|
||||||
|
LOG.warn("跳过非法 Agent 远程运行命令: messageId={}", message.getMessageId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!properties.getInstanceId().equals(command.getTargetNodeId())) {
|
||||||
|
LOG.warn("跳过非本节点 Agent 远程运行命令: messageId={}, targetNodeId={}, currentNodeId={}",
|
||||||
|
message.getMessageId(), command.getTargetNodeId(), properties.getInstanceId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (command.getAction() == AgentRuntimeCommandAction.APPROVE) {
|
||||||
|
agentRunService.approveRuntimeLocal(
|
||||||
|
command.getRequestId(), command.getResumeToken(), command.getOperatorId(), command.getUserId());
|
||||||
|
} else if (command.getAction() == AgentRuntimeCommandAction.REJECT) {
|
||||||
|
agentRunService.rejectRuntimeLocal(
|
||||||
|
command.getRequestId(), command.getResumeToken(), command.getReason(),
|
||||||
|
command.getOperatorId(), command.getUserId());
|
||||||
|
} else {
|
||||||
|
markFailureQuietly(command, new IllegalArgumentException("不支持的 Agent 远程运行命令"));
|
||||||
|
LOG.warn("跳过不支持的 Agent 远程运行命令: messageId={}, commandId={}, action={}",
|
||||||
|
message.getMessageId(), command.getCommandId(), command.getAction());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
markFailureQuietly(command, e);
|
||||||
|
LOG.warn("Agent 远程运行命令处理失败: messageId={}, commandId={}",
|
||||||
|
message.getMessageId(), command.getCommandId(), e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markSuccessQuietly(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String commandTopic() {
|
||||||
|
return properties.getCommandTopicPrefix() + ":" + properties.getInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markSuccessQuietly(AgentRuntimeCommandMessage command) {
|
||||||
|
try {
|
||||||
|
resultRegistry.markSuccess(command.getCommandId());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOG.error("Agent 远程运行命令成功结果写入失败: commandId={}", command.getCommandId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markFailureQuietly(AgentRuntimeCommandMessage command, RuntimeException cause) {
|
||||||
|
try {
|
||||||
|
resultRegistry.markFailure(command.getCommandId(), cause.getMessage());
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOG.error("Agent 远程运行命令失败结果写入失败: commandId={}", command.getCommandId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程恢复命令消息。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeCommandMessage {
|
||||||
|
|
||||||
|
private String commandId;
|
||||||
|
private String requestId;
|
||||||
|
private String resumeToken;
|
||||||
|
private AgentRuntimeCommandAction action;
|
||||||
|
private String reason;
|
||||||
|
private BigInteger operatorId;
|
||||||
|
private String userId;
|
||||||
|
private String targetNodeId;
|
||||||
|
private Date occurredAt;
|
||||||
|
|
||||||
|
public String getCommandId() {
|
||||||
|
return commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCommandId(String commandId) {
|
||||||
|
this.commandId = commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRequestId() {
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestId(String requestId) {
|
||||||
|
this.requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResumeToken() {
|
||||||
|
return resumeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResumeToken(String resumeToken) {
|
||||||
|
this.resumeToken = resumeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AgentRuntimeCommandAction getAction() {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAction(AgentRuntimeCommandAction action) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReason(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigInteger getOperatorId() {
|
||||||
|
return operatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOperatorId(BigInteger operatorId) {
|
||||||
|
this.operatorId = operatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTargetNodeId() {
|
||||||
|
return targetNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargetNodeId(String targetNodeId) {
|
||||||
|
this.targetNodeId = targetNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getOccurredAt() {
|
||||||
|
return occurredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOccurredAt(Date occurredAt) {
|
||||||
|
this.occurredAt = occurredAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||||
|
import tech.easyflow.common.mq.core.MQMessage;
|
||||||
|
import tech.easyflow.common.mq.core.MQProducer;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程命令生产者。
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AgentRuntimeCommandProducer {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AgentRuntimeCommandProducer.class);
|
||||||
|
|
||||||
|
private final MQProducer mqProducer;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final AgentRuntimeProperties properties;
|
||||||
|
private final AgentRuntimeCommandResultRegistry resultRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试子类构造器。
|
||||||
|
*/
|
||||||
|
protected AgentRuntimeCommandProducer() {
|
||||||
|
this.mqProducer = null;
|
||||||
|
this.objectMapper = null;
|
||||||
|
this.properties = null;
|
||||||
|
this.resultRegistry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 运行态远程命令生产者。
|
||||||
|
*
|
||||||
|
* @param mqProducer MQ 生产者
|
||||||
|
* @param objectMapper JSON 序列化器
|
||||||
|
* @param properties Agent 运行配置
|
||||||
|
* @param resultRegistry 远程命令结果注册表
|
||||||
|
*/
|
||||||
|
public AgentRuntimeCommandProducer(MQProducer mqProducer,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
AgentRuntimeProperties properties,
|
||||||
|
AgentRuntimeCommandResultRegistry resultRegistry) {
|
||||||
|
this.mqProducer = mqProducer;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.properties = properties;
|
||||||
|
this.resultRegistry = resultRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 投递远程批准命令。
|
||||||
|
*
|
||||||
|
* @param targetNodeId 目标节点 ID
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param userId 用户 ID
|
||||||
|
*/
|
||||||
|
public void sendApprove(String targetNodeId,
|
||||||
|
String requestId,
|
||||||
|
String resumeToken,
|
||||||
|
BigInteger operatorId,
|
||||||
|
String userId) {
|
||||||
|
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.APPROVE, null, operatorId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 投递远程拒绝命令。
|
||||||
|
*
|
||||||
|
* @param targetNodeId 目标节点 ID
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @param reason 拒绝原因
|
||||||
|
* @param operatorId 操作人 ID
|
||||||
|
* @param userId 用户 ID
|
||||||
|
*/
|
||||||
|
public void sendReject(String targetNodeId,
|
||||||
|
String requestId,
|
||||||
|
String resumeToken,
|
||||||
|
String reason,
|
||||||
|
BigInteger operatorId,
|
||||||
|
String userId) {
|
||||||
|
sendAndWait(targetNodeId, requestId, resumeToken, AgentRuntimeCommandAction.REJECT, reason, operatorId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendAndWait(String targetNodeId,
|
||||||
|
String requestId,
|
||||||
|
String resumeToken,
|
||||||
|
AgentRuntimeCommandAction action,
|
||||||
|
String reason,
|
||||||
|
BigInteger operatorId,
|
||||||
|
String userId) {
|
||||||
|
if (targetNodeId == null || targetNodeId.isBlank()) {
|
||||||
|
throw new BusinessException("Agent 运行节点不可用,请重新发起对话");
|
||||||
|
}
|
||||||
|
AgentRuntimeCommandMessage command = new AgentRuntimeCommandMessage();
|
||||||
|
command.setCommandId(UUID.randomUUID().toString());
|
||||||
|
command.setRequestId(requestId);
|
||||||
|
command.setResumeToken(resumeToken);
|
||||||
|
command.setAction(action);
|
||||||
|
command.setReason(reason);
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
command.setUserId(userId);
|
||||||
|
command.setTargetNodeId(targetNodeId);
|
||||||
|
command.setOccurredAt(new Date());
|
||||||
|
|
||||||
|
MQMessage message = new MQMessage();
|
||||||
|
message.setMessageId(command.getCommandId());
|
||||||
|
message.setTopic(commandTopic(targetNodeId));
|
||||||
|
message.setKey(command.getCommandId());
|
||||||
|
message.setCreatedAt(command.getOccurredAt());
|
||||||
|
try {
|
||||||
|
message.setBody(objectMapper.writeValueAsString(command));
|
||||||
|
String recordId = mqProducer.send(message);
|
||||||
|
LOG.info("Agent 远程运行命令已投递: action={}, requestId={}, targetNodeId={}, recordId={}",
|
||||||
|
action, requestId, targetNodeId, recordId);
|
||||||
|
AgentRuntimeCommandResult result = resultRegistry.waitForResult(command.getCommandId());
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
throw new BusinessException(result.getMessage());
|
||||||
|
}
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new BusinessException("Agent 运行命令序列化失败");
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOG.error("Agent 远程运行命令投递失败: action={}, requestId={}, targetNodeId={}",
|
||||||
|
action, requestId, targetNodeId, e);
|
||||||
|
throw new BusinessException("Agent 运行节点不可用,请重新发起对话");
|
||||||
|
} finally {
|
||||||
|
deleteResultQuietly(command.getCommandId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String commandTopic(String nodeId) {
|
||||||
|
return properties.getCommandTopicPrefix() + ":" + nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteResultQuietly(String commandId) {
|
||||||
|
try {
|
||||||
|
resultRegistry.deleteResult(commandId);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOG.warn("Agent 远程运行命令结果清理失败,等待 TTL 兜底: commandId={}", commandId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程命令结果。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeCommandResult {
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断命令是否执行成功。
|
||||||
|
*
|
||||||
|
* @return true 表示执行成功
|
||||||
|
*/
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置命令是否执行成功。
|
||||||
|
*
|
||||||
|
* @param success 是否执行成功
|
||||||
|
*/
|
||||||
|
public void setSuccess(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取结果消息。
|
||||||
|
*
|
||||||
|
* @return 结果消息
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置结果消息。
|
||||||
|
*
|
||||||
|
* @param message 结果消息
|
||||||
|
*/
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态远程命令结果注册表。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AgentRuntimeCommandResultRegistry {
|
||||||
|
|
||||||
|
private static final String RESULT_PREFIX = "easyflow:agent:runtime:command-result:";
|
||||||
|
private static final long POLL_INTERVAL_MILLIS = 50L;
|
||||||
|
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final AgentRuntimeProperties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 运行态远程命令结果注册表。
|
||||||
|
*
|
||||||
|
* @param stringRedisTemplate Redis 字符串模板
|
||||||
|
* @param objectMapper JSON 序列化器
|
||||||
|
* @param properties Agent 运行配置
|
||||||
|
*/
|
||||||
|
public AgentRuntimeCommandResultRegistry(StringRedisTemplate stringRedisTemplate,
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
AgentRuntimeProperties properties) {
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入成功结果。
|
||||||
|
*
|
||||||
|
* @param commandId 命令 ID
|
||||||
|
*/
|
||||||
|
public void markSuccess(String commandId) {
|
||||||
|
AgentRuntimeCommandResult result = new AgentRuntimeCommandResult();
|
||||||
|
result.setSuccess(true);
|
||||||
|
result.setMessage("OK");
|
||||||
|
writeResult(commandId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入失败结果。
|
||||||
|
*
|
||||||
|
* @param commandId 命令 ID
|
||||||
|
* @param message 失败消息
|
||||||
|
*/
|
||||||
|
public void markFailure(String commandId, String message) {
|
||||||
|
AgentRuntimeCommandResult result = new AgentRuntimeCommandResult();
|
||||||
|
result.setSuccess(false);
|
||||||
|
result.setMessage(message == null || message.isBlank() ? "Agent 运行节点不可用,请重新发起对话" : message);
|
||||||
|
writeResult(commandId, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待远程命令结果。
|
||||||
|
*
|
||||||
|
* @param commandId 命令 ID
|
||||||
|
* @return 命令结果
|
||||||
|
*/
|
||||||
|
public AgentRuntimeCommandResult waitForResult(String commandId) {
|
||||||
|
long deadline = System.nanoTime() + properties.getCommandResultTimeout().toNanos();
|
||||||
|
while (System.nanoTime() <= deadline) {
|
||||||
|
AgentRuntimeCommandResult result = readResult(commandId);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
sleep();
|
||||||
|
}
|
||||||
|
throw new BusinessException("Agent 运行节点响应超时,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除远程命令结果。
|
||||||
|
*
|
||||||
|
* @param commandId 命令 ID
|
||||||
|
*/
|
||||||
|
public void deleteResult(String commandId) {
|
||||||
|
if (commandId == null || commandId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.delete(resultKey(commandId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentRuntimeCommandResult readResult(String commandId) {
|
||||||
|
if (commandId == null || commandId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = stringRedisTemplate.opsForValue().get(resultKey(commandId));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(value, AgentRuntimeCommandResult.class);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new BusinessException("Agent 运行命令结果解析失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeResult(String commandId, AgentRuntimeCommandResult result) {
|
||||||
|
if (commandId == null || commandId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.opsForValue().set(
|
||||||
|
resultKey(commandId),
|
||||||
|
objectMapper.writeValueAsString(result),
|
||||||
|
properties.getCommandResultTtl());
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("Agent 运行命令结果序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resultKey(String commandId) {
|
||||||
|
return RESULT_PREFIX + commandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(POLL_INTERVAL_MILLIS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new BusinessException("Agent 运行节点响应等待被中断");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行节点心跳维护器。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AgentRuntimeNodeHeartbeat {
|
||||||
|
|
||||||
|
private static final Duration HEARTBEAT_TTL = Duration.ofSeconds(90);
|
||||||
|
|
||||||
|
private final AgentRuntimeRouteRegistry routeRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 运行节点心跳维护器。
|
||||||
|
*
|
||||||
|
* @param routeRegistry Agent 运行态 Redis 路由注册表
|
||||||
|
*/
|
||||||
|
public AgentRuntimeNodeHeartbeat(AgentRuntimeRouteRegistry routeRegistry) {
|
||||||
|
this.routeRegistry = routeRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时立即写入一次当前节点心跳。
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定期刷新当前节点心跳。
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelayString = "${easyflow.agent.runtime.node-heartbeat-delay:30000}", initialDelay = 30000L)
|
||||||
|
public void refresh() {
|
||||||
|
routeRegistry.heartbeat(HEARTBEAT_TTL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态 owner 路由。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeRoute {
|
||||||
|
|
||||||
|
private String nodeId;
|
||||||
|
private String bootId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 owner 节点 ID。
|
||||||
|
*
|
||||||
|
* @return owner 节点 ID
|
||||||
|
*/
|
||||||
|
public String getNodeId() {
|
||||||
|
return nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 owner 节点 ID。
|
||||||
|
*
|
||||||
|
* @param nodeId owner 节点 ID
|
||||||
|
*/
|
||||||
|
public void setNodeId(String nodeId) {
|
||||||
|
this.nodeId = nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 owner 启动代 ID。
|
||||||
|
*
|
||||||
|
* @return 启动代 ID
|
||||||
|
*/
|
||||||
|
public String getBootId() {
|
||||||
|
return bootId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 owner 启动代 ID。
|
||||||
|
*
|
||||||
|
* @param bootId 启动代 ID
|
||||||
|
*/
|
||||||
|
public void setBootId(String bootId) {
|
||||||
|
this.bootId = bootId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package tech.easyflow.agent.distributed;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行态 Redis 路由注册表。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class AgentRuntimeRouteRegistry {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(AgentRuntimeRouteRegistry.class);
|
||||||
|
|
||||||
|
private static final String REQUEST_ROUTE_PREFIX = "easyflow:agent:runtime:request:";
|
||||||
|
private static final String TOKEN_ROUTE_PREFIX = "easyflow:agent:runtime:resume-token:";
|
||||||
|
private static final String NODE_HEARTBEAT_PREFIX = "easyflow:agent:runtime:node:";
|
||||||
|
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
private final AgentRuntimeProperties properties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Agent 运行态 Redis 路由注册表。
|
||||||
|
*
|
||||||
|
* @param stringRedisTemplate Redis 字符串模板
|
||||||
|
* @param properties Agent 运行配置
|
||||||
|
* @param objectMapper JSON 序列化器
|
||||||
|
*/
|
||||||
|
public AgentRuntimeRouteRegistry(StringRedisTemplate stringRedisTemplate,
|
||||||
|
AgentRuntimeProperties properties,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
this.properties = properties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册运行请求 owner 节点。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
*/
|
||||||
|
public void registerRun(String requestId) {
|
||||||
|
if (requestId == null || requestId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.opsForValue().set(requestKey(requestId), serializeRoute(currentRoute()), properties.getRouteTtl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册恢复令牌与请求 ID 的关系。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
*/
|
||||||
|
public void registerResumeToken(String requestId, String resumeToken) {
|
||||||
|
if (requestId == null || requestId.isBlank() || resumeToken == null || resumeToken.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.opsForValue().set(tokenKey(resumeToken), requestId, properties.getRouteTtl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询请求 ID 所属节点。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @return owner 节点 ID
|
||||||
|
*/
|
||||||
|
public String findOwnerNode(String requestId) {
|
||||||
|
AgentRuntimeRoute route = findOwnerRoute(requestId);
|
||||||
|
return route == null ? null : route.getNodeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询请求 ID 所属路由。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @return owner 路由
|
||||||
|
*/
|
||||||
|
public AgentRuntimeRoute findOwnerRoute(String requestId) {
|
||||||
|
if (requestId == null || requestId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = stringRedisTemplate.opsForValue().get(requestKey(requestId));
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return deserializeRoute(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据恢复令牌查询请求 ID。
|
||||||
|
*
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @return 请求 ID
|
||||||
|
*/
|
||||||
|
public String findRequestIdByResumeToken(String resumeToken) {
|
||||||
|
if (resumeToken == null || resumeToken.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stringRedisTemplate.opsForValue().get(tokenKey(resumeToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定运行请求的路由。
|
||||||
|
*
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
*/
|
||||||
|
public void removeRun(String requestId) {
|
||||||
|
if (requestId == null || requestId.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteQuietly(requestKey(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定恢复令牌的路由。
|
||||||
|
*
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
*/
|
||||||
|
public void removeResumeToken(String resumeToken) {
|
||||||
|
if (resumeToken == null || resumeToken.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deleteQuietly(tokenKey(resumeToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前节点 ID。
|
||||||
|
*
|
||||||
|
* @return 当前节点 ID
|
||||||
|
*/
|
||||||
|
public String currentNodeId() {
|
||||||
|
return properties.getInstanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前节点存活心跳。
|
||||||
|
*
|
||||||
|
* @param ttl 心跳 TTL
|
||||||
|
*/
|
||||||
|
public void heartbeat(Duration ttl) {
|
||||||
|
stringRedisTemplate.opsForValue().set(nodeKey(properties.getInstanceId()), properties.getBootId(), ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定节点是否仍有存活心跳。
|
||||||
|
*
|
||||||
|
* @param nodeId 节点 ID
|
||||||
|
* @return true 表示节点心跳仍有效
|
||||||
|
*/
|
||||||
|
public boolean isNodeAlive(String nodeId) {
|
||||||
|
return currentNodeBootId(nodeId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定节点当前启动代 ID。
|
||||||
|
*
|
||||||
|
* @param nodeId 节点 ID
|
||||||
|
* @return 启动代 ID
|
||||||
|
*/
|
||||||
|
public String currentNodeBootId(String nodeId) {
|
||||||
|
if (nodeId == null || nodeId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stringRedisTemplate.opsForValue().get(nodeKey(nodeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requestKey(String requestId) {
|
||||||
|
return REQUEST_ROUTE_PREFIX + requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tokenKey(String resumeToken) {
|
||||||
|
return TOKEN_ROUTE_PREFIX + resumeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nodeKey(String nodeId) {
|
||||||
|
return NODE_HEARTBEAT_PREFIX + nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentRuntimeRoute currentRoute() {
|
||||||
|
AgentRuntimeRoute route = new AgentRuntimeRoute();
|
||||||
|
route.setNodeId(properties.getInstanceId());
|
||||||
|
route.setBootId(properties.getBootId());
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String serializeRoute(AgentRuntimeRoute route) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(route);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("Agent 运行路由序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentRuntimeRoute deserializeRoute(String value) {
|
||||||
|
try {
|
||||||
|
if (value.trim().startsWith("{")) {
|
||||||
|
return objectMapper.readValue(value, AgentRuntimeRoute.class);
|
||||||
|
}
|
||||||
|
AgentRuntimeRoute legacyRoute = new AgentRuntimeRoute();
|
||||||
|
legacyRoute.setNodeId(value);
|
||||||
|
return legacyRoute;
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("Agent 运行路由反序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteQuietly(String key) {
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.delete(key);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
LOG.warn("清理 Agent 运行态 Redis 路由失败: key={}", key, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 主实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent")
|
||||||
|
public class Agent extends DateEntity implements VisibilityResource, Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger deptId;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String avatar;
|
||||||
|
private BigInteger categoryId;
|
||||||
|
private BigInteger modelId;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> modelConfigJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> generationConfigJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> promptConfigJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> memoryConfigJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> executionConfigJson = new LinkedHashMap<>();
|
||||||
|
private Integer status;
|
||||||
|
private String visibilityScope;
|
||||||
|
private String publishStatus;
|
||||||
|
private BigInteger currentApprovalInstanceId;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> publishedSnapshotJson = new LinkedHashMap<>();
|
||||||
|
private Date publishedAt;
|
||||||
|
private BigInteger publishedBy;
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Boolean approvalPending;
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String currentApprovalActionType;
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String displayPublishStatus;
|
||||||
|
@Column(ignore = true)
|
||||||
|
private String createdByName;
|
||||||
|
@Column(ignore = true)
|
||||||
|
private List<AgentToolBinding> toolBindings;
|
||||||
|
@Column(ignore = true)
|
||||||
|
private List<AgentKnowledgeBinding> knowledgeBindings;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getDeptId() { return deptId; }
|
||||||
|
public void setDeptId(BigInteger deptId) { this.deptId = deptId; }
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
public String getDescription() { return description; }
|
||||||
|
public void setDescription(String description) { this.description = description; }
|
||||||
|
public String getAvatar() { return avatar; }
|
||||||
|
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||||
|
public BigInteger getCategoryId() { return categoryId; }
|
||||||
|
public void setCategoryId(BigInteger categoryId) { this.categoryId = categoryId; }
|
||||||
|
public BigInteger getModelId() { return modelId; }
|
||||||
|
public void setModelId(BigInteger modelId) { this.modelId = modelId; }
|
||||||
|
public Map<String, Object> getModelConfigJson() { return modelConfigJson; }
|
||||||
|
public void setModelConfigJson(Map<String, Object> modelConfigJson) { this.modelConfigJson = modelConfigJson == null ? new LinkedHashMap<>() : modelConfigJson; }
|
||||||
|
public Map<String, Object> getGenerationConfigJson() { return generationConfigJson; }
|
||||||
|
public void setGenerationConfigJson(Map<String, Object> generationConfigJson) { this.generationConfigJson = generationConfigJson == null ? new LinkedHashMap<>() : generationConfigJson; }
|
||||||
|
public Map<String, Object> getPromptConfigJson() { return promptConfigJson; }
|
||||||
|
public void setPromptConfigJson(Map<String, Object> promptConfigJson) { this.promptConfigJson = promptConfigJson == null ? new LinkedHashMap<>() : promptConfigJson; }
|
||||||
|
public Map<String, Object> getMemoryConfigJson() { return memoryConfigJson; }
|
||||||
|
public void setMemoryConfigJson(Map<String, Object> memoryConfigJson) { this.memoryConfigJson = memoryConfigJson == null ? new LinkedHashMap<>() : memoryConfigJson; }
|
||||||
|
public Map<String, Object> getExecutionConfigJson() { return executionConfigJson; }
|
||||||
|
public void setExecutionConfigJson(Map<String, Object> executionConfigJson) { this.executionConfigJson = executionConfigJson == null ? new LinkedHashMap<>() : executionConfigJson; }
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
public String getVisibilityScope() { return visibilityScope; }
|
||||||
|
public void setVisibilityScope(String visibilityScope) { 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 == null ? new LinkedHashMap<>() : 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; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
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; }
|
||||||
|
public String getCreatedByName() { return createdByName; }
|
||||||
|
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
|
||||||
|
public List<AgentToolBinding> getToolBindings() { return toolBindings; }
|
||||||
|
public void setToolBindings(List<AgentToolBinding> toolBindings) { this.toolBindings = toolBindings; }
|
||||||
|
public List<AgentKnowledgeBinding> getKnowledgeBindings() { return knowledgeBindings; }
|
||||||
|
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) { this.knowledgeBindings = knowledgeBindings; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 分类实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_category")
|
||||||
|
public class AgentCategory extends DateEntity implements Serializable {
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private String categoryName;
|
||||||
|
private Integer sortNo;
|
||||||
|
private Integer status;
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public String getCategoryName() { return categoryName; }
|
||||||
|
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
|
||||||
|
public Integer getSortNo() { return sortNo; }
|
||||||
|
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||||
|
public Integer getStatus() { return status; }
|
||||||
|
public void setStatus(Integer status) { this.status = status; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 工具审批挂起态实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_hitl_pending")
|
||||||
|
public class AgentHitlPending extends DateEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger agentId;
|
||||||
|
private BigInteger chatSessionId;
|
||||||
|
private String runtimeSessionId;
|
||||||
|
private String requestId;
|
||||||
|
private String resumeToken;
|
||||||
|
private String toolCallId;
|
||||||
|
private String toolName;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> toolInputJson = new LinkedHashMap<>();
|
||||||
|
private String status;
|
||||||
|
private String rejectReason;
|
||||||
|
private Date expiresAt;
|
||||||
|
private Date consumedAt;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||||
|
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||||
|
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||||
|
public String getRuntimeSessionId() { return runtimeSessionId; }
|
||||||
|
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
|
||||||
|
public String getRequestId() { return requestId; }
|
||||||
|
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||||
|
public String getResumeToken() { return resumeToken; }
|
||||||
|
public void setResumeToken(String resumeToken) { this.resumeToken = resumeToken; }
|
||||||
|
public String getToolCallId() { return toolCallId; }
|
||||||
|
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
|
||||||
|
public String getToolName() { return toolName; }
|
||||||
|
public void setToolName(String toolName) { this.toolName = toolName; }
|
||||||
|
public Map<String, Object> getToolInputJson() { return toolInputJson; }
|
||||||
|
public void setToolInputJson(Map<String, Object> toolInputJson) { this.toolInputJson = toolInputJson == null ? new LinkedHashMap<>() : toolInputJson; }
|
||||||
|
public String getStatus() { return status; }
|
||||||
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
public String getRejectReason() { return rejectReason; }
|
||||||
|
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
|
||||||
|
public Date getExpiresAt() { return expiresAt; }
|
||||||
|
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||||
|
public Date getConsumedAt() { return consumedAt; }
|
||||||
|
public void setConsumedAt(Date consumedAt) { this.consumedAt = consumedAt; }
|
||||||
|
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||||
|
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 知识库绑定实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_knowledge_binding")
|
||||||
|
public class AgentKnowledgeBinding extends DateEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger agentId;
|
||||||
|
private BigInteger knowledgeId;
|
||||||
|
private String retrievalMode;
|
||||||
|
private Boolean enabled;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> optionsJson = new LinkedHashMap<>();
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
|
||||||
|
private Integer sortNo;
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||||
|
public BigInteger getKnowledgeId() { return knowledgeId; }
|
||||||
|
public void setKnowledgeId(BigInteger knowledgeId) { this.knowledgeId = knowledgeId; }
|
||||||
|
public String getRetrievalMode() { return retrievalMode; }
|
||||||
|
public void setRetrievalMode(String retrievalMode) { this.retrievalMode = retrievalMode; }
|
||||||
|
public Boolean getEnabled() { return enabled; }
|
||||||
|
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||||
|
public Map<String, Object> getOptionsJson() { return optionsJson; }
|
||||||
|
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
|
||||||
|
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
|
||||||
|
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
|
||||||
|
public Map<String, Object> getResourceSummary() { return resourceSummary; }
|
||||||
|
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
|
||||||
|
public Integer getSortNo() { return sortNo; }
|
||||||
|
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行事件摘要实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_run_event")
|
||||||
|
public class AgentRunEventRecord implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger agentId;
|
||||||
|
private BigInteger chatSessionId;
|
||||||
|
private BigInteger roundId;
|
||||||
|
private Integer roundNo;
|
||||||
|
private Integer variantIndex;
|
||||||
|
private String requestId;
|
||||||
|
private String eventId;
|
||||||
|
private String eventType;
|
||||||
|
private String eventPhase;
|
||||||
|
private String toolCallId;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> payloadJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||||
|
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||||
|
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||||
|
public BigInteger getRoundId() { return roundId; }
|
||||||
|
public void setRoundId(BigInteger roundId) { this.roundId = roundId; }
|
||||||
|
public Integer getRoundNo() { return roundNo; }
|
||||||
|
public void setRoundNo(Integer roundNo) { this.roundNo = roundNo; }
|
||||||
|
public Integer getVariantIndex() { return variantIndex; }
|
||||||
|
public void setVariantIndex(Integer variantIndex) { this.variantIndex = variantIndex; }
|
||||||
|
public String getRequestId() { return requestId; }
|
||||||
|
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||||
|
public String getEventId() { return eventId; }
|
||||||
|
public void setEventId(String eventId) { this.eventId = eventId; }
|
||||||
|
public String getEventType() { return eventType; }
|
||||||
|
public void setEventType(String eventType) { this.eventType = eventType; }
|
||||||
|
public String getEventPhase() { return eventPhase; }
|
||||||
|
public void setEventPhase(String eventPhase) { this.eventPhase = eventPhase; }
|
||||||
|
public String getToolCallId() { return toolCallId; }
|
||||||
|
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
|
||||||
|
public Map<String, Object> getPayloadJson() { return payloadJson; }
|
||||||
|
public void setPayloadJson(Map<String, Object> payloadJson) { this.payloadJson = payloadJson == null ? new LinkedHashMap<>() : payloadJson; }
|
||||||
|
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||||
|
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||||
|
public Date getCreated() { return created; }
|
||||||
|
public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentScope 会话状态实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_session")
|
||||||
|
public class AgentSession extends DateEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger agentId;
|
||||||
|
private BigInteger chatSessionId;
|
||||||
|
private String runtimeSessionId;
|
||||||
|
private String sessionKey;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> stateJson = new LinkedHashMap<>();
|
||||||
|
private Long version;
|
||||||
|
private Long cacheVersion;
|
||||||
|
private Date lastAccessAt;
|
||||||
|
private Date expiresAt;
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
private Integer isDeleted;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||||
|
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||||
|
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||||
|
public String getRuntimeSessionId() { return runtimeSessionId; }
|
||||||
|
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
|
||||||
|
public String getSessionKey() { return sessionKey; }
|
||||||
|
public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; }
|
||||||
|
public Map<String, Object> getStateJson() { return stateJson; }
|
||||||
|
public void setStateJson(Map<String, Object> stateJson) { this.stateJson = stateJson == null ? new LinkedHashMap<>() : stateJson; }
|
||||||
|
public Long getVersion() { return version; }
|
||||||
|
public void setVersion(Long version) { this.version = version; }
|
||||||
|
public Long getCacheVersion() { return cacheVersion; }
|
||||||
|
public void setCacheVersion(Long cacheVersion) { this.cacheVersion = cacheVersion; }
|
||||||
|
public Date getLastAccessAt() { return lastAccessAt; }
|
||||||
|
public void setLastAccessAt(Date lastAccessAt) { this.lastAccessAt = lastAccessAt; }
|
||||||
|
public Date getExpiresAt() { return expiresAt; }
|
||||||
|
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
public Integer getIsDeleted() { return isDeleted; }
|
||||||
|
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package tech.easyflow.agent.entity;
|
||||||
|
|
||||||
|
import com.mybatisflex.annotation.Column;
|
||||||
|
import com.mybatisflex.annotation.Id;
|
||||||
|
import com.mybatisflex.annotation.KeyType;
|
||||||
|
import com.mybatisflex.annotation.Table;
|
||||||
|
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||||
|
import tech.easyflow.common.entity.DateEntity;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 工具绑定实体。
|
||||||
|
*/
|
||||||
|
@Table("tb_agent_tool_binding")
|
||||||
|
public class AgentToolBinding extends DateEntity implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||||
|
private BigInteger id;
|
||||||
|
@Column(tenantId = true)
|
||||||
|
private BigInteger tenantId;
|
||||||
|
private BigInteger agentId;
|
||||||
|
private String toolType;
|
||||||
|
private BigInteger targetId;
|
||||||
|
private String toolName;
|
||||||
|
private Boolean enabled;
|
||||||
|
private Boolean hitlEnabled;
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> hitlConfigJson = new LinkedHashMap<>();
|
||||||
|
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||||
|
private Map<String, Object> optionsJson = new LinkedHashMap<>();
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
|
||||||
|
@Column(ignore = true)
|
||||||
|
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
|
||||||
|
private Integer sortNo;
|
||||||
|
private Date created;
|
||||||
|
private BigInteger createdBy;
|
||||||
|
private Date modified;
|
||||||
|
private BigInteger modifiedBy;
|
||||||
|
|
||||||
|
public BigInteger getId() { return id; }
|
||||||
|
public void setId(BigInteger id) { this.id = id; }
|
||||||
|
public BigInteger getTenantId() { return tenantId; }
|
||||||
|
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||||
|
public BigInteger getAgentId() { return agentId; }
|
||||||
|
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||||
|
public String getToolType() { return toolType; }
|
||||||
|
public void setToolType(String toolType) { this.toolType = toolType; }
|
||||||
|
public BigInteger getTargetId() { return targetId; }
|
||||||
|
public void setTargetId(BigInteger targetId) { this.targetId = targetId; }
|
||||||
|
public String getToolName() { return toolName; }
|
||||||
|
public void setToolName(String toolName) { this.toolName = toolName; }
|
||||||
|
public Boolean getEnabled() { return enabled; }
|
||||||
|
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||||
|
public Boolean getHitlEnabled() { return hitlEnabled; }
|
||||||
|
public void setHitlEnabled(Boolean hitlEnabled) { this.hitlEnabled = hitlEnabled; }
|
||||||
|
public Map<String, Object> getHitlConfigJson() { return hitlConfigJson; }
|
||||||
|
public void setHitlConfigJson(Map<String, Object> hitlConfigJson) { this.hitlConfigJson = hitlConfigJson == null ? new LinkedHashMap<>() : hitlConfigJson; }
|
||||||
|
public Map<String, Object> getOptionsJson() { return optionsJson; }
|
||||||
|
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
|
||||||
|
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
|
||||||
|
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
|
||||||
|
public Map<String, Object> getResourceSummary() { return resourceSummary; }
|
||||||
|
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
|
||||||
|
public Integer getSortNo() { return sortNo; }
|
||||||
|
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||||
|
@Override public Date getCreated() { return created; }
|
||||||
|
@Override public void setCreated(Date created) { this.created = created; }
|
||||||
|
public BigInteger getCreatedBy() { return createdBy; }
|
||||||
|
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||||
|
@Override public Date getModified() { return modified; }
|
||||||
|
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||||
|
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||||
|
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package tech.easyflow.agent.enums;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 工具绑定类型。
|
||||||
|
*/
|
||||||
|
public enum AgentToolType {
|
||||||
|
|
||||||
|
WORKFLOW,
|
||||||
|
PLUGIN,
|
||||||
|
MCP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析工具类型。
|
||||||
|
*
|
||||||
|
* @param value 类型值
|
||||||
|
* @return 工具类型
|
||||||
|
*/
|
||||||
|
public static AgentToolType from(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("toolType 不能为空");
|
||||||
|
}
|
||||||
|
return AgentToolType.valueOf(value.trim().toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 分类 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentCategoryMapper extends BaseMapper<AgentCategory> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 工具审批挂起态 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentHitlPendingMapper extends BaseMapper<AgentHitlPending> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 知识库绑定 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentKnowledgeBindingMapper extends BaseMapper<AgentKnowledgeBinding> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentMapper extends BaseMapper<Agent> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentRunEventRecord;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 运行事件摘要 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentRunEventRecordMapper extends BaseMapper<AgentRunEventRecord> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentScope 会话状态 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentSessionMapper extends BaseMapper<AgentSession> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.agent.mapper;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.BaseMapper;
|
||||||
|
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 工具绑定 Mapper。
|
||||||
|
*/
|
||||||
|
public interface AgentToolBindingMapper extends BaseMapper<AgentToolBinding> {
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user