Compare commits
10 Commits
1ecc28e498
...
7e7c236c2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e7c236c2a | |||
| bb72e19c84 | |||
| b5dd427920 | |||
| 2592a1f09d | |||
| a402d920be | |||
| 58d6fb4e63 | |||
| 029f290c1b | |||
| acbf087bf3 | |||
| a4f75a5e4c | |||
| 25e80433a5 |
@@ -118,7 +118,32 @@ services:
|
||||
minio-init:
|
||||
condition: service_completed_successfully
|
||||
ports:
|
||||
- "19530:19530"
|
||||
- "39530:19530"
|
||||
- "9091:9091"
|
||||
volumes:
|
||||
- ./data/milvus:/var/lib/milvus
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.5
|
||||
container_name: easyflow-elasticsearch
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
discovery.type: single-node
|
||||
ELASTIC_PASSWORD: elastic
|
||||
xpack.security.enabled: "true"
|
||||
ES_JAVA_OPTS: -Xms1g -Xmx1g
|
||||
ports:
|
||||
- "39200:9200"
|
||||
volumes:
|
||||
- ./data/elasticsearch:/usr/share/elasticsearch/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -fsS -u elastic:elastic http://127.0.0.1:9200 >/dev/null || exit 1",
|
||||
]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
start_period: 60s
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
### 1. 产品主题与总规范
|
||||
|
||||
- [easyflow-product-design-spec.md](./easyflow-product-design-spec.md)
|
||||
- [easyflow-product-design.md](easyflow-product-design.md)
|
||||
|
||||
适用场景:
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
<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-auth</artifactId>
|
||||
@@ -29,4 +33,4 @@
|
||||
<artifactId>easyflow-common-captcha</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -22,11 +22,15 @@ import tech.easyflow.ai.service.*;
|
||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.core.runtime.ChatChannel;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
@@ -158,7 +162,15 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
if (errorEmitter != null) {
|
||||
return errorEmitter;
|
||||
}
|
||||
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
|
||||
return botService.startChat(
|
||||
botId,
|
||||
prompt,
|
||||
conversationId,
|
||||
messages,
|
||||
chatCheckResult,
|
||||
attachments,
|
||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments)
|
||||
);
|
||||
}
|
||||
|
||||
@PostMapping("updateLlmId")
|
||||
@@ -319,6 +331,24 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
||||
return result;
|
||||
}
|
||||
|
||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setChannel(ChatChannel.ADMIN);
|
||||
context.setSessionId(conversationId);
|
||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
||||
context.setUserAccount(account == null ? "admin" : account.getLoginName());
|
||||
context.setUserName(account == null ? "管理员" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||
context.setAttachments(attachments);
|
||||
return context;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import tech.easyflow.ai.dto.BotKnowledgeBindingRequest;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot;
|
||||
@@ -63,20 +64,21 @@ public class BotDocumentCollectionController extends BaseCurdController<BotDocum
|
||||
}
|
||||
|
||||
@PostMapping("updateBotKnowledgeIds")
|
||||
public Result<?> save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) {
|
||||
if (knowledgeIds != null) {
|
||||
for (BigInteger knowledgeId : knowledgeIds) {
|
||||
if (knowledgeId == null) {
|
||||
public Result<?> save(@JsonBody("botId") BigInteger botId,
|
||||
@JsonBody("knowledgeBindings") List<BotKnowledgeBindingRequest> knowledgeBindings) {
|
||||
if (knowledgeBindings != null) {
|
||||
for (BotKnowledgeBindingRequest binding : knowledgeBindings) {
|
||||
if (binding == null || binding.getKnowledgeId() == null) {
|
||||
continue;
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(knowledgeId);
|
||||
DocumentCollection collection = documentCollectionService.getById(binding.getKnowledgeId());
|
||||
if (collection == null) {
|
||||
continue;
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库");
|
||||
}
|
||||
}
|
||||
service.saveBotAndKnowledge(botId, knowledgeIds);
|
||||
service.saveBotAndKnowledge(botId, knowledgeBindings);
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
|
||||
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.domain.query.ChatSessionFilterQuery;
|
||||
import tech.easyflow.chatlog.service.ChatHistoryManageService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/chatHistory")
|
||||
public class ChatHistoryController {
|
||||
|
||||
private final ChatHistoryManageService chatHistoryManageService;
|
||||
|
||||
public ChatHistoryController(ChatHistoryManageService chatHistoryManageService) {
|
||||
this.chatHistoryManageService = chatHistoryManageService;
|
||||
}
|
||||
|
||||
@GetMapping("/sessions")
|
||||
public Result<ChatSessionPage> listSessions(ChatSessionFilterQuery query) {
|
||||
return Result.ok(chatHistoryManageService.queryAdminSessions(query));
|
||||
}
|
||||
|
||||
@GetMapping("/sessions/{sessionId}")
|
||||
public Result<ChatSessionSummary> getSession(@PathVariable BigInteger sessionId) {
|
||||
return Result.ok(chatHistoryManageService.getAdminSession(sessionId));
|
||||
}
|
||||
|
||||
@GetMapping("/sessions/{sessionId}/messages")
|
||||
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||
return Result.ok(chatHistoryManageService.queryAdminMessages(sessionId, query));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.easyagents.rag.retrieval.RagRetrievalMetadataKeys;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -12,9 +13,12 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportDtos;
|
||||
import tech.easyflow.ai.dto.KnowledgeSearchResultItem;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
@@ -32,11 +36,15 @@ import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 控制层。
|
||||
@@ -105,13 +113,11 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
Map<String, Object> options = entity.getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(entity.getOptions());
|
||||
if (entity.getSearchEngineEnable() == null){
|
||||
entity.setSearchEngineEnable(false);
|
||||
}
|
||||
options.putIfAbsent(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true);
|
||||
options.putIfAbsent(DocumentCollection.KEY_RERANK_ENABLE, entity.getRerankModelId() != null);
|
||||
entity.setOptions(options);
|
||||
}
|
||||
normalizeInfrastructureFields(entity, isSave);
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
@@ -124,8 +130,16 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
idExpr = "#knowledgeId",
|
||||
denyMessage = "无权限访问知识库"
|
||||
)
|
||||
public Result<List<Document>> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) {
|
||||
return Result.ok(service.search(knowledgeId, keyword));
|
||||
public Result<List<KnowledgeSearchResultItem>> search(@RequestParam BigInteger knowledgeId,
|
||||
@RequestParam String keyword,
|
||||
@RequestParam(required = false) String retrievalMode) {
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(knowledgeId);
|
||||
request.setQuery(keyword);
|
||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode));
|
||||
request.setCallerType("API");
|
||||
request.setCallerId(String.valueOf(knowledgeId));
|
||||
return Result.ok(toKnowledgeSearchResult(service.search(request)));
|
||||
}
|
||||
|
||||
|
||||
@@ -234,4 +248,85 @@ public class DocumentCollectionController extends BaseCurdController<DocumentCol
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
private void normalizeInfrastructureFields(DocumentCollection entity, boolean isSave) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
if (isSave) {
|
||||
entity.setVectorStoreEnable(true);
|
||||
entity.setVectorStoreType("milvus");
|
||||
entity.setSearchEngineEnable(true);
|
||||
entity.setVectorStoreCollection(generateVectorCollectionName());
|
||||
return;
|
||||
}
|
||||
if (entity.getVectorStoreEnable() != null) {
|
||||
entity.setVectorStoreEnable(true);
|
||||
}
|
||||
if (entity.getVectorStoreType() != null) {
|
||||
entity.setVectorStoreType("milvus");
|
||||
}
|
||||
if (entity.getSearchEngineEnable() != null) {
|
||||
entity.setSearchEngineEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateVectorCollectionName() {
|
||||
return "kb_" + UUID.randomUUID().toString().replace("-", "").substring(0, 28);
|
||||
}
|
||||
|
||||
private List<KnowledgeSearchResultItem> toKnowledgeSearchResult(List<Document> documents) {
|
||||
List<KnowledgeSearchResultItem> results = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return results;
|
||||
}
|
||||
for (int index = 0; index < documents.size(); index++) {
|
||||
Document document = documents.get(index);
|
||||
if (document == null) {
|
||||
continue;
|
||||
}
|
||||
KnowledgeSearchResultItem item = new KnowledgeSearchResultItem();
|
||||
item.setSorting(index + 1);
|
||||
item.setContent(document.getContent());
|
||||
item.setScore(roundScore(document.getScore()));
|
||||
item.setHitSource(readMetadataAsString(document, RagRetrievalMetadataKeys.HIT_SOURCE));
|
||||
item.setVectorScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.VECTOR_SCORE)));
|
||||
item.setKeywordScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.KEYWORD_SCORE)));
|
||||
results.add(item);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private String readMetadataAsString(Document document, String key) {
|
||||
Object value = document == null ? null : document.getMetadata(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value);
|
||||
return StringUtils.hasText(text) ? text : null;
|
||||
}
|
||||
|
||||
private Double readMetadataAsDouble(Document document, String key) {
|
||||
Object value = document == null ? null : document.getMetadata(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
}
|
||||
if (value instanceof String && StringUtils.hasText((String) value)) {
|
||||
try {
|
||||
return Double.valueOf((String) value);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Double roundScore(Double value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return new BigDecimal(String.valueOf(value))
|
||||
.setScale(4, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.easyflow.admin.controller.ai;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult;
|
||||
import tech.easyflow.chatlog.service.PublicChatSessionRestoreService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/public-chat")
|
||||
public class PublicChatSessionController {
|
||||
|
||||
private final PublicChatSessionRestoreService publicChatSessionRestoreService;
|
||||
|
||||
public PublicChatSessionController(PublicChatSessionRestoreService publicChatSessionRestoreService) {
|
||||
this.publicChatSessionRestoreService = publicChatSessionRestoreService;
|
||||
}
|
||||
|
||||
@GetMapping("/session/restore")
|
||||
public Result<PublicChatSessionRestoreResult> restoreSession(BigInteger botId,
|
||||
BigInteger conversationId,
|
||||
Integer limit) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
BigInteger userId = account == null ? null : account.getId();
|
||||
PublicChatSessionRestoreResult result = publicChatSessionRestoreService.restoreSession(
|
||||
userId,
|
||||
botId,
|
||||
conversationId,
|
||||
limit
|
||||
);
|
||||
return Result.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
package tech.easyflow.admin.controller.auth;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
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 tech.easyflow.auth.entity.LoginDTO;
|
||||
import tech.easyflow.auth.entity.LoginVO;
|
||||
import tech.easyflow.auth.service.AuthService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
@@ -27,6 +28,12 @@ public class AuthController {
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
@PostMapping("loginByApiKey")
|
||||
@SaIgnore
|
||||
public Result<LoginVO> loginByApiKey(@JsonBody(value = "apiKey", required = true) String apiKey) {
|
||||
return Result.ok(authService.loginByApiKey(apiKey));
|
||||
}
|
||||
|
||||
@PostMapping("logout")
|
||||
public Result<Void> logout() {
|
||||
StpUtil.logout();
|
||||
|
||||
@@ -13,10 +13,13 @@ import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.core.runtime.ChatChannel;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* bot 接口
|
||||
@@ -51,6 +54,7 @@ public class PublicBotController {
|
||||
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
||||
}
|
||||
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
||||
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apikey);
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||
int size = chatRequestParams.getMessages().size();
|
||||
String prompt = null;
|
||||
@@ -62,7 +66,30 @@ public class PublicBotController {
|
||||
if (errorEmitter != null) {
|
||||
return errorEmitter;
|
||||
}
|
||||
return botService.startPublicChat(chatRequestParams.getBotId(), prompt, chatRequestParams.getMessages(), chatCheckResult);
|
||||
return botService.startPublicChat(
|
||||
chatRequestParams.getBotId(),
|
||||
prompt,
|
||||
chatRequestParams.getMessages(),
|
||||
chatCheckResult,
|
||||
buildRuntimeContext(chatCheckResult.getAiBot(), chatRequestParams.getConversationId(), prompt, sysApiKey)
|
||||
);
|
||||
}
|
||||
|
||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, String conversationId, String prompt, SysApiKey sysApiKey) {
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setChannel(ChatChannel.PUBLIC_API);
|
||||
context.setSessionId(new BigInteger(conversationId));
|
||||
context.setTenantId(BigInteger.ZERO);
|
||||
context.setDeptId(BigInteger.ZERO);
|
||||
context.setUserId(BigInteger.ZERO);
|
||||
context.setUserAccount("apikey:" + sysApiKey.getId());
|
||||
context.setUserName("API 调用方");
|
||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||
context.setSessionTitle(prompt != null && prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||
context.setAnonymous(true);
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,10 +20,14 @@
|
||||
<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-common-captcha</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -23,10 +23,13 @@ import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.core.runtime.ChatChannel;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
@@ -75,8 +78,6 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
@Resource
|
||||
private BotPluginService botPluginService;
|
||||
@Resource
|
||||
private BotConversationService conversationMessageService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@GetMapping("/generateConversationId")
|
||||
@@ -161,27 +162,16 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
if (errorEmitter != null) {
|
||||
return errorEmitter;
|
||||
}
|
||||
BotConversation conversation = conversationMessageService.getById(conversationId);
|
||||
if (conversation == null) {
|
||||
conversation = new BotConversation();
|
||||
conversation.setId(conversationId);
|
||||
if (prompt.length() > 200) {
|
||||
conversation.setTitle(prompt.substring(0, 200));
|
||||
} else {
|
||||
conversation.setTitle(prompt);
|
||||
}
|
||||
conversation.setBotId(botId);
|
||||
conversation.setAccountId(SaTokenUtil.getLoginAccount().getId());
|
||||
commonFiled(conversation, SaTokenUtil.getLoginAccount().getId(), SaTokenUtil.getLoginAccount().getTenantId(), SaTokenUtil.getLoginAccount().getDeptId());
|
||||
try {
|
||||
conversationMessageService.save(conversation);
|
||||
} catch (DuplicateKeyException e) {
|
||||
// 并发重试场景下允许重复创建请求,唯一主键冲突按已创建处理。
|
||||
log.debug("conversation already exists, conversationId={}", conversationId, e);
|
||||
}
|
||||
}
|
||||
|
||||
return botService.startChat(botId, prompt, conversationId, messages, chatCheckResult, attachments);
|
||||
return botService.startChat(
|
||||
botId,
|
||||
prompt,
|
||||
conversationId,
|
||||
messages,
|
||||
chatCheckResult,
|
||||
attachments,
|
||||
buildRuntimeContext(chatCheckResult.getAiBot(), conversationId, prompt, attachments)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -296,6 +286,25 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
private ChatRuntimeContext buildRuntimeContext(Bot bot, BigInteger conversationId, String prompt, List<String> attachments) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setChannel(ChatChannel.USER_CENTER);
|
||||
context.setSessionId(conversationId);
|
||||
context.setTenantId(account == null ? BigInteger.ZERO : account.getTenantId());
|
||||
context.setDeptId(account == null ? BigInteger.ZERO : account.getDeptId());
|
||||
context.setUserId(account == null ? BigInteger.ZERO : account.getId());
|
||||
context.setUserAccount(account == null ? "anonymous" : account.getLoginName());
|
||||
context.setUserName(account == null ? "匿名用户" : (StringUtils.hasText(account.getNickname()) ? account.getNickname() : account.getLoginName()));
|
||||
context.setAssistantId(bot == null ? BigInteger.ZERO : bot.getId());
|
||||
context.setAssistantCode(bot == null ? null : bot.getAlias());
|
||||
context.setAssistantName(bot == null ? null : bot.getTitle());
|
||||
context.setSessionTitle(prompt.length() > 200 ? prompt.substring(0, 200) : prompt);
|
||||
context.setAnonymous(account == null || BigInteger.ZERO.equals(account.getId()));
|
||||
context.setAttachments(attachments);
|
||||
return context;
|
||||
}
|
||||
|
||||
private Map<String, Object> getDefaultLlmOptions() {
|
||||
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
||||
defaultLlmOptions.put("temperature", 0.7);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import tech.easyflow.ai.entity.BotConversation;
|
||||
import tech.easyflow.ai.service.BotConversationService;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/userCenter/botConversation")
|
||||
@SaIgnore
|
||||
public class UcBotConversationController extends BaseCurdController<BotConversationService, BotConversation> {
|
||||
|
||||
@Resource
|
||||
private BotConversationService conversationMessageService;
|
||||
|
||||
public UcBotConversationController(BotConversationService service) {
|
||||
super(service);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定会话
|
||||
*/
|
||||
@GetMapping("/deleteConversation")
|
||||
public Result<Void> deleteConversation(String botId, String conversationId) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
conversationMessageService.deleteConversation(botId, conversationId, account.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
@GetMapping("/updateConversation")
|
||||
public Result<Void> updateConversation(String botId, String conversationId, String title) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
conversationMessageService.updateConversation(botId, conversationId, title, account.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<List<BotConversation>> list(BotConversation entity, Boolean asTree, String sortKey, String sortType) {
|
||||
entity.setAccountId(SaTokenUtil.getLoginAccount().getId());
|
||||
sortKey = "created";
|
||||
sortType = "desc";
|
||||
return super.list(entity, asTree, sortKey, sortType);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Result<?> onSaveOrUpdateBefore(BotConversation entity, boolean isSave) {
|
||||
entity.setAccountId(SaTokenUtil.getLoginAccount().getId());
|
||||
entity.setCreated(new Date());
|
||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询会话列表
|
||||
*
|
||||
* @param request 查询数据
|
||||
* @param sortKey 排序字段
|
||||
* @param sortType 排序方式 asc | desc
|
||||
* @param pageNumber 当前页码
|
||||
* @param pageSize 每页的数据量
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("pageList")
|
||||
public Result<Page<BotConversation>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
|
||||
if (pageNumber == null || pageNumber < 1) {
|
||||
pageNumber = 1L;
|
||||
}
|
||||
if (pageSize == null || pageSize < 1) {
|
||||
pageSize = 10L;
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = buildQueryWrapper(request);
|
||||
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
|
||||
Page<BotConversation> botConversationPage = service.getMapper().paginateWithRelations(pageNumber, pageSize, queryWrapper);
|
||||
return Result.ok(botConversationPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表主键查询数据详情。
|
||||
*
|
||||
* @param id 主键值
|
||||
* @return 内容详情
|
||||
*/
|
||||
@GetMapping("detail")
|
||||
@SaIgnore
|
||||
public Result<BotConversation> detail(String id) {
|
||||
if (tech.easyflow.common.util.StringUtil.noText(id)) {
|
||||
throw new BusinessException("id must not be null");
|
||||
}
|
||||
return Result.ok(service.getMapper().selectOneWithRelationsById(id));
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
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.ai.entity.BotMessage;
|
||||
import tech.easyflow.ai.service.BotMessageService;
|
||||
import tech.easyflow.ai.vo.ChatMessageVO;
|
||||
import tech.easyflow.common.annotation.UsePermission;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bot 消息记录表 控制层。
|
||||
*
|
||||
* @author michael
|
||||
* @since 2024-11-04
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/userCenter/botMessage")
|
||||
@UsePermission(moduleName = "/api/v1/bot")
|
||||
public class UcBotMessageController extends BaseCurdController<BotMessageService, BotMessage> {
|
||||
private final BotMessageService botMessageService;
|
||||
|
||||
public UcBotMessageController(BotMessageService service, BotMessageService botMessageService) {
|
||||
super(service);
|
||||
this.botMessageService = botMessageService;
|
||||
}
|
||||
|
||||
@GetMapping("/getMessages")
|
||||
@SaIgnore
|
||||
public Result<List<ChatMessageVO>> getMessages(BigInteger botId, BigInteger conversationId) {
|
||||
List<ChatMessageVO> res = new ArrayList<>();
|
||||
QueryWrapper w = QueryWrapper.create();
|
||||
w.eq(BotMessage::getBotId, botId);
|
||||
w.eq(BotMessage::getConversationId, conversationId);
|
||||
List<BotMessage> list = botMessageService.list(w);
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
for (BotMessage message : list) {
|
||||
ChatMessageVO vo = new ChatMessageVO();
|
||||
vo.setKey(message.getId().toString());
|
||||
vo.setRole(message.getRole());
|
||||
vo.setContent(JSON.parseObject(message.getContent()).getString("textContent"));
|
||||
vo.setPlacement("user".equals(message.getRole()) ? "end" : "start");
|
||||
vo.setCreated(message.getCreated());
|
||||
res.add(vo);
|
||||
}
|
||||
}
|
||||
return Result.ok(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tech.easyflow.usercenter.controller.ai;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
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.chatlog.domain.dto.ChatHistoryPage;
|
||||
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.ChatHistoryManageService;
|
||||
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;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/userCenter/chatHistory")
|
||||
@SaIgnore
|
||||
public class UcChatHistoryController {
|
||||
|
||||
private final ChatHistoryManageService chatHistoryManageService;
|
||||
|
||||
public UcChatHistoryController(ChatHistoryManageService chatHistoryManageService) {
|
||||
this.chatHistoryManageService = chatHistoryManageService;
|
||||
}
|
||||
|
||||
@GetMapping("/sessions")
|
||||
public Result<ChatSessionPage> listSessions(BigInteger assistantId, ChatPageQuery query) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.queryUserSessions(account.getId(), assistantId, query));
|
||||
}
|
||||
|
||||
@GetMapping("/sessions/{sessionId}")
|
||||
public Result<ChatSessionSummary> getSession(@PathVariable BigInteger sessionId) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.getUserSession(account.getId(), sessionId));
|
||||
}
|
||||
|
||||
@GetMapping("/sessions/{sessionId}/messages")
|
||||
public Result<ChatHistoryPage> queryMessages(@PathVariable BigInteger sessionId, ChatPageQuery query) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
return Result.ok(chatHistoryManageService.queryUserMessages(account.getId(), sessionId, query));
|
||||
}
|
||||
|
||||
@PostMapping("/sessions/{sessionId}/rename")
|
||||
public Result<Void> renameSession(@PathVariable BigInteger sessionId,
|
||||
@JsonBody(value = "title", required = true) String title) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
chatHistoryManageService.renameUserSession(account.getId(), sessionId, title, account.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/sessions/{sessionId}/delete")
|
||||
public Result<Void> deleteSession(@PathVariable BigInteger sessionId) {
|
||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||
chatHistoryManageService.deleteUserSession(account.getId(), sessionId, account.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@
|
||||
<artifactId>easyflow-common-all</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-ai</artifactId>
|
||||
@@ -23,6 +27,10 @@
|
||||
<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-file-storage</artifactId>
|
||||
|
||||
48
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
48
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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-commons</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-common-analytical-db</name>
|
||||
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-base</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.clickhouse</groupId>
|
||||
<artifactId>clickhouse-jdbc</artifactId>
|
||||
<classifier>all</classifier>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-database-clickhouse</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.common.analyticaldb.config;
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
|
||||
import tech.easyflow.common.analyticaldb.core.DefaultAnalyticalDBOperations;
|
||||
|
||||
@Configuration
|
||||
public class AnalyticalDBConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||
public AnalyticalDBResources analyticalDBResources(AnalyticalDBProperties properties) {
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setPoolName("easyflow-analytical-db");
|
||||
config.setDriverClassName(properties.getDriverClassName());
|
||||
config.setJdbcUrl(properties.getUrl());
|
||||
config.setUsername(properties.getUsername());
|
||||
config.setPassword(properties.getPassword());
|
||||
config.setMaximumPoolSize(properties.getPool().getMaxPoolSize());
|
||||
config.setMinimumIdle(properties.getPool().getMinIdle());
|
||||
config.setConnectionTimeout(properties.getPool().getConnectionTimeout());
|
||||
config.setValidationTimeout(properties.getPool().getValidationTimeout());
|
||||
config.setIdleTimeout(properties.getPool().getIdleTimeout());
|
||||
config.setMaxLifetime(properties.getPool().getMaxLifetime());
|
||||
config.setInitializationFailTimeout(-1L);
|
||||
HikariDataSource dataSource = new HikariDataSource(config);
|
||||
return new AnalyticalDBResources(dataSource, new JdbcTemplate(dataSource));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||
public AnalyticalDBOperations analyticalDBOperations(AnalyticalDBResources resources) {
|
||||
return new DefaultAnalyticalDBOperations(resources.jdbcTemplate());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true")
|
||||
public InitializingBean analyticalDBFlywayInitializer(AnalyticalDBResources resources,
|
||||
AnalyticalDBFlywayProperties properties) {
|
||||
return () -> Flyway.configure()
|
||||
.dataSource(resources.dataSource())
|
||||
.locations(splitLocations(properties.getLocations()))
|
||||
.table(properties.getTable())
|
||||
.baselineOnMigrate(properties.isBaselineOnMigrate())
|
||||
.validateOnMigrate(properties.isValidateOnMigrate())
|
||||
.load()
|
||||
.migrate();
|
||||
}
|
||||
|
||||
private String[] splitLocations(String locations) {
|
||||
if (!StringUtils.hasText(locations)) {
|
||||
return new String[]{"classpath:db/migration/analyticaldb"};
|
||||
}
|
||||
return StringUtils.commaDelimitedListToStringArray(locations);
|
||||
}
|
||||
|
||||
public record AnalyticalDBResources(HikariDataSource dataSource, JdbcTemplate jdbcTemplate) implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package tech.easyflow.common.analyticaldb.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "easyflow.flyway.analytical-db")
|
||||
public class AnalyticalDBFlywayProperties {
|
||||
|
||||
private String locations = "classpath:db/migration/analyticaldb";
|
||||
private String table = "flyway_schema_history_analytical_db";
|
||||
private boolean baselineOnMigrate = false;
|
||||
private boolean validateOnMigrate = true;
|
||||
|
||||
public String getLocations() {
|
||||
return locations;
|
||||
}
|
||||
|
||||
public void setLocations(String locations) {
|
||||
this.locations = locations;
|
||||
}
|
||||
|
||||
public String getTable() {
|
||||
return table;
|
||||
}
|
||||
|
||||
public void setTable(String table) {
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public boolean isBaselineOnMigrate() {
|
||||
return baselineOnMigrate;
|
||||
}
|
||||
|
||||
public void setBaselineOnMigrate(boolean baselineOnMigrate) {
|
||||
this.baselineOnMigrate = baselineOnMigrate;
|
||||
}
|
||||
|
||||
public boolean isValidateOnMigrate() {
|
||||
return validateOnMigrate;
|
||||
}
|
||||
|
||||
public void setValidateOnMigrate(boolean validateOnMigrate) {
|
||||
this.validateOnMigrate = validateOnMigrate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package tech.easyflow.common.analyticaldb.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "easyflow.analytical-db")
|
||||
public class AnalyticalDBProperties {
|
||||
|
||||
private boolean enabled = false;
|
||||
private String url;
|
||||
private String username;
|
||||
private String password;
|
||||
private String driverClassName = "com.clickhouse.jdbc.ClickHouseDriver";
|
||||
private Pool pool = new Pool();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getDriverClassName() {
|
||||
return driverClassName;
|
||||
}
|
||||
|
||||
public void setDriverClassName(String driverClassName) {
|
||||
this.driverClassName = driverClassName;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public void setPool(Pool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public static class Pool {
|
||||
|
||||
private int maxPoolSize = 10;
|
||||
private int minIdle = 1;
|
||||
private long connectionTimeout = 5000L;
|
||||
private long validationTimeout = 3000L;
|
||||
private long idleTimeout = 600000L;
|
||||
private long maxLifetime = 1800000L;
|
||||
|
||||
public int getMaxPoolSize() {
|
||||
return maxPoolSize;
|
||||
}
|
||||
|
||||
public void setMaxPoolSize(int maxPoolSize) {
|
||||
this.maxPoolSize = maxPoolSize;
|
||||
}
|
||||
|
||||
public int getMinIdle() {
|
||||
return minIdle;
|
||||
}
|
||||
|
||||
public void setMinIdle(int minIdle) {
|
||||
this.minIdle = minIdle;
|
||||
}
|
||||
|
||||
public long getConnectionTimeout() {
|
||||
return connectionTimeout;
|
||||
}
|
||||
|
||||
public void setConnectionTimeout(long connectionTimeout) {
|
||||
this.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
|
||||
public long getValidationTimeout() {
|
||||
return validationTimeout;
|
||||
}
|
||||
|
||||
public void setValidationTimeout(long validationTimeout) {
|
||||
this.validationTimeout = validationTimeout;
|
||||
}
|
||||
|
||||
public long getIdleTimeout() {
|
||||
return idleTimeout;
|
||||
}
|
||||
|
||||
public void setIdleTimeout(long idleTimeout) {
|
||||
this.idleTimeout = idleTimeout;
|
||||
}
|
||||
|
||||
public long getMaxLifetime() {
|
||||
return maxLifetime;
|
||||
}
|
||||
|
||||
public void setMaxLifetime(long maxLifetime) {
|
||||
this.maxLifetime = maxLifetime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tech.easyflow.common.analyticaldb.core;
|
||||
|
||||
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AnalyticalDBOperations {
|
||||
|
||||
boolean available();
|
||||
|
||||
void assertAvailable();
|
||||
|
||||
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args);
|
||||
|
||||
<T> T queryOne(String sql, Class<T> requiredType, Object... args);
|
||||
|
||||
<T> T queryOne(String sql, RowMapper<T> rowMapper, Object... args);
|
||||
|
||||
<T> List<T> queryForList(String sql, Class<T> elementType, Object... args);
|
||||
|
||||
int update(String sql, Object... args);
|
||||
|
||||
<T> int[][] batchUpdate(String sql, List<T> items, int batchSize, ParameterizedPreparedStatementSetter<T> setter);
|
||||
|
||||
<T> AnalyticalDBPageResult<T> page(String countSql,
|
||||
Object[] countArgs,
|
||||
String dataSql,
|
||||
Object[] dataArgs,
|
||||
AnalyticalDBPageRequest pageRequest,
|
||||
RowMapper<T> rowMapper);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package tech.easyflow.common.analyticaldb.core;
|
||||
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import tech.easyflow.common.analyticaldb.exception.AnalyticalDBException;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DefaultAnalyticalDBOperations implements AnalyticalDBOperations {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public DefaultAnalyticalDBOperations(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean available() {
|
||||
return jdbcTemplate != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertAvailable() {
|
||||
if (!available()) {
|
||||
throw new AnalyticalDBException("AnalyticalDB 数据源未启用");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
assertAvailable();
|
||||
return jdbcTemplate.query(sql, rowMapper, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T queryOne(String sql, Class<T> requiredType, Object... args) {
|
||||
assertAvailable();
|
||||
try {
|
||||
return jdbcTemplate.queryForObject(sql, requiredType, args);
|
||||
} catch (EmptyResultDataAccessException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T queryOne(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
assertAvailable();
|
||||
List<T> rows = jdbcTemplate.query(sql, rowMapper, args);
|
||||
return rows.isEmpty() ? null : rows.get(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> queryForList(String sql, Class<T> elementType, Object... args) {
|
||||
assertAvailable();
|
||||
return jdbcTemplate.queryForList(sql, elementType, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(String sql, Object... args) {
|
||||
assertAvailable();
|
||||
return jdbcTemplate.update(sql, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> int[][] batchUpdate(String sql, List<T> items, int batchSize, ParameterizedPreparedStatementSetter<T> setter) {
|
||||
assertAvailable();
|
||||
if (items == null || items.isEmpty()) {
|
||||
return new int[0][0];
|
||||
}
|
||||
return jdbcTemplate.batchUpdate(sql, items, Math.max(batchSize, 1), setter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> AnalyticalDBPageResult<T> page(String countSql,
|
||||
Object[] countArgs,
|
||||
String dataSql,
|
||||
Object[] dataArgs,
|
||||
AnalyticalDBPageRequest pageRequest,
|
||||
RowMapper<T> rowMapper) {
|
||||
assertAvailable();
|
||||
AnalyticalDBPageRequest request = pageRequest == null ? new AnalyticalDBPageRequest() : pageRequest;
|
||||
Long total = jdbcTemplate.queryForObject(countSql, Long.class, nullSafeArgs(countArgs));
|
||||
List<T> records = new ArrayList<>();
|
||||
if (total != null && total > 0) {
|
||||
Object[] argsWithPage = appendPageArgs(dataArgs, request.getSafePageSize(), request.getOffset());
|
||||
records = jdbcTemplate.query(dataSql + " LIMIT ? OFFSET ?", rowMapper, argsWithPage);
|
||||
}
|
||||
AnalyticalDBPageResult<T> result = new AnalyticalDBPageResult<>();
|
||||
result.setTotal(total == null ? 0L : total);
|
||||
result.setPageNumber(request.getPageNumber());
|
||||
result.setPageSize(request.getSafePageSize());
|
||||
result.setRecords(records);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object[] appendPageArgs(Object[] args, int limit, int offset) {
|
||||
Object[] safeArgs = nullSafeArgs(args);
|
||||
Object[] merged = new Object[safeArgs.length + 2];
|
||||
System.arraycopy(safeArgs, 0, merged, 0, safeArgs.length);
|
||||
merged[safeArgs.length] = limit;
|
||||
merged[safeArgs.length + 1] = offset;
|
||||
return merged;
|
||||
}
|
||||
|
||||
private Object[] nullSafeArgs(Object[] args) {
|
||||
return args == null ? new Object[0] : args;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.common.analyticaldb.exception;
|
||||
|
||||
public class AnalyticalDBException extends RuntimeException {
|
||||
|
||||
public AnalyticalDBException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AnalyticalDBException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package tech.easyflow.common.analyticaldb.page;
|
||||
|
||||
public class AnalyticalDBPageRequest {
|
||||
|
||||
private int pageNumber = 1;
|
||||
private int pageSize = 20;
|
||||
|
||||
public AnalyticalDBPageRequest() {
|
||||
}
|
||||
|
||||
public AnalyticalDBPageRequest(int pageNumber, int pageSize) {
|
||||
this.pageNumber = pageNumber;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public int getPageNumber() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
public void setPageNumber(int pageNumber) {
|
||||
this.pageNumber = pageNumber;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public int getOffset() {
|
||||
int safePageNumber = Math.max(pageNumber, 1);
|
||||
return (safePageNumber - 1) * getSafePageSize();
|
||||
}
|
||||
|
||||
public int getSafePageSize() {
|
||||
return Math.max(pageSize, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package tech.easyflow.common.analyticaldb.page;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AnalyticalDBPageResult<T> {
|
||||
|
||||
private long total;
|
||||
private int pageNumber;
|
||||
private int pageSize;
|
||||
private List<T> records = new ArrayList<>();
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total) {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public int getPageNumber() {
|
||||
return pageNumber;
|
||||
}
|
||||
|
||||
public void setPageNumber(int pageNumber) {
|
||||
this.pageNumber = pageNumber;
|
||||
}
|
||||
|
||||
public int getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
public void setPageSize(int pageSize) {
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
public List<T> getRecords() {
|
||||
return records;
|
||||
}
|
||||
|
||||
public void setRecords(List<T> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.common.analyticaldb.support;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.analyticaldb.config.AnalyticalDBFlywayProperties;
|
||||
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
|
||||
|
||||
@Component
|
||||
public class AnalyticalDBHealthSupport {
|
||||
|
||||
private final ObjectProvider<AnalyticalDBOperations> analyticalDBOperationsProvider;
|
||||
private final AnalyticalDBFlywayProperties flywayProperties;
|
||||
|
||||
public AnalyticalDBHealthSupport(ObjectProvider<AnalyticalDBOperations> analyticalDBOperationsProvider,
|
||||
AnalyticalDBFlywayProperties flywayProperties) {
|
||||
this.analyticalDBOperationsProvider = analyticalDBOperationsProvider;
|
||||
this.flywayProperties = flywayProperties;
|
||||
}
|
||||
|
||||
public boolean enabled() {
|
||||
return analyticalDBOperationsProvider.getIfAvailable() != null;
|
||||
}
|
||||
|
||||
public void selfCheck() {
|
||||
AnalyticalDBOperations operations = analyticalDBOperationsProvider.getIfAvailable();
|
||||
if (operations == null) {
|
||||
return;
|
||||
}
|
||||
operations.queryOne("SELECT 1", Integer.class);
|
||||
operations.queryOne("SELECT COUNT(1) FROM " + flywayProperties.getTable(), Long.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatAssistantAccumulator {
|
||||
|
||||
private final StringBuilder content = new StringBuilder();
|
||||
private final StringBuilder reasoning = new StringBuilder();
|
||||
private final List<Map<String, Object>> chains = new ArrayList<>();
|
||||
|
||||
public void appendContent(String delta) {
|
||||
if (delta != null && !delta.isEmpty()) {
|
||||
content.append(delta);
|
||||
}
|
||||
}
|
||||
|
||||
public void appendReasoning(String delta) {
|
||||
if (delta != null && !delta.isEmpty()) {
|
||||
reasoning.append(delta);
|
||||
}
|
||||
}
|
||||
|
||||
public void appendToolCall(String id, String name, Object arguments) {
|
||||
Map<String, Object> chain = findToolChain(id, name);
|
||||
chain.put("status", "TOOL_CALL");
|
||||
chain.put("result", arguments);
|
||||
}
|
||||
|
||||
public void appendToolResult(String id, String name, Object result) {
|
||||
Map<String, Object> chain = findToolChain(id, name);
|
||||
chain.put("status", "TOOL_RESULT");
|
||||
chain.put("result", result);
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
public Map<String, Object> buildPayload() {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
List<Map<String, Object>> payloadChains = new ArrayList<>();
|
||||
if (reasoning.length() > 0) {
|
||||
Map<String, Object> think = new LinkedHashMap<>();
|
||||
think.put("reasoning_content", reasoning.toString());
|
||||
think.put("thinkingStatus", "end");
|
||||
think.put("thinlCollapse", Boolean.TRUE);
|
||||
payloadChains.add(think);
|
||||
}
|
||||
payloadChains.addAll(chains);
|
||||
if (!payloadChains.isEmpty()) {
|
||||
payload.put("chains", payloadChains);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
private Map<String, Object> findToolChain(String id, String name) {
|
||||
for (Map<String, Object> chain : chains) {
|
||||
if (String.valueOf(chain.get("id")).equals(id)) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
chain.put("name", name);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
Map<String, Object> chain = new LinkedHashMap<>();
|
||||
chain.put("id", id);
|
||||
chain.put("name", name);
|
||||
chains.add(chain);
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
public enum ChatChannel {
|
||||
|
||||
ADMIN,
|
||||
USER_CENTER,
|
||||
PUBLIC_API
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatRuntimeContext implements Serializable {
|
||||
|
||||
private ChatChannel channel;
|
||||
private BigInteger sessionId;
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger userId;
|
||||
private String userAccount;
|
||||
private String userName;
|
||||
private BigInteger assistantId;
|
||||
private String assistantCode;
|
||||
private String assistantName;
|
||||
private String sessionTitle;
|
||||
private boolean anonymous;
|
||||
private List<String> attachments = new ArrayList<>();
|
||||
private Map<String, Object> ext = new LinkedHashMap<>();
|
||||
|
||||
public ChatChannel getChannel() {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public void setChannel(ChatChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public BigInteger getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(BigInteger sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
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 BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserAccount() {
|
||||
return userAccount;
|
||||
}
|
||||
|
||||
public void setUserAccount(String userAccount) {
|
||||
this.userAccount = userAccount;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
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 getSessionTitle() {
|
||||
return sessionTitle;
|
||||
}
|
||||
|
||||
public void setSessionTitle(String sessionTitle) {
|
||||
this.sessionTitle = sessionTitle;
|
||||
}
|
||||
|
||||
public boolean isAnonymous() {
|
||||
return anonymous;
|
||||
}
|
||||
|
||||
public void setAnonymous(boolean anonymous) {
|
||||
this.anonymous = anonymous;
|
||||
}
|
||||
|
||||
public List<String> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public void setAttachments(List<String> attachments) {
|
||||
this.attachments = attachments == null ? new ArrayList<>() : attachments;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExt() {
|
||||
return ext;
|
||||
}
|
||||
|
||||
public void setExt(Map<String, Object> ext) {
|
||||
this.ext = ext == null ? new LinkedHashMap<>() : ext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatRuntimeListener {
|
||||
|
||||
default void onSessionPrepared(ChatRuntimeContext context) {
|
||||
}
|
||||
|
||||
default void onUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
}
|
||||
|
||||
default void onAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
}
|
||||
|
||||
default void onAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
}
|
||||
|
||||
default void onChatFailed(ChatRuntimeContext context, Throwable throwable) {
|
||||
}
|
||||
|
||||
default void onChatCompleted(ChatRuntimeContext context) {
|
||||
}
|
||||
|
||||
default List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatRuntimeManager {
|
||||
|
||||
void prepareSession(ChatRuntimeContext context);
|
||||
|
||||
void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message);
|
||||
|
||||
void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message);
|
||||
|
||||
void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message);
|
||||
|
||||
void recordFailure(ChatRuntimeContext context, Throwable throwable);
|
||||
|
||||
void recordCompleted(ChatRuntimeContext context);
|
||||
|
||||
List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatRuntimeMessage implements Serializable {
|
||||
|
||||
private BigInteger messageId;
|
||||
private String role;
|
||||
private String contentType = "TEXT";
|
||||
private String contentText;
|
||||
private Map<String, Object> contentPayload = new LinkedHashMap<>();
|
||||
private Date createdAt = new Date();
|
||||
private BigInteger senderId;
|
||||
private String senderName;
|
||||
|
||||
public BigInteger getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(BigInteger messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentText() {
|
||||
return contentText;
|
||||
}
|
||||
|
||||
public void setContentText(String contentText) {
|
||||
this.contentText = contentText;
|
||||
}
|
||||
|
||||
public Map<String, Object> getContentPayload() {
|
||||
return contentPayload;
|
||||
}
|
||||
|
||||
public void setContentPayload(Map<String, Object> contentPayload) {
|
||||
this.contentPayload = contentPayload == null ? new LinkedHashMap<>() : contentPayload;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public BigInteger getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public void setSenderId(BigInteger senderId) {
|
||||
this.senderId = senderId;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
public void setSenderName(String senderName) {
|
||||
this.senderName = senderName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class CompositeChatRuntimeManager implements ChatRuntimeManager {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CompositeChatRuntimeManager.class);
|
||||
|
||||
private final ObjectProvider<ChatRuntimeListener> listenerProvider;
|
||||
|
||||
public CompositeChatRuntimeManager(ObjectProvider<ChatRuntimeListener> listenerProvider) {
|
||||
this.listenerProvider = listenerProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSession(ChatRuntimeContext context) {
|
||||
forEach(listener -> listener.onSessionPrepared(context), "prepareSession", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
forEach(listener -> listener.onUserMessage(context, message), "recordUserMessage", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
forEach(listener -> listener.onAssistantDelta(context, message), "recordAssistantDelta", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
forEach(listener -> listener.onAssistantCompleted(context, message), "recordAssistantCompleted", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordFailure(ChatRuntimeContext context, Throwable throwable) {
|
||||
forEach(listener -> listener.onChatFailed(context, throwable), "recordFailure", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordCompleted(ChatRuntimeContext context) {
|
||||
forEach(listener -> listener.onChatCompleted(context), "recordCompleted", context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||
for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) {
|
||||
try {
|
||||
List<ChatRuntimeMessage> messages = listener.loadMessages(context, limit);
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
return messages;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.warn("chat runtime loadMessages failed, channel={}, sessionId={}, listener={}",
|
||||
context == null || context.getChannel() == null ? null : context.getChannel().name(),
|
||||
context == null ? null : context.getSessionId(),
|
||||
listener.getClass().getName(),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void forEach(ListenerConsumer consumer, String action, ChatRuntimeContext context) {
|
||||
for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) {
|
||||
try {
|
||||
consumer.accept(listener);
|
||||
} catch (BusinessException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
log.warn("chat runtime {} failed, channel={}, sessionId={}, listener={}",
|
||||
action,
|
||||
context == null || context.getChannel() == null ? null : context.getChannel().name(),
|
||||
context == null ? null : context.getSessionId(),
|
||||
listener.getClass().getName(),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ListenerConsumer {
|
||||
void accept(ChatRuntimeListener listener);
|
||||
}
|
||||
}
|
||||
26
easyflow-commons/easyflow-common-mq/pom.xml
Normal file
26
easyflow-commons/easyflow-common-mq/pom.xml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?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-commons</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-common-mq</name>
|
||||
<artifactId>easyflow-common-mq</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,129 @@
|
||||
package tech.easyflow.common.mq.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisPassword;
|
||||
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import tech.easyflow.common.mq.core.MQConsumerContainer;
|
||||
import tech.easyflow.common.mq.core.MQConsumerHandler;
|
||||
import tech.easyflow.common.mq.core.MQDeadLetterHandler;
|
||||
import tech.easyflow.common.mq.core.MQDeadLetterService;
|
||||
import tech.easyflow.common.mq.core.MQMessageConverter;
|
||||
import tech.easyflow.common.mq.core.MQProducer;
|
||||
import tech.easyflow.common.mq.redis.JacksonMQMessageConverter;
|
||||
import tech.easyflow.common.mq.redis.RedisMQConsumerContainer;
|
||||
import tech.easyflow.common.mq.redis.RedisMQDeadLetterService;
|
||||
import tech.easyflow.common.mq.redis.RedisMQProducer;
|
||||
import tech.easyflow.common.mq.redis.RedisStreamKeySupport;
|
||||
import tech.easyflow.common.mq.support.MQHealthSupport;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(MQProperties.class)
|
||||
public class MQConfiguration {
|
||||
|
||||
@Bean(destroyMethod = "close")
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQRedisResources mqRedisResources(RedisProperties redisProperties, MQProperties mqProperties) {
|
||||
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
|
||||
configuration.setHostName(redisProperties.getHost());
|
||||
configuration.setPort(redisProperties.getPort());
|
||||
configuration.setDatabase(mqProperties.getRedis().getDatabase());
|
||||
if (redisProperties.getUsername() != null) {
|
||||
configuration.setUsername(redisProperties.getUsername());
|
||||
}
|
||||
if (redisProperties.getPassword() != null) {
|
||||
configuration.setPassword(RedisPassword.of(redisProperties.getPassword()));
|
||||
}
|
||||
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration);
|
||||
connectionFactory.afterPropertiesSet();
|
||||
return new MQRedisResources(connectionFactory, new StringRedisTemplate(connectionFactory));
|
||||
}
|
||||
|
||||
@Bean(name = "mqRedisConnectionFactory", autowireCandidate = false, defaultCandidate = false)
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public LettuceConnectionFactory mqRedisConnectionFactory(MQRedisResources mqRedisResources) {
|
||||
return mqRedisResources.connectionFactory();
|
||||
}
|
||||
|
||||
@Bean(name = "mqStringRedisTemplate", autowireCandidate = false, defaultCandidate = false)
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public StringRedisTemplate mqStringRedisTemplate(MQRedisResources mqRedisResources) {
|
||||
return mqRedisResources.stringRedisTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public RedisStreamKeySupport redisStreamKeySupport(MQProperties mqProperties) {
|
||||
return new RedisStreamKeySupport(mqProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQMessageConverter mqMessageConverter(ObjectMapper objectMapper) {
|
||||
return new JacksonMQMessageConverter(objectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQDeadLetterService mqDeadLetterService(MQRedisResources mqRedisResources,
|
||||
MQMessageConverter mqMessageConverter,
|
||||
RedisStreamKeySupport redisStreamKeySupport,
|
||||
ObjectProvider<MQDeadLetterHandler> handlersProvider) {
|
||||
List<MQDeadLetterHandler> handlers = handlersProvider.orderedStream().toList();
|
||||
return new RedisMQDeadLetterService(
|
||||
mqRedisResources.stringRedisTemplate(),
|
||||
mqMessageConverter,
|
||||
redisStreamKeySupport,
|
||||
handlers
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQProducer mqProducer(MQRedisResources mqRedisResources,
|
||||
MQProperties mqProperties,
|
||||
MQMessageConverter mqMessageConverter,
|
||||
RedisStreamKeySupport redisStreamKeySupport) {
|
||||
return new RedisMQProducer(
|
||||
mqRedisResources.stringRedisTemplate(),
|
||||
mqProperties,
|
||||
mqMessageConverter,
|
||||
redisStreamKeySupport
|
||||
);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQHealthSupport mqHealthSupport(MQRedisResources mqRedisResources) {
|
||||
return new MQHealthSupport(mqRedisResources.connectionFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||
public MQConsumerContainer mqConsumerContainer(MQRedisResources mqRedisResources,
|
||||
MQProperties mqProperties,
|
||||
MQMessageConverter mqMessageConverter,
|
||||
MQDeadLetterService mqDeadLetterService,
|
||||
RedisStreamKeySupport redisStreamKeySupport,
|
||||
ObjectProvider<MQConsumerHandler> handlersProvider) {
|
||||
List<MQConsumerHandler> handlers = handlersProvider.orderedStream().toList();
|
||||
return new RedisMQConsumerContainer(
|
||||
mqRedisResources.connectionFactory(),
|
||||
mqRedisResources.stringRedisTemplate(),
|
||||
mqProperties,
|
||||
mqMessageConverter,
|
||||
mqDeadLetterService,
|
||||
redisStreamKeySupport,
|
||||
handlers
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package tech.easyflow.common.mq.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "easyflow.mq")
|
||||
public class MQProperties {
|
||||
|
||||
private boolean enabled = true;
|
||||
private String type = "redis";
|
||||
private final Redis redis = new Redis();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Redis getRedis() {
|
||||
return redis;
|
||||
}
|
||||
|
||||
public static class Redis {
|
||||
|
||||
private int database = 1;
|
||||
private String streamPrefix = "easyflow:mq";
|
||||
private int chatPersistShardCount = 4;
|
||||
private int consumerBatchSize = 200;
|
||||
private Duration consumerBlockTimeout = Duration.ofMillis(2000);
|
||||
private Duration pendingClaimIdle = Duration.ofMillis(60000);
|
||||
private int maxRetry = 16;
|
||||
|
||||
public int getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public void setDatabase(int database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public String getStreamPrefix() {
|
||||
return streamPrefix;
|
||||
}
|
||||
|
||||
public void setStreamPrefix(String streamPrefix) {
|
||||
this.streamPrefix = streamPrefix;
|
||||
}
|
||||
|
||||
public int getChatPersistShardCount() {
|
||||
return chatPersistShardCount;
|
||||
}
|
||||
|
||||
public void setChatPersistShardCount(int chatPersistShardCount) {
|
||||
this.chatPersistShardCount = chatPersistShardCount;
|
||||
}
|
||||
|
||||
public int getConsumerBatchSize() {
|
||||
return consumerBatchSize;
|
||||
}
|
||||
|
||||
public void setConsumerBatchSize(int consumerBatchSize) {
|
||||
this.consumerBatchSize = consumerBatchSize;
|
||||
}
|
||||
|
||||
public Duration getConsumerBlockTimeout() {
|
||||
return consumerBlockTimeout;
|
||||
}
|
||||
|
||||
public void setConsumerBlockTimeout(Duration consumerBlockTimeout) {
|
||||
this.consumerBlockTimeout = consumerBlockTimeout;
|
||||
}
|
||||
|
||||
public Duration getPendingClaimIdle() {
|
||||
return pendingClaimIdle;
|
||||
}
|
||||
|
||||
public void setPendingClaimIdle(Duration pendingClaimIdle) {
|
||||
this.pendingClaimIdle = pendingClaimIdle;
|
||||
}
|
||||
|
||||
public int getMaxRetry() {
|
||||
return maxRetry;
|
||||
}
|
||||
|
||||
public void setMaxRetry(int maxRetry) {
|
||||
this.maxRetry = maxRetry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package tech.easyflow.common.mq.config;
|
||||
|
||||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
public record MQRedisResources(LettuceConnectionFactory connectionFactory,
|
||||
StringRedisTemplate stringRedisTemplate) implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
connectionFactory.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MQAcknowledger {
|
||||
|
||||
void acknowledge(List<MQMessage> messages);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public interface MQConsumerContainer {
|
||||
|
||||
boolean isRunning();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MQConsumerHandler {
|
||||
|
||||
MQSubscription subscription();
|
||||
|
||||
void handle(List<MQMessage> messages) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public interface MQDeadLetterHandler {
|
||||
|
||||
boolean supports(String topic);
|
||||
|
||||
void handle(MQMessage message, String reason);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public interface MQDeadLetterService {
|
||||
|
||||
void deadLetter(MQMessage message, String reason);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MQMessage implements Serializable {
|
||||
|
||||
private String messageId;
|
||||
private String topic;
|
||||
private String key;
|
||||
private String body;
|
||||
private Date createdAt;
|
||||
private int retryCount;
|
||||
private String streamKey;
|
||||
private String streamMessageId;
|
||||
private Map<String, String> headers = new LinkedHashMap<>();
|
||||
|
||||
public String getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(String messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(Date createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public int getRetryCount() {
|
||||
return retryCount;
|
||||
}
|
||||
|
||||
public void setRetryCount(int retryCount) {
|
||||
this.retryCount = retryCount;
|
||||
}
|
||||
|
||||
public String getStreamKey() {
|
||||
return streamKey;
|
||||
}
|
||||
|
||||
public void setStreamKey(String streamKey) {
|
||||
this.streamKey = streamKey;
|
||||
}
|
||||
|
||||
public String getStreamMessageId() {
|
||||
return streamMessageId;
|
||||
}
|
||||
|
||||
public void setStreamMessageId(String streamMessageId) {
|
||||
this.streamMessageId = streamMessageId;
|
||||
}
|
||||
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public interface MQMessageConverter {
|
||||
|
||||
String serialize(MQMessage message);
|
||||
|
||||
MQMessage deserialize(String payload);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public interface MQProducer {
|
||||
|
||||
String send(MQMessage message);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.common.mq.core;
|
||||
|
||||
public class MQSubscription {
|
||||
|
||||
private String topic;
|
||||
private String consumerGroup;
|
||||
private int shardCount;
|
||||
|
||||
public String getTopic() {
|
||||
return topic;
|
||||
}
|
||||
|
||||
public void setTopic(String topic) {
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public String getConsumerGroup() {
|
||||
return consumerGroup;
|
||||
}
|
||||
|
||||
public void setConsumerGroup(String consumerGroup) {
|
||||
this.consumerGroup = consumerGroup;
|
||||
}
|
||||
|
||||
public int getShardCount() {
|
||||
return shardCount;
|
||||
}
|
||||
|
||||
public void setShardCount(int shardCount) {
|
||||
this.shardCount = shardCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package tech.easyflow.common.mq.exception;
|
||||
|
||||
public class MQException extends RuntimeException {
|
||||
|
||||
public MQException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public MQException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQMessageConverter;
|
||||
import tech.easyflow.common.mq.exception.MQException;
|
||||
|
||||
public class JacksonMQMessageConverter implements MQMessageConverter {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JacksonMQMessageConverter(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize(MQMessage message) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(message);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new MQException("MQ 消息序列化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MQMessage deserialize(String payload) {
|
||||
try {
|
||||
return objectMapper.readValue(payload, MQMessage.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new MQException("MQ 消息反序列化失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.data.domain.Range;
|
||||
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.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamReadOptions;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
import tech.easyflow.common.mq.core.MQAcknowledger;
|
||||
import tech.easyflow.common.mq.core.MQConsumerContainer;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle {
|
||||
|
||||
private final RedisConnectionFactory redisConnectionFactory;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MQProperties properties;
|
||||
private final MQMessageConverter messageConverter;
|
||||
private final MQDeadLetterService deadLetterService;
|
||||
private final RedisStreamKeySupport keySupport;
|
||||
private final List<MQConsumerHandler> handlers;
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
|
||||
private volatile boolean running;
|
||||
|
||||
public RedisMQConsumerContainer(RedisConnectionFactory redisConnectionFactory,
|
||||
StringRedisTemplate stringRedisTemplate,
|
||||
MQProperties properties,
|
||||
MQMessageConverter messageConverter,
|
||||
MQDeadLetterService deadLetterService,
|
||||
RedisStreamKeySupport keySupport,
|
||||
List<MQConsumerHandler> handlers) {
|
||||
this.redisConnectionFactory = redisConnectionFactory;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.properties = properties;
|
||||
this.messageConverter = messageConverter;
|
||||
this.deadLetterService = deadLetterService;
|
||||
this.keySupport = keySupport;
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (running) {
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
for (MQConsumerHandler handler : handlers) {
|
||||
MQSubscription subscription = handler.subscription();
|
||||
for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) {
|
||||
int currentShard = shard;
|
||||
executorService.submit(() -> consumeLoop(handler, subscription, currentShard));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
running = false;
|
||||
executorService.shutdownNow();
|
||||
try {
|
||||
executorService.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
stop();
|
||||
}
|
||||
|
||||
private void consumeLoop(MQConsumerHandler handler, MQSubscription subscription, int shard) {
|
||||
String streamKey = keySupport.streamKey(subscription.getTopic(), shard);
|
||||
String consumerName = subscription.getConsumerGroup() + "-" + shard;
|
||||
ensureConsumerGroup(streamKey, subscription.getConsumerGroup());
|
||||
while (running) {
|
||||
try {
|
||||
reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName);
|
||||
List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
|
||||
Consumer.from(subscription.getConsumerGroup(), consumerName),
|
||||
StreamReadOptions.empty()
|
||||
.count(properties.getRedis().getConsumerBatchSize())
|
||||
.block(properties.getRedis().getConsumerBlockTimeout()),
|
||||
StreamOffset.create(streamKey, org.springframework.data.redis.connection.stream.ReadOffset.lastConsumed())
|
||||
);
|
||||
if (records == null || records.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
List<MQMessage> messages = toMessages(streamKey, records);
|
||||
if (messages.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
handleMessages(handler, streamKey, subscription.getConsumerGroup(), messages);
|
||||
} catch (Exception ignored) {
|
||||
sleepSilently(1000L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reclaimPending(String streamKey, String group, String consumerName) {
|
||||
Duration idle = properties.getRedis().getPendingClaimIdle();
|
||||
try (RedisConnection connection = redisConnectionFactory.getConnection()) {
|
||||
RedisStreamCommands.XPendingOptions options = RedisStreamCommands.XPendingOptions
|
||||
.range(Range.unbounded(), (long) properties.getRedis().getConsumerBatchSize());
|
||||
var pendingMessages = connection.streamCommands()
|
||||
.xPending(streamKey.getBytes(StandardCharsets.UTF_8), group, options);
|
||||
if (pendingMessages == null || pendingMessages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<RecordId> ids = new ArrayList<>();
|
||||
for (PendingMessage pendingMessage : pendingMessages) {
|
||||
if (pendingMessage.getElapsedTimeSinceLastDelivery().compareTo(idle) >= 0) {
|
||||
ids.add(pendingMessage.getId());
|
||||
}
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
stringRedisTemplate.opsForStream().claim(
|
||||
streamKey,
|
||||
group,
|
||||
consumerName,
|
||||
idle,
|
||||
ids.toArray(new RecordId[0])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureConsumerGroup(String streamKey, String group) {
|
||||
try (RedisConnection connection = redisConnectionFactory.getConnection()) {
|
||||
connection.streamCommands().xGroupCreate(
|
||||
streamKey.getBytes(StandardCharsets.UTF_8),
|
||||
group,
|
||||
org.springframework.data.redis.connection.stream.ReadOffset.latest(),
|
||||
true
|
||||
);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private List<MQMessage> toMessages(String streamKey, List<MapRecord<String, Object, Object>> records) {
|
||||
List<MQMessage> messages = new ArrayList<>(records.size());
|
||||
for (MapRecord<String, Object, Object> record : records) {
|
||||
Object payload = record.getValue().get("payload");
|
||||
if (payload == null) {
|
||||
continue;
|
||||
}
|
||||
MQMessage message = messageConverter.deserialize(String.valueOf(payload));
|
||||
message.setStreamKey(streamKey);
|
||||
message.setStreamMessageId(record.getId().getValue());
|
||||
messages.add(message);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private void retryOrDeadLetter(List<MQMessage> messages, String reason) {
|
||||
for (MQMessage message : messages) {
|
||||
int retryCount = message.getRetryCount() + 1;
|
||||
message.setRetryCount(retryCount);
|
||||
message.getHeaders().put("lastError", reason == null ? "" : reason);
|
||||
if (retryCount > properties.getRedis().getMaxRetry()) {
|
||||
deadLetterService.deadLetter(message, reason);
|
||||
} else {
|
||||
stringRedisTemplate.opsForStream().add(
|
||||
org.springframework.data.redis.connection.stream.StreamRecords.string(
|
||||
Map.of("payload", messageConverter.serialize(message))
|
||||
).withStreamKey(message.getStreamKey())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMessages(MQConsumerHandler handler, String streamKey, String group, List<MQMessage> messages) throws Exception {
|
||||
try {
|
||||
handler.handle(messages);
|
||||
acknowledge(streamKey, group, messages);
|
||||
return;
|
||||
} catch (Exception batchEx) {
|
||||
if (messages.size() == 1) {
|
||||
retryOrDeadLetter(messages, resolveReason(batchEx));
|
||||
acknowledge(streamKey, group, messages);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (MQMessage message : messages) {
|
||||
try {
|
||||
handler.handle(List.of(message));
|
||||
} catch (Exception singleEx) {
|
||||
retryOrDeadLetter(List.of(message), resolveReason(singleEx));
|
||||
} finally {
|
||||
acknowledge(streamKey, group, List.of(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void acknowledge(String streamKey, String group, List<MQMessage> messages) {
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String[] ids = messages.stream()
|
||||
.map(MQMessage::getStreamMessageId)
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(String[]::new);
|
||||
if (ids.length == 0) {
|
||||
return;
|
||||
}
|
||||
MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids);
|
||||
acknowledger.acknowledge(messages);
|
||||
}
|
||||
|
||||
private String resolveReason(Exception exception) {
|
||||
if (exception == null || exception.getMessage() == null || exception.getMessage().isBlank()) {
|
||||
return "消费失败";
|
||||
}
|
||||
return exception.getMessage();
|
||||
}
|
||||
|
||||
private void sleepSilently(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import tech.easyflow.common.mq.core.MQDeadLetterHandler;
|
||||
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.exception.MQException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RedisMQDeadLetterService implements MQDeadLetterService {
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MQMessageConverter messageConverter;
|
||||
private final RedisStreamKeySupport keySupport;
|
||||
private final List<MQDeadLetterHandler> handlers;
|
||||
|
||||
public RedisMQDeadLetterService(StringRedisTemplate stringRedisTemplate,
|
||||
MQMessageConverter messageConverter,
|
||||
RedisStreamKeySupport keySupport,
|
||||
List<MQDeadLetterHandler> handlers) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.messageConverter = messageConverter;
|
||||
this.keySupport = keySupport;
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deadLetter(MQMessage message, String reason) {
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
message.getHeaders().put("deadLetterReason", reason == null ? "" : reason);
|
||||
String deadLetterKey = keySupport.deadLetterKey(message.getTopic());
|
||||
if (stringRedisTemplate.opsForStream().add(
|
||||
StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(deadLetterKey)
|
||||
) == null) {
|
||||
throw new MQException("写入死信流失败");
|
||||
}
|
||||
for (MQDeadLetterHandler handler : handlers) {
|
||||
if (handler.supports(message.getTopic())) {
|
||||
handler.handle(message, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import org.springframework.data.redis.connection.stream.RecordId;
|
||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
import tech.easyflow.common.mq.core.MQMessage;
|
||||
import tech.easyflow.common.mq.core.MQProducer;
|
||||
import tech.easyflow.common.mq.core.MQMessageConverter;
|
||||
import tech.easyflow.common.mq.exception.MQException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RedisMQProducer implements MQProducer {
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final MQProperties properties;
|
||||
private final MQMessageConverter messageConverter;
|
||||
private final RedisStreamKeySupport keySupport;
|
||||
|
||||
public RedisMQProducer(StringRedisTemplate stringRedisTemplate,
|
||||
MQProperties properties,
|
||||
MQMessageConverter messageConverter,
|
||||
RedisStreamKeySupport keySupport) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.properties = properties;
|
||||
this.messageConverter = messageConverter;
|
||||
this.keySupport = keySupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String send(MQMessage message) {
|
||||
if (message == null) {
|
||||
throw new MQException("MQ 消息不能为空");
|
||||
}
|
||||
if (message.getTopic() == null || message.getTopic().isBlank()) {
|
||||
throw new MQException("MQ topic 不能为空");
|
||||
}
|
||||
if (message.getMessageId() == null || message.getMessageId().isBlank()) {
|
||||
message.setMessageId(UUID.randomUUID().toString());
|
||||
}
|
||||
if (message.getCreatedAt() == null) {
|
||||
message.setCreatedAt(new Date());
|
||||
}
|
||||
int shardCount = Math.max(properties.getRedis().getChatPersistShardCount(), 1);
|
||||
int shard = keySupport.resolveShard(message.getKey(), shardCount);
|
||||
String streamKey = keySupport.streamKey(message.getTopic(), shard);
|
||||
RecordId recordId = stringRedisTemplate.opsForStream().add(
|
||||
StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(streamKey)
|
||||
);
|
||||
if (recordId == null) {
|
||||
throw new MQException("MQ 消息投递失败");
|
||||
}
|
||||
return recordId.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tech.easyflow.common.mq.redis;
|
||||
|
||||
import tech.easyflow.common.mq.config.MQProperties;
|
||||
|
||||
public class RedisStreamKeySupport {
|
||||
|
||||
private final MQProperties properties;
|
||||
|
||||
public RedisStreamKeySupport(MQProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public String streamKey(String topic, int shard) {
|
||||
return properties.getRedis().getStreamPrefix() + ":" + topic + ":" + String.format("%02d", shard);
|
||||
}
|
||||
|
||||
public String deadLetterKey(String topic) {
|
||||
return properties.getRedis().getStreamPrefix() + ":dead-letter:" + topic;
|
||||
}
|
||||
|
||||
public int resolveShard(String key, int shardCount) {
|
||||
if (shardCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floorMod(key == null ? 0 : key.hashCode(), shardCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package tech.easyflow.common.mq.support;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import tech.easyflow.common.mq.exception.MQException;
|
||||
|
||||
public class MQHealthSupport {
|
||||
|
||||
private final RedisConnectionFactory redisConnectionFactory;
|
||||
|
||||
public MQHealthSupport(RedisConnectionFactory redisConnectionFactory) {
|
||||
this.redisConnectionFactory = redisConnectionFactory;
|
||||
}
|
||||
|
||||
public boolean available() {
|
||||
try (var connection = redisConnectionFactory.getConnection()) {
|
||||
String pong = connection.ping();
|
||||
return pong != null;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void assertAvailable() {
|
||||
if (!available()) {
|
||||
throw new MQException("MQ Redis 不可用");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
<modules>
|
||||
<module>easyflow-common-all</module>
|
||||
<module>easyflow-common-analytical-db</module>
|
||||
<module>easyflow-common-mq</module>
|
||||
<module>easyflow-common-web</module>
|
||||
<module>easyflow-common-captcha</module>
|
||||
<module>easyflow-common-base</module>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-support</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-rag-retrieval</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-spring-boot-starter</artifactId>
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
package tech.easyflow.ai.config;
|
||||
|
||||
import com.easyagents.engine.es.ESConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "rag.searcher.elastic")
|
||||
public class AiEsConfig extends ESConfig {
|
||||
|
||||
@Value("${rag.searcher.elastic.host}")
|
||||
@Override
|
||||
public void setHost(String host) {
|
||||
super.setHost(host);
|
||||
}
|
||||
|
||||
@Value("${rag.searcher.elastic.userName}")
|
||||
@Override
|
||||
public void setUserName(String userName) {
|
||||
super.setUserName(userName);
|
||||
}
|
||||
|
||||
@Value("${rag.searcher.elastic.password}")
|
||||
@Override
|
||||
public void setPassword(String password) {
|
||||
super.setPassword(password);
|
||||
}
|
||||
|
||||
@Value("${rag.searcher.elastic.indexName}")
|
||||
@Override
|
||||
public void setIndexName(String indexName) {
|
||||
super.setIndexName(indexName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
package tech.easyflow.ai.config;
|
||||
|
||||
import com.easyagents.search.engine.lucene.LuceneConfig;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "rag.searcher.lucene")
|
||||
public class AiLuceneConfig extends LuceneConfig {
|
||||
|
||||
@Value("${rag.searcher.lucene.indexDirPath}")
|
||||
@Override
|
||||
public void setIndexDirPath(String indexDirPath) {
|
||||
super.setIndexDirPath(indexDirPath);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.ai.config;
|
||||
|
||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "rag.milvus")
|
||||
public class AiMilvusConfig extends MilvusVectorStoreConfig {
|
||||
|
||||
public MilvusVectorStoreConfig copyForCollection(String collectionName) {
|
||||
MilvusVectorStoreConfig config = new MilvusVectorStoreConfig();
|
||||
config.setUri(getUri());
|
||||
config.setToken(getToken());
|
||||
config.setDatabaseName(getDatabaseName());
|
||||
config.setUsername(getUsername());
|
||||
config.setPassword(getPassword());
|
||||
config.setAutoCreateCollection(isAutoCreateCollection());
|
||||
config.setDefaultCollectionName(collectionName);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package tech.easyflow.ai.config;
|
||||
|
||||
import com.easyagents.engine.es.ElasticSearcher;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.easyagents.store.milvus.MilvusVectorStore;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.rag.KeywordEngineType;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
|
||||
@Component
|
||||
public class RagInfrastructureValidator implements SmartInitializingSingleton {
|
||||
|
||||
private static final int STARTUP_CHECK_RETRY_TIMES = 10;
|
||||
private static final long STARTUP_CHECK_RETRY_INTERVAL_MS = 1000L;
|
||||
|
||||
@Resource
|
||||
private AiMilvusConfig aiMilvusConfig;
|
||||
|
||||
@Resource
|
||||
private AiLuceneConfig aiLuceneConfig;
|
||||
|
||||
@Resource
|
||||
private SearcherFactory searcherFactory;
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
validateMilvus();
|
||||
validateKeywordSearcher();
|
||||
}
|
||||
|
||||
private void validateMilvus() {
|
||||
Exception lastException = null;
|
||||
for (int i = 0; i < STARTUP_CHECK_RETRY_TIMES; i++) {
|
||||
try {
|
||||
MilvusVectorStore vectorStore = new MilvusVectorStore(aiMilvusConfig.copyForCollection("__rag_boot_probe__"));
|
||||
if (vectorStore.checkAvailable()) {
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
}
|
||||
sleepBeforeRetry();
|
||||
}
|
||||
if (lastException != null) {
|
||||
throw new BusinessException("Milvus 服务不可用,项目启动失败,请检查 rag.milvus 配置与服务状态: " + lastException.getMessage());
|
||||
}
|
||||
throw new BusinessException("Milvus 服务不可用,项目启动失败,请检查 rag.milvus 配置与服务状态");
|
||||
}
|
||||
|
||||
private void validateKeywordSearcher() {
|
||||
KeywordEngineType engineType = KeywordEngineType.from(
|
||||
SpringContextUtil.getProperty("rag.engine", "ES")
|
||||
);
|
||||
if (engineType == KeywordEngineType.LUCENE) {
|
||||
validateLuceneDirectory();
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (!(searcher instanceof ElasticSearcher) || !checkElasticAvailable((ElasticSearcher) searcher)) {
|
||||
throw new BusinessException("ES 服务不可用,项目启动失败,请检查 rag.engine 与 rag.searcher.elastic 配置");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkElasticAvailable(ElasticSearcher elasticSearcher) {
|
||||
for (int i = 0; i < STARTUP_CHECK_RETRY_TIMES; i++) {
|
||||
if (elasticSearcher.checkAvailable()) {
|
||||
return true;
|
||||
}
|
||||
sleepBeforeRetry();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void validateLuceneDirectory() {
|
||||
String indexDirPath = aiLuceneConfig.getIndexDirPath();
|
||||
if (StringUtil.noText(indexDirPath)) {
|
||||
throw new BusinessException("Lucene 索引目录未配置,请检查 rag.searcher.lucene.indexDirPath");
|
||||
}
|
||||
File indexDir = new File(indexDirPath);
|
||||
if (!indexDir.exists() && !indexDir.mkdirs()) {
|
||||
throw new BusinessException("Lucene 索引目录创建失败: " + indexDirPath);
|
||||
}
|
||||
if (!indexDir.isDirectory() || !indexDir.canRead() || !indexDir.canWrite()) {
|
||||
throw new BusinessException("Lucene 索引目录不可读写: " + indexDirPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void sleepBeforeRetry() {
|
||||
try {
|
||||
Thread.sleep(STARTUP_CHECK_RETRY_INTERVAL_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new BusinessException("中间件启动校验被中断");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,40 +3,37 @@ package tech.easyflow.ai.config;
|
||||
import com.easyagents.engine.es.ElasticSearcher;
|
||||
import com.easyagents.search.engine.lucene.LuceneSearcher;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SearcherFactory {
|
||||
|
||||
@Autowired
|
||||
private AiLuceneConfig luceneConfig;
|
||||
private final ObjectProvider<DocumentSearcher> documentSearcherProvider;
|
||||
|
||||
@Autowired
|
||||
private AiEsConfig aiEsConfig;
|
||||
public SearcherFactory(ObjectProvider<DocumentSearcher> documentSearcherProvider) {
|
||||
this.documentSearcherProvider = documentSearcherProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LuceneSearcher luceneSearcher() {
|
||||
@ConditionalOnProperty(prefix = "rag", name = "engine", havingValue = "LUCENE")
|
||||
public LuceneSearcher luceneSearcher(AiLuceneConfig luceneConfig) {
|
||||
return new LuceneSearcher(luceneConfig);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ElasticSearcher elasticSearcher() {
|
||||
@ConditionalOnProperty(prefix = "rag", name = "engine", havingValue = "ES", matchIfMissing = true)
|
||||
public ElasticSearcher elasticSearcher(AiEsConfig aiEsConfig) {
|
||||
return new ElasticSearcher(aiEsConfig);
|
||||
}
|
||||
|
||||
public DocumentSearcher getSearcher() {
|
||||
return documentSearcherProvider.getIfAvailable();
|
||||
}
|
||||
|
||||
public DocumentSearcher getSearcher(String defaultSearcherType) {
|
||||
if (defaultSearcherType == null) {
|
||||
defaultSearcherType = "lucene";
|
||||
}
|
||||
switch (defaultSearcherType) {
|
||||
case "elasticSearch":
|
||||
return new ElasticSearcher(aiEsConfig);
|
||||
case "lucene":
|
||||
default:
|
||||
return new LuceneSearcher(luceneConfig);
|
||||
}
|
||||
public DocumentSearcher getSearcher(String ignored) {
|
||||
return getSearcher();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class BotKnowledgeBindingRequest {
|
||||
|
||||
private BigInteger knowledgeId;
|
||||
private String retrievalMode;
|
||||
|
||||
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 RetrievalMode resolveRetrievalMode() {
|
||||
return KnowledgeRetrievalModes.parse(retrievalMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tech.easyflow.ai.dto;
|
||||
|
||||
public class KnowledgeSearchResultItem {
|
||||
|
||||
private Integer sorting;
|
||||
private String content;
|
||||
private Double score;
|
||||
private String hitSource;
|
||||
private Double vectorScore;
|
||||
private Double keywordScore;
|
||||
|
||||
public Integer getSorting() {
|
||||
return sorting;
|
||||
}
|
||||
|
||||
public void setSorting(Integer sorting) {
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Double getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public void setScore(Double score) {
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public String getHitSource() {
|
||||
return hitSource;
|
||||
}
|
||||
|
||||
public void setHitSource(String hitSource) {
|
||||
this.hitSource = hitSource;
|
||||
}
|
||||
|
||||
public Double getVectorScore() {
|
||||
return vectorScore;
|
||||
}
|
||||
|
||||
public void setVectorScore(Double vectorScore) {
|
||||
this.vectorScore = vectorScore;
|
||||
}
|
||||
|
||||
public Double getKeywordScore() {
|
||||
return keywordScore;
|
||||
}
|
||||
|
||||
public void setKeywordScore(Double keywordScore) {
|
||||
this.keywordScore = keywordScore;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,12 @@ import tech.easyflow.core.chat.protocol.ChatType;
|
||||
import tech.easyflow.core.chat.protocol.MessageRole;
|
||||
import tech.easyflow.core.chat.protocol.payload.ErrorPayload;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeManager;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -33,6 +38,9 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
private final MemoryPrompt memoryPrompt;
|
||||
private final ChatSseEmitter sseEmitter;
|
||||
private final ChatOptions chatOptions;
|
||||
private final ChatRuntimeManager chatRuntimeManager;
|
||||
private final ChatRuntimeContext runtimeContext;
|
||||
private final ChatAssistantAccumulator assistantAccumulator;
|
||||
// 核心标记:是否允许执行onStop业务逻辑(仅最后一次无后续工具调用时为true)
|
||||
private boolean canStop = true;
|
||||
// 辅助标记:是否进入过工具调用(避免重复递归判断)
|
||||
@@ -40,12 +48,17 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
// 流式响应只能结束一次,避免重复发送导致 IllegalStateException
|
||||
private final AtomicBoolean completed = new AtomicBoolean(false);
|
||||
|
||||
public ChatStreamListener(String conversationId, ChatModel chatModel, MemoryPrompt memoryPrompt, ChatSseEmitter sseEmitter, ChatOptions chatOptions) {
|
||||
public ChatStreamListener(String conversationId, ChatModel chatModel, MemoryPrompt memoryPrompt, ChatSseEmitter sseEmitter,
|
||||
ChatOptions chatOptions, ChatRuntimeManager chatRuntimeManager,
|
||||
ChatRuntimeContext runtimeContext, ChatAssistantAccumulator assistantAccumulator) {
|
||||
this.conversationId = conversationId;
|
||||
this.chatModel = chatModel;
|
||||
this.memoryPrompt = memoryPrompt;
|
||||
this.sseEmitter = sseEmitter;
|
||||
this.chatOptions = chatOptions;
|
||||
this.chatRuntimeManager = chatRuntimeManager;
|
||||
this.runtimeContext = runtimeContext;
|
||||
this.assistantAccumulator = assistantAccumulator;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,6 +83,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
List<ToolCall> toolCalls = aiMessage.getToolCalls();
|
||||
if (toolCalls != null) {
|
||||
for (ToolCall toolCall : toolCalls) {
|
||||
assistantAccumulator.appendToolCall(toolCall.getId(), toolCall.getName(), toolCall.getArguments());
|
||||
sendToolCallEnvelope(toolCall);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +92,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages();
|
||||
for (ToolMessage toolMessage : toolMessages) {
|
||||
memoryPrompt.addMessage(toolMessage);
|
||||
assistantAccumulator.appendToolResult(toolMessage.getToolCallId(), null, toolMessage.getContent());
|
||||
sendToolResultEnvelope(toolMessage);
|
||||
}
|
||||
chatModel.chatStream(memoryPrompt, this, chatOptions);
|
||||
@@ -87,10 +102,14 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
}
|
||||
String reasoningContent = aiMessage.getReasoningContent();
|
||||
if (reasoningContent != null && !reasoningContent.isEmpty()) {
|
||||
assistantAccumulator.appendReasoning(reasoningContent);
|
||||
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(reasoningContent, ChatType.THINKING));
|
||||
sendChatEnvelope(sseEmitter, reasoningContent, ChatType.THINKING);
|
||||
} else {
|
||||
String delta = aiMessage.getContent();
|
||||
if (delta != null && !delta.isEmpty()) {
|
||||
assistantAccumulator.appendContent(delta);
|
||||
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(delta, ChatType.MESSAGE));
|
||||
sendChatEnvelope(sseEmitter, delta, ChatType.MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -111,10 +130,13 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
// 仅当canStop为true(最后一次无后续工具调用的响应)时,执行业务逻辑
|
||||
if (this.canStop && completed.compareAndSet(false, true)) {
|
||||
if (context.getThrowable() != null) {
|
||||
chatRuntimeManager.recordFailure(runtimeContext, context.getThrowable());
|
||||
sendSystemError(sseEmitter, context.getThrowable().getMessage(), context.getThrowable());
|
||||
return;
|
||||
}
|
||||
memoryPrompt.addMessage(context.getFullMessage());
|
||||
chatRuntimeManager.recordAssistantCompleted(runtimeContext, buildAssistantCompletedMessage(context));
|
||||
chatRuntimeManager.recordCompleted(runtimeContext);
|
||||
ChatEnvelope<Map<String, String>> chatEnvelope = new ChatEnvelope<>();
|
||||
chatEnvelope.setDomain(ChatDomain.SYSTEM);
|
||||
boolean doneSent = sseEmitter.sendDone(chatEnvelope);
|
||||
@@ -133,6 +155,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
conversationId, throwable.getMessage(), throwable.toString(), throwable);
|
||||
}
|
||||
if (throwable != null && completed.compareAndSet(false, true)) {
|
||||
chatRuntimeManager.recordFailure(runtimeContext, throwable);
|
||||
sendSystemError(sseEmitter, throwable.getMessage(), throwable);
|
||||
}
|
||||
stopStreamClient(context, "on_failure", throwable);
|
||||
@@ -235,4 +258,28 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
}
|
||||
}
|
||||
|
||||
private ChatRuntimeMessage buildAssistantDeltaMessage(String delta, ChatType chatType) {
|
||||
ChatRuntimeMessage message = new ChatRuntimeMessage();
|
||||
message.setRole("assistant");
|
||||
message.setContentType(chatType == ChatType.THINKING ? "THINKING" : "TEXT");
|
||||
message.setContentText(delta);
|
||||
message.setCreatedAt(new Date());
|
||||
message.setSenderId(runtimeContext.getAssistantId());
|
||||
message.setSenderName(runtimeContext.getAssistantName());
|
||||
return message;
|
||||
}
|
||||
|
||||
private ChatRuntimeMessage buildAssistantCompletedMessage(StreamContext context) {
|
||||
ChatRuntimeMessage message = new ChatRuntimeMessage();
|
||||
message.setRole("assistant");
|
||||
message.setContentType("TEXT");
|
||||
String fullContent = context != null && context.getFullMessage() != null ? context.getFullMessage().getContent() : null;
|
||||
message.setContentText(StringUtil.hasText(fullContent) ? fullContent : assistantAccumulator.getContent());
|
||||
message.setContentPayload(assistantAccumulator.buildPayload());
|
||||
message.setCreatedAt(new Date());
|
||||
message.setSenderId(runtimeContext.getAssistantId());
|
||||
message.setSenderName(runtimeContext.getAssistantName());
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package tech.easyflow.ai.easyagents.memory;
|
||||
|
||||
import com.easyagents.core.memory.ChatMemory;
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.SystemMessage;
|
||||
import com.easyagents.core.message.UserMessage;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class RuntimeChatMemory implements ChatMemory {
|
||||
|
||||
private final Object id;
|
||||
private final List<Message> messages = new ArrayList<>();
|
||||
|
||||
public RuntimeChatMemory(Object id, List<ChatRuntimeMessage> runtimeMessages) {
|
||||
this.id = id;
|
||||
if (runtimeMessages != null) {
|
||||
for (ChatRuntimeMessage runtimeMessage : runtimeMessages) {
|
||||
Message message = toMessage(runtimeMessage);
|
||||
if (message != null) {
|
||||
this.messages.add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> getMessages(int count) {
|
||||
if (messages.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (count <= 0 || messages.size() <= count) {
|
||||
return new ArrayList<>(messages);
|
||||
}
|
||||
return new ArrayList<>(messages.subList(Math.max(messages.size() - count, 0), messages.size()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMessage(Message message) {
|
||||
if (message != null) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
messages.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
private Message toMessage(ChatRuntimeMessage runtimeMessage) {
|
||||
if (runtimeMessage == null || runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String role = runtimeMessage.getRole();
|
||||
if ("assistant".equalsIgnoreCase(role)) {
|
||||
return new AiMessage(runtimeMessage.getContentText());
|
||||
}
|
||||
if ("system".equalsIgnoreCase(role)) {
|
||||
return new SystemMessage(runtimeMessage.getContentText());
|
||||
}
|
||||
if ("tool".equalsIgnoreCase(role)) {
|
||||
return new SystemMessage(runtimeMessage.getContentText());
|
||||
}
|
||||
return new UserMessage(runtimeMessage.getContentText());
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package tech.easyflow.ai.easyagents.tool;
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.easyagents.core.model.chat.tool.BaseTool;
|
||||
import com.easyagents.core.model.chat.tool.Parameter;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
|
||||
@@ -14,12 +16,18 @@ import java.util.Map;
|
||||
public class DocumentCollectionTool extends BaseTool {
|
||||
|
||||
private BigInteger knowledgeId;
|
||||
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
||||
|
||||
public DocumentCollectionTool() {
|
||||
}
|
||||
|
||||
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName) {
|
||||
this(documentCollection, needEnglishName, RetrievalMode.HYBRID);
|
||||
}
|
||||
|
||||
public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName, RetrievalMode retrievalMode) {
|
||||
this.knowledgeId = documentCollection.getId();
|
||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||
if (needEnglishName) {
|
||||
this.name = documentCollection.getEnglishName();
|
||||
} else {
|
||||
@@ -47,11 +55,25 @@ public class DocumentCollectionTool extends BaseTool {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public RetrievalMode getRetrievalMode() {
|
||||
return retrievalMode;
|
||||
}
|
||||
|
||||
public void setRetrievalMode(RetrievalMode retrievalMode) {
|
||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Map<String, Object> argsMap) {
|
||||
|
||||
DocumentCollectionService knowledgeService = SpringContextUtil.getBean(DocumentCollectionService.class);
|
||||
List<Document> documents = knowledgeService.search(this.knowledgeId, (String) argsMap.get("input"));
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(this.knowledgeId);
|
||||
request.setQuery((String) argsMap.get("input"));
|
||||
request.setRetrievalMode(this.retrievalMode);
|
||||
request.setCallerType("BOT_TOOL");
|
||||
request.setCallerId(this.knowledgeId == null ? null : this.knowledgeId.toString());
|
||||
List<Document> documents = knowledgeService.search(request);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (documents != null) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.easyagents.flow.core.knowledge.Knowledge;
|
||||
import com.easyagents.flow.core.knowledge.KnowledgeProvider;
|
||||
import com.easyagents.flow.core.node.KnowledgeNode;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -30,7 +32,17 @@ public class KnowledgeProviderImpl implements KnowledgeProvider {
|
||||
return new Knowledge() {
|
||||
@Override
|
||||
public List<Map<String, Object>> search(String keyword, int limit, KnowledgeNode knowledgeNode, Chain chain) {
|
||||
List<Document> documents = documentCollectionService.search(new BigInteger(id.toString()), keyword);
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(new BigInteger(id.toString()));
|
||||
request.setQuery(keyword);
|
||||
request.setLimit(limit);
|
||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(knowledgeNode.getRetrievalMode()));
|
||||
request.setCallerType("WORKFLOW");
|
||||
request.setCallerId(knowledgeNode.getId());
|
||||
List<Document> documents = documentCollectionService.search(request);
|
||||
if (limit > 0 && documents.size() > limit) {
|
||||
documents = new ArrayList<>(documents.subList(0, limit));
|
||||
}
|
||||
List<Map<String, Object>> res = new ArrayList<>();
|
||||
for (Document document : documents) {
|
||||
res.add(JSONObject.from(document));
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import tech.easyflow.ai.entity.base.BotDocumentCollectionBase;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import com.mybatisflex.annotation.RelationOneToOne;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 实体类。
|
||||
*
|
||||
@@ -14,6 +19,8 @@ import com.mybatisflex.annotation.Table;
|
||||
@Table("tb_bot_document_collection")
|
||||
public class BotDocumentCollection extends BotDocumentCollectionBase {
|
||||
|
||||
public static final String OPTION_KEY_RETRIEVAL_MODE = "retrievalMode";
|
||||
|
||||
@RelationOneToOne(selfField = "documentCollectionId", targetField = "id")
|
||||
private DocumentCollection knowledge;
|
||||
|
||||
@@ -24,4 +31,21 @@ public class BotDocumentCollection extends BotDocumentCollectionBase {
|
||||
public void setKnowledge(DocumentCollection knowledge) {
|
||||
this.knowledge = knowledge;
|
||||
}
|
||||
|
||||
public RetrievalMode getRetrievalMode() {
|
||||
Map<String, Object> options = getOptions();
|
||||
if (options == null) {
|
||||
return RetrievalMode.HYBRID;
|
||||
}
|
||||
Object value = options.get(OPTION_KEY_RETRIEVAL_MODE);
|
||||
return KnowledgeRetrievalModes.parse(value == null ? null : String.valueOf(value));
|
||||
}
|
||||
|
||||
public void setRetrievalMode(RetrievalMode retrievalMode) {
|
||||
Map<String, Object> options = getOptions() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(getOptions());
|
||||
options.put(OPTION_KEY_RETRIEVAL_MODE, (retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode).name());
|
||||
setOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,16 @@ package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.store.aliyun.AliyunVectorStore;
|
||||
import com.easyagents.store.aliyun.AliyunVectorStoreConfig;
|
||||
import com.easyagents.store.elasticsearch.ElasticSearchVectorStore;
|
||||
import com.easyagents.store.elasticsearch.ElasticSearchVectorStoreConfig;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import com.easyagents.store.milvus.MilvusVectorStore;
|
||||
import com.easyagents.store.milvus.MilvusVectorStoreConfig;
|
||||
import com.easyagents.store.opensearch.OpenSearchVectorStore;
|
||||
import com.easyagents.store.opensearch.OpenSearchVectorStoreConfig;
|
||||
import com.easyagents.store.qcloud.QCloudVectorStore;
|
||||
import com.easyagents.store.qcloud.QCloudVectorStoreConfig;
|
||||
import com.easyagents.store.redis.RedisVectorStore;
|
||||
import com.easyagents.store.redis.RedisVectorStoreConfig;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.config.AiMilvusConfig;
|
||||
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
|
||||
import tech.easyflow.ai.entity.base.DocumentCollectionBase;
|
||||
import tech.easyflow.common.util.PropertiesUtil;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.common.util.SpringContextUtil;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -58,11 +50,6 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
*/
|
||||
public static final String KEY_SIMILARITY_THRESHOLD = "simThreshold";
|
||||
|
||||
/**
|
||||
* 搜索引擎类型
|
||||
*/
|
||||
public static final String KEY_SEARCH_ENGINE_TYPE = "searchEngineType";
|
||||
|
||||
/**
|
||||
* 是否允许更新向量模型
|
||||
*/
|
||||
@@ -78,28 +65,10 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles";
|
||||
|
||||
public DocumentStore toDocumentStore() {
|
||||
String storeType = this.getVectorStoreType();
|
||||
if (StringUtil.noText(storeType)) {
|
||||
throw new BusinessException("向量数据库类型未设置");
|
||||
}
|
||||
if (storeType == null) {
|
||||
if (StringUtil.noText(this.getVectorStoreCollection())) {
|
||||
return null;
|
||||
}
|
||||
switch (storeType.toLowerCase()) {
|
||||
case "redis":
|
||||
return redisStore();
|
||||
case "milvus":
|
||||
return milvusStore();
|
||||
case "opensearch":
|
||||
return openSearchStore();
|
||||
case "elasticsearch":
|
||||
return elasticSearchStore();
|
||||
case "aliyun":
|
||||
return aliyunStore();
|
||||
case "qcloud":
|
||||
return qcloudStore();
|
||||
}
|
||||
return null;
|
||||
return milvusStore();
|
||||
}
|
||||
|
||||
public boolean isVectorStoreEnabled() {
|
||||
@@ -115,53 +84,31 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
}
|
||||
|
||||
public boolean isSearchEngineEnabled() {
|
||||
return this.getSearchEngineEnable() != null && this.getSearchEngineEnable();
|
||||
}
|
||||
|
||||
|
||||
private DocumentStore redisStore() {
|
||||
RedisVectorStoreConfig redisVectorStoreConfig = getStoreConfig(RedisVectorStoreConfig.class);
|
||||
return new RedisVectorStore(redisVectorStoreConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
private DocumentStore milvusStore() {
|
||||
MilvusVectorStoreConfig milvusVectorStoreConfig = getStoreConfig(MilvusVectorStoreConfig.class);
|
||||
if (milvusVectorStoreConfig != null && StringUtil.noText(milvusVectorStoreConfig.getDefaultCollectionName())) {
|
||||
milvusVectorStoreConfig.setDefaultCollectionName(this.getVectorStoreCollection());
|
||||
}
|
||||
AiMilvusConfig aiMilvusConfig = SpringContextUtil.getBean(AiMilvusConfig.class);
|
||||
MilvusVectorStoreConfig milvusVectorStoreConfig = aiMilvusConfig.copyForCollection(this.getVectorStoreCollection());
|
||||
return new MilvusVectorStore(milvusVectorStoreConfig);
|
||||
}
|
||||
|
||||
private DocumentStore openSearchStore() {
|
||||
OpenSearchVectorStoreConfig openSearchVectorStoreConfig = getStoreConfig(OpenSearchVectorStoreConfig.class);
|
||||
return new OpenSearchVectorStore(openSearchVectorStoreConfig);
|
||||
}
|
||||
|
||||
private DocumentStore elasticSearchStore() {
|
||||
ElasticSearchVectorStoreConfig elasticSearchVectorStoreConfig = getStoreConfig(ElasticSearchVectorStoreConfig.class);
|
||||
return new ElasticSearchVectorStore(elasticSearchVectorStoreConfig);
|
||||
}
|
||||
|
||||
private DocumentStore aliyunStore() {
|
||||
AliyunVectorStoreConfig aliyunVectorStoreConfig = getStoreConfig(AliyunVectorStoreConfig.class);
|
||||
return new AliyunVectorStore(aliyunVectorStoreConfig);
|
||||
}
|
||||
|
||||
private DocumentStore qcloudStore() {
|
||||
QCloudVectorStoreConfig qCloudVectorStoreConfig = getStoreConfig(QCloudVectorStoreConfig.class);
|
||||
return new QCloudVectorStore(qCloudVectorStoreConfig);
|
||||
}
|
||||
|
||||
private <T> T getStoreConfig(Class<T> clazz) {
|
||||
return PropertiesUtil.propertiesTextToEntity(this.getVectorStoreConfig(), clazz);
|
||||
}
|
||||
|
||||
public Tool toFunction(boolean needEnglishName) {
|
||||
return new DocumentCollectionTool(this, needEnglishName);
|
||||
return toFunction(needEnglishName, RetrievalMode.HYBRID.name());
|
||||
}
|
||||
|
||||
public Tool toFunction(boolean needEnglishName, String retrievalMode) {
|
||||
return new DocumentCollectionTool(this, needEnglishName, KnowledgeRetrievalModes.parse(retrievalMode));
|
||||
}
|
||||
|
||||
public Object getOptionsByKey(String key) {
|
||||
Map<String, Object> options = this.getOptions();
|
||||
if (KEY_DOC_RECALL_MAX_NUM.equals(key) && (options == null || !options.containsKey(KEY_DOC_RECALL_MAX_NUM))) {
|
||||
return 5;
|
||||
}
|
||||
if (KEY_SIMILARITY_THRESHOLD.equals(key) && (options == null || !options.containsKey(KEY_SIMILARITY_THRESHOLD))) {
|
||||
return 0.6f;
|
||||
}
|
||||
if (KEY_RERANK_ENABLE.equals(key)) {
|
||||
if (options == null || !options.containsKey(KEY_RERANK_ENABLE)) {
|
||||
return this.getRerankModelId() != null;
|
||||
@@ -182,21 +129,9 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi
|
||||
if (options == null) {
|
||||
return null;
|
||||
}
|
||||
if (KEY_DOC_RECALL_MAX_NUM.equals(key) && !options.containsKey(KEY_DOC_RECALL_MAX_NUM)) {
|
||||
return 5;
|
||||
}
|
||||
if (KEY_SIMILARITY_THRESHOLD.equals(key)) {
|
||||
if (!options.containsKey(KEY_SIMILARITY_THRESHOLD)) {
|
||||
return 0.6f;
|
||||
} else {
|
||||
BigDecimal score = (BigDecimal) options.get(key);
|
||||
return (float) score.doubleValue();
|
||||
}
|
||||
}
|
||||
if (KEY_SEARCH_ENGINE_TYPE.equals(key)) {
|
||||
if (!options.containsKey(KEY_SEARCH_ENGINE_TYPE)) {
|
||||
return "lucene";
|
||||
}
|
||||
BigDecimal score = (BigDecimal) options.get(key);
|
||||
return (float) score.doubleValue();
|
||||
}
|
||||
return options.get(key);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class OpenAiGateway extends AbstractOpenAiCompatibleGateway implements Mo
|
||||
"zhipu",
|
||||
"minimax",
|
||||
"kimi",
|
||||
"siliconlow",
|
||||
"siliconflow",
|
||||
"ollama",
|
||||
"self-hosted"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package tech.easyflow.ai.rag;
|
||||
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
public enum KeywordEngineType {
|
||||
ES,
|
||||
LUCENE;
|
||||
|
||||
public static KeywordEngineType from(String value) {
|
||||
if (StringUtil.noText(value)) {
|
||||
return ES;
|
||||
}
|
||||
for (KeywordEngineType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(value.trim())) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new BusinessException("不支持的关键词检索引擎: " + value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package tech.easyflow.ai.rag;
|
||||
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
public final class KnowledgeRetrievalModes {
|
||||
|
||||
private KnowledgeRetrievalModes() {
|
||||
}
|
||||
|
||||
public static RetrievalMode parse(String value) {
|
||||
try {
|
||||
return RetrievalMode.from(value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("不支持的检索方式: " + value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package tech.easyflow.ai.rag;
|
||||
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class KnowledgeRetrievalRequest {
|
||||
|
||||
private BigInteger knowledgeId;
|
||||
private String query;
|
||||
private Integer limit;
|
||||
private RetrievalMode retrievalMode = RetrievalMode.HYBRID;
|
||||
private String callerType;
|
||||
private String callerId;
|
||||
|
||||
public BigInteger getKnowledgeId() {
|
||||
return knowledgeId;
|
||||
}
|
||||
|
||||
public void setKnowledgeId(BigInteger knowledgeId) {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(String query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
public Integer getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(Integer limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public RetrievalMode getRetrievalMode() {
|
||||
return retrievalMode;
|
||||
}
|
||||
|
||||
public void setRetrievalMode(RetrievalMode retrievalMode) {
|
||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||
}
|
||||
|
||||
public String getCallerType() {
|
||||
return callerType;
|
||||
}
|
||||
|
||||
public void setCallerType(String callerType) {
|
||||
this.callerType = callerType;
|
||||
}
|
||||
|
||||
public String getCallerId() {
|
||||
return callerId;
|
||||
}
|
||||
|
||||
public void setCallerId(String callerId) {
|
||||
this.callerId = callerId;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import tech.easyflow.ai.dto.BotKnowledgeBindingRequest;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
@@ -16,5 +17,5 @@ public interface BotDocumentCollectionService extends IService<BotDocumentCollec
|
||||
|
||||
List<BotDocumentCollection> listByBotId(BigInteger botId);
|
||||
|
||||
void saveBotAndKnowledge(BigInteger botId, BigInteger[] knowledgeIds);
|
||||
void saveBotAndKnowledge(BigInteger botId, List<BotKnowledgeBindingRequest> knowledgeBindings);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import tech.easyflow.ai.entity.Bot;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
@@ -31,8 +32,10 @@ public interface BotService extends IService<Bot> {
|
||||
|
||||
SseEmitter checkChatBeforeStart(BigInteger botId, String prompt, String conversationId, BotServiceImpl.ChatCheckResult chatCheckResult);
|
||||
|
||||
SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages, BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments);
|
||||
SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages,
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments, ChatRuntimeContext runtimeContext);
|
||||
|
||||
SseEmitter startPublicChat(BigInteger botId, String prompt, List<Message> messages, BotServiceImpl.ChatCheckResult chatCheckResult);
|
||||
SseEmitter startPublicChat(BigInteger botId, String prompt, List<Message> messages,
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, ChatRuntimeContext runtimeContext);
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package tech.easyflow.ai.service;
|
||||
|
||||
import com.easyagents.core.document.Document;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import com.mybatisflex.core.service.IService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -17,6 +18,8 @@ public interface DocumentCollectionService extends IService<DocumentCollection>
|
||||
|
||||
List<Document> search(BigInteger id, String keyword);
|
||||
|
||||
List<Document> search(KnowledgeRetrievalRequest request);
|
||||
|
||||
DocumentCollection getDetail(String idOrAlias);
|
||||
|
||||
DocumentCollection getByAlias(String idOrAlias);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import tech.easyflow.ai.dto.BotKnowledgeBindingRequest;
|
||||
import tech.easyflow.ai.entity.BotDocumentCollection;
|
||||
import tech.easyflow.ai.mapper.BotDocumentCollectionMapper;
|
||||
import tech.easyflow.ai.service.BotDocumentCollectionService;
|
||||
@@ -11,9 +13,10 @@ import tech.easyflow.common.cache.RedisLockExecutor;
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.time.Duration;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
|
||||
@@ -44,26 +47,30 @@ public class BotDocumentCollectionServiceImpl extends ServiceImpl<BotDocumentCol
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void saveBotAndKnowledge(BigInteger botId, BigInteger[] knowledgeIds) {
|
||||
public void saveBotAndKnowledge(BigInteger botId, List<BotKnowledgeBindingRequest> knowledgeBindings) {
|
||||
redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> {
|
||||
this.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId));
|
||||
Set<BigInteger> uniqueKnowledgeIds = new LinkedHashSet<>();
|
||||
if (knowledgeIds != null) {
|
||||
for (BigInteger knowledgeId : knowledgeIds) {
|
||||
if (knowledgeId != null) {
|
||||
uniqueKnowledgeIds.add(knowledgeId);
|
||||
Map<BigInteger, RetrievalMode> bindingMap = new LinkedHashMap<>();
|
||||
if (knowledgeBindings != null) {
|
||||
for (BotKnowledgeBindingRequest binding : knowledgeBindings) {
|
||||
if (binding == null || binding.getKnowledgeId() == null) {
|
||||
continue;
|
||||
}
|
||||
bindingMap.put(binding.getKnowledgeId(), binding.resolveRetrievalMode());
|
||||
}
|
||||
}
|
||||
if (uniqueKnowledgeIds.isEmpty()) {
|
||||
if (bindingMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<BotDocumentCollection> list = new ArrayList<>(uniqueKnowledgeIds.size());
|
||||
for (BigInteger knowledgeId : uniqueKnowledgeIds) {
|
||||
List<BotDocumentCollection> list = new ArrayList<>(bindingMap.size());
|
||||
for (Map.Entry<BigInteger, RetrievalMode> entry : bindingMap.entrySet()) {
|
||||
BotDocumentCollection botDocumentCollection = new BotDocumentCollection();
|
||||
botDocumentCollection.setBotId(botId);
|
||||
botDocumentCollection.setDocumentCollectionId(knowledgeId);
|
||||
botDocumentCollection.setDocumentCollectionId(entry.getKey());
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put(BotDocumentCollection.OPTION_KEY_RETRIEVAL_MODE, entry.getValue().name());
|
||||
botDocumentCollection.setOptions(options);
|
||||
list.add(botDocumentCollection);
|
||||
}
|
||||
this.saveBatch(list);
|
||||
|
||||
@@ -24,9 +24,9 @@ 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.ai.easyagents.listener.ChatStreamListener;
|
||||
import tech.easyflow.ai.easyagents.memory.BotMessageMemory;
|
||||
import tech.easyflow.ai.easyagents.memory.DefaultBotMessageMemory;
|
||||
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
||||
import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.mapper.BotMapper;
|
||||
import tech.easyflow.ai.service.*;
|
||||
@@ -41,11 +41,17 @@ import tech.easyflow.common.util.UrlEncoderUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeManager;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -87,9 +93,6 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
public void setConversationIdStr(String conversationIdStr) {this.conversationIdStr = conversationIdStr;}
|
||||
}
|
||||
|
||||
@Resource
|
||||
private BotMessageService botMessageService;
|
||||
|
||||
@Resource(name = "sseThreadPool")
|
||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||
@Resource
|
||||
@@ -110,6 +113,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
FileStorageService storageService;
|
||||
@Resource
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
@Resource
|
||||
private ChatRuntimeManager chatRuntimeManager;
|
||||
|
||||
@Override
|
||||
public Bot getDetail(String id) {
|
||||
@@ -189,7 +194,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
|
||||
@Override
|
||||
public SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages,
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments) {
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, List<String> attachments, ChatRuntimeContext runtimeContext) {
|
||||
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||
final MemoryPrompt memoryPrompt = new MemoryPrompt();
|
||||
@@ -214,19 +219,33 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
chatOptions.setThinkingEnabled(enableDeepThinking);
|
||||
ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
|
||||
SseEmitter emitter = chatSseEmitter.getEmitter();
|
||||
int historyLimit = resolveHistoryLimit(maxMessageCount);
|
||||
if (messages != null && !messages.isEmpty()) {
|
||||
ChatMemory defaultChatMemory = new DefaultBotMessageMemory(conversationId, chatSseEmitter, messages);
|
||||
memoryPrompt.setMemory(defaultChatMemory);
|
||||
} else {
|
||||
BotMessageMemory memory = new BotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(), conversationId, botMessageService);
|
||||
memoryPrompt.setMemory(memory);
|
||||
memoryPrompt.setMemory(new RuntimeChatMemory(
|
||||
conversationId,
|
||||
chatRuntimeManager.loadMessages(runtimeContext, historyLimit)
|
||||
));
|
||||
}
|
||||
chatRuntimeManager.prepareSession(runtimeContext);
|
||||
chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, attachments));
|
||||
memoryPrompt.addMessage(userMessage);
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
threadPoolTaskExecutor.execute(() -> {
|
||||
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
||||
RequestContextHolder.setRequestAttributes(sra, true);
|
||||
StreamResponseListener streamResponseListener = new ChatStreamListener(conversationId.toString(), chatModel, memoryPrompt, chatSseEmitter, chatOptions);
|
||||
StreamResponseListener streamResponseListener = new ChatStreamListener(
|
||||
conversationId.toString(),
|
||||
chatModel,
|
||||
memoryPrompt,
|
||||
chatSseEmitter,
|
||||
chatOptions,
|
||||
chatRuntimeManager,
|
||||
runtimeContext,
|
||||
new ChatAssistantAccumulator()
|
||||
);
|
||||
chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
|
||||
});
|
||||
|
||||
@@ -239,7 +258,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter startPublicChat(BigInteger botId, String prompt, List<Message> messages, BotServiceImpl.ChatCheckResult chatCheckResult) {
|
||||
public SseEmitter startPublicChat(BigInteger botId, String prompt, List<Message> messages,
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, ChatRuntimeContext runtimeContext) {
|
||||
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||
ChatOptions chatOptions = getChatOptions(modelOptions);
|
||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||
@@ -260,11 +280,22 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
memoryPrompt.setSystemMessage(SystemMessage.of(systemPrompt));
|
||||
}
|
||||
memoryPrompt.addMessage(userMessage);
|
||||
chatRuntimeManager.prepareSession(runtimeContext);
|
||||
chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, null));
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
||||
threadPoolTaskExecutor.execute(() -> {
|
||||
RequestContextHolder.setRequestAttributes(sra, true);
|
||||
StreamResponseListener streamResponseListener = new ChatStreamListener(chatCheckResult.getConversationIdStr(), chatModel, memoryPrompt, chatSseEmitter, chatOptions);
|
||||
StreamResponseListener streamResponseListener = new ChatStreamListener(
|
||||
chatCheckResult.getConversationIdStr(),
|
||||
chatModel,
|
||||
memoryPrompt,
|
||||
chatSseEmitter,
|
||||
chatOptions,
|
||||
chatRuntimeManager,
|
||||
runtimeContext,
|
||||
new ChatAssistantAccumulator()
|
||||
);
|
||||
chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
|
||||
});
|
||||
|
||||
@@ -370,7 +401,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
.selectListWithRelationsByQuery(queryWrapper);
|
||||
if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) {
|
||||
for (BotDocumentCollection botDocumentCollection : botDocumentCollections) {
|
||||
Tool function = botDocumentCollection.getKnowledge().toFunction(needEnglishName);
|
||||
Tool function = botDocumentCollection.getKnowledge()
|
||||
.toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name());
|
||||
functionList.add(function);
|
||||
}
|
||||
}
|
||||
@@ -425,6 +457,36 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
return messageBuilder.toString();
|
||||
}
|
||||
|
||||
private int resolveHistoryLimit(Integer maxMessageCount) {
|
||||
if (maxMessageCount == null || maxMessageCount <= 0) {
|
||||
return 20;
|
||||
}
|
||||
return Math.min(maxMessageCount, 200);
|
||||
}
|
||||
|
||||
private ChatRuntimeMessage buildUserRuntimeMessage(ChatRuntimeContext context, String prompt, List<String> attachments) {
|
||||
ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
|
||||
runtimeMessage.setRole("user");
|
||||
runtimeMessage.setContentType("TEXT");
|
||||
runtimeMessage.setContentText(prompt);
|
||||
runtimeMessage.setCreatedAt(new Date());
|
||||
runtimeMessage.setSenderId(context.getUserId());
|
||||
runtimeMessage.setSenderName(resolveUserDisplayName(context));
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("attachments", attachments);
|
||||
runtimeMessage.setContentPayload(payload);
|
||||
}
|
||||
return runtimeMessage;
|
||||
}
|
||||
|
||||
private String resolveUserDisplayName(ChatRuntimeContext context) {
|
||||
if (context.getUserName() != null && !context.getUserName().isBlank()) {
|
||||
return context.getUserName();
|
||||
}
|
||||
return context.getUserAccount();
|
||||
}
|
||||
|
||||
private String buildSystemPromptWithFaqImageRule(String systemPrompt) {
|
||||
if (!StringUtils.hasLength(systemPrompt)) {
|
||||
return FAQ_IMAGE_SYSTEM_RULE;
|
||||
|
||||
@@ -12,8 +12,6 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_SEARCH_ENGINE_TYPE;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
*
|
||||
@@ -28,10 +26,9 @@ public class DocumentChunkServiceImpl extends ServiceImpl<DocumentChunkMapper, D
|
||||
|
||||
@Override
|
||||
public boolean removeChunk(DocumentCollection knowledge, BigInteger chunkId) {
|
||||
String searchEngineType = (String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE);
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher(searchEngineType);
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
// 删除搜索引擎中的数据
|
||||
if (searcherFactory.getSearcher(searchEngineType) == null){
|
||||
if (searcher == null){
|
||||
return true;
|
||||
}
|
||||
return searcher.deleteDocument(chunkId);
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.easyagents.core.model.rerank.RerankException;
|
||||
import com.easyagents.core.model.rerank.RerankModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.SearchWrapper;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.rag.retrieval.HitSource;
|
||||
import com.easyagents.rag.retrieval.KeywordRetriever;
|
||||
import com.easyagents.rag.retrieval.RagHit;
|
||||
import com.easyagents.rag.retrieval.RagQuery;
|
||||
import com.easyagents.rag.retrieval.RagRetrievalExecutor;
|
||||
import com.easyagents.rag.retrieval.RagScoreNormalizer;
|
||||
import com.easyagents.rag.retrieval.RagRetrievalResult;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import com.easyagents.rag.retrieval.RrfFusionStrategy;
|
||||
import com.easyagents.rag.retrieval.VectorRetriever;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.easyagents.search.engine.service.KeywordSearchRequest;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.slf4j.Logger;
|
||||
@@ -22,7 +32,7 @@ import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||
import tech.easyflow.ai.mapper.FaqItemMapper;
|
||||
import tech.easyflow.ai.service.DocumentChunkService;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
import tech.easyflow.ai.utils.CustomBeanUtils;
|
||||
@@ -35,12 +45,18 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.*;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_DOC_RECALL_MAX_NUM;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_RERANK_ENABLE;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_SIMILARITY_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
@@ -57,9 +73,6 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
@Resource
|
||||
private ModelService llmService;
|
||||
|
||||
@Resource
|
||||
private DocumentChunkService chunkService;
|
||||
|
||||
@Autowired
|
||||
private SearcherFactory searcherFactory;
|
||||
|
||||
@@ -69,14 +82,96 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
@Autowired
|
||||
private FaqItemMapper faqItemMapper;
|
||||
|
||||
|
||||
@Override
|
||||
public List<Document> search(BigInteger id, String keyword) {
|
||||
DocumentCollection documentCollection = getById(id);
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(id);
|
||||
request.setQuery(keyword);
|
||||
request.setRetrievalMode(RetrievalMode.HYBRID);
|
||||
return search(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Document> search(KnowledgeRetrievalRequest request) {
|
||||
if (request == null || request.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
}
|
||||
String keyword = request.getQuery();
|
||||
if (StringUtil.noText(keyword)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
RetrievalMode retrievalMode = request.getRetrievalMode() == null
|
||||
? RetrievalMode.HYBRID
|
||||
: request.getRetrievalMode();
|
||||
DocumentCollection documentCollection = getById(request.getKnowledgeId());
|
||||
if (documentCollection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
|
||||
int docRecallMaxNum = readIntegerOption(documentCollection, KEY_DOC_RECALL_MAX_NUM, 5);
|
||||
float minSimilarity = readFloatOption(documentCollection, KEY_SIMILARITY_THRESHOLD, 0.6F);
|
||||
|
||||
RagQuery ragQuery = new RagQuery();
|
||||
ragQuery.setQuery(keyword);
|
||||
ragQuery.setRetrievalMode(retrievalMode);
|
||||
ragQuery.setTopK(docRecallMaxNum);
|
||||
ragQuery.setMinScore((double) minSimilarity);
|
||||
|
||||
RagRetrievalExecutor retrievalExecutor = new RagRetrievalExecutor(
|
||||
buildVectorRetriever(documentCollection, docRecallMaxNum, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null),
|
||||
buildKeywordRetriever(documentCollection, docRecallMaxNum),
|
||||
new RrfFusionStrategy()
|
||||
);
|
||||
|
||||
RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery);
|
||||
List<Document> searchDocuments = toDocuments(retrievalResult.getHits());
|
||||
fillSearchContent(documentCollection, searchDocuments);
|
||||
if (searchDocuments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
RerankModel rerankModel = resolveRerankModel(documentCollection);
|
||||
boolean reranked = false;
|
||||
if (rerankModel != null) {
|
||||
try {
|
||||
RagRetrievalResult rerankResult = retrievalExecutor.rerank(keyword, toRagHits(searchDocuments), rerankModel, docRecallMaxNum);
|
||||
searchDocuments = toDocuments(rerankResult.getHits());
|
||||
reranked = true;
|
||||
} catch (RerankException e) {
|
||||
LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to retrieved results. message={}",
|
||||
documentCollection.getId(), documentCollection.getRerankModelId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
RagScoreNormalizer.normalize(searchDocuments, retrievalMode, reranked);
|
||||
return formatDocuments(searchDocuments, shouldApplyMinSimilarityFilter(retrievalMode, reranked), minSimilarity, docRecallMaxNum);
|
||||
}
|
||||
|
||||
private VectorRetriever buildVectorRetriever(DocumentCollection documentCollection,
|
||||
int docRecallMaxNum,
|
||||
Float minSimilarity) {
|
||||
return new VectorRetriever() {
|
||||
@Override
|
||||
public List<RagHit> retrieve(RagQuery query) {
|
||||
return adaptDocuments(searchVectorDocuments(documentCollection, query.getQuery(), docRecallMaxNum, minSimilarity), HitSource.VECTOR);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private KeywordRetriever buildKeywordRetriever(DocumentCollection documentCollection, int docRecallMaxNum) {
|
||||
return new KeywordRetriever() {
|
||||
@Override
|
||||
public List<RagHit> retrieve(RagQuery query) {
|
||||
return adaptDocuments(searchKeywordDocuments(documentCollection, query.getQuery(), docRecallMaxNum), HitSource.KEYWORD);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<Document> searchVectorDocuments(DocumentCollection documentCollection,
|
||||
String keyword,
|
||||
int docRecallMaxNum,
|
||||
Float minSimilarity) {
|
||||
DocumentStore documentStore = documentCollection.toDocumentStore();
|
||||
if (documentStore == null) {
|
||||
throw new BusinessException("知识库没有配置向量库");
|
||||
@@ -88,88 +183,92 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
}
|
||||
|
||||
documentStore.setEmbeddingModel(model.toEmbeddingModel());
|
||||
// 最大召回知识条数
|
||||
Integer docRecallMaxNum = (Integer) documentCollection.getOptionsByKey(KEY_DOC_RECALL_MAX_NUM);
|
||||
// 最低相似度
|
||||
float minSimilarity = (float) documentCollection.getOptionsByKey(KEY_SIMILARITY_THRESHOLD);
|
||||
SearchWrapper wrapper = new SearchWrapper();
|
||||
wrapper.setMaxResults(docRecallMaxNum);
|
||||
wrapper.setMinScore((double) minSimilarity);
|
||||
if (minSimilarity != null) {
|
||||
wrapper.setMinScore((double) minSimilarity);
|
||||
}
|
||||
wrapper.setText(keyword);
|
||||
|
||||
StoreOptions options = StoreOptions.ofCollectionName(documentCollection.getVectorStoreCollection());
|
||||
options.setIndexName(documentCollection.getVectorStoreCollection());
|
||||
List<Document> documents = documentStore.search(wrapper, options);
|
||||
return documents == null ? Collections.<Document>emptyList() : documents;
|
||||
}
|
||||
|
||||
// 并行查询:向量库 + 搜索引擎
|
||||
CompletableFuture<List<Document>> vectorFuture = CompletableFuture.supplyAsync(() ->
|
||||
documentStore.search(wrapper, options)
|
||||
);
|
||||
|
||||
CompletableFuture<List<Document>> searcherFuture = CompletableFuture.supplyAsync(() -> {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher((String) documentCollection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
if (searcher == null || !documentCollection.isSearchEngineEnabled()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Document> documents = searcher.searchDocuments(keyword);
|
||||
return documents == null ? Collections.emptyList() : documents;
|
||||
});
|
||||
|
||||
// 合并两个查询结果
|
||||
CompletableFuture<Map<String, Document>> combinedFuture = vectorFuture.thenCombine(
|
||||
searcherFuture,
|
||||
(vectorDocs, searcherDocs) -> {
|
||||
Map<String, Document> uniqueDocs = new HashMap<>();
|
||||
vectorDocs.forEach(doc -> uniqueDocs.putIfAbsent(doc.getId().toString(), doc));
|
||||
searcherDocs.forEach(doc -> uniqueDocs.putIfAbsent(doc.getId().toString(), doc));
|
||||
return uniqueDocs;
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
Map<String, Document> uniqueDocs = combinedFuture.get(); // 阻塞等待所有查询完成
|
||||
List<Document> searchDocuments = new ArrayList<>(uniqueDocs.values());
|
||||
searchDocuments.sort((doc1, doc2) -> Double.compare(doc2.getScore(), doc1.getScore()));
|
||||
fillSearchContent(documentCollection, searchDocuments);
|
||||
if (searchDocuments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
boolean rerankEnable = Boolean.TRUE.equals(documentCollection.getOptionsByKey(KEY_RERANK_ENABLE));
|
||||
if (!rerankEnable || documentCollection.getRerankModelId() == null) {
|
||||
return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum);
|
||||
}
|
||||
|
||||
Model modelRerank = llmService.getModelInstance(documentCollection.getRerankModelId());
|
||||
if (modelRerank == null) {
|
||||
return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum);
|
||||
}
|
||||
|
||||
RerankModel rerankModel = modelRerank.toRerankModel();
|
||||
if (rerankModel == null) {
|
||||
return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum);
|
||||
}
|
||||
|
||||
Map<Object, Double> originalScores = new HashMap<>();
|
||||
searchDocuments.forEach(item -> originalScores.put(item.getId(), item.getScore()));
|
||||
searchDocuments.forEach(item -> item.setScore(null));
|
||||
try {
|
||||
List<Document> rerankDocuments = rerankModel.rerank(keyword, searchDocuments);
|
||||
return formatDocuments(rerankDocuments, minSimilarity, docRecallMaxNum);
|
||||
} catch (RerankException e) {
|
||||
searchDocuments.forEach(item -> item.setScore(originalScores.get(item.getId())));
|
||||
LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to vector results. message={}",
|
||||
documentCollection.getId(), documentCollection.getRerankModelId(), e.getMessage());
|
||||
return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum);
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
private List<Document> searchKeywordDocuments(DocumentCollection documentCollection, String keyword, int docRecallMaxNum) {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (searcher == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KeywordSearchRequest request = KeywordSearchRequest.of(keyword, docRecallMaxNum);
|
||||
request.setKnowledgeId(documentCollection == null || documentCollection.getId() == null
|
||||
? null
|
||||
: documentCollection.getId().toString());
|
||||
List<Document> documents = searcher.searchDocuments(request);
|
||||
return documents == null ? Collections.<Document>emptyList() : documents;
|
||||
}
|
||||
|
||||
private List<RagHit> adaptDocuments(List<Document> documents, HitSource hitSource) {
|
||||
List<RagHit> hits = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return hits;
|
||||
}
|
||||
for (Document document : documents) {
|
||||
RagHit hit = RagHit.fromDocument(document, hitSource);
|
||||
if (hit != null) {
|
||||
hits.add(hit);
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
private List<Document> toDocuments(List<RagHit> hits) {
|
||||
List<Document> documents = new ArrayList<>();
|
||||
if (hits == null) {
|
||||
return documents;
|
||||
}
|
||||
for (RagHit hit : hits) {
|
||||
if (hit == null) {
|
||||
continue;
|
||||
}
|
||||
documents.add(hit.toDocument());
|
||||
}
|
||||
return documents;
|
||||
}
|
||||
|
||||
private List<RagHit> toRagHits(List<Document> documents) {
|
||||
List<RagHit> hits = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return hits;
|
||||
}
|
||||
for (Document document : documents) {
|
||||
RagHit hit = RagHit.fromDocument(document);
|
||||
if (hit != null) {
|
||||
hits.add(hit);
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
private RerankModel resolveRerankModel(DocumentCollection documentCollection) {
|
||||
boolean rerankEnable = Boolean.TRUE.equals(documentCollection.getOptionsByKey(KEY_RERANK_ENABLE));
|
||||
if (!rerankEnable || documentCollection.getRerankModelId() == null) {
|
||||
return null;
|
||||
}
|
||||
Model modelRerank = llmService.getModelInstance(documentCollection.getRerankModelId());
|
||||
if (modelRerank == null) {
|
||||
return null;
|
||||
}
|
||||
return modelRerank.toRerankModel();
|
||||
}
|
||||
|
||||
private boolean shouldApplyMinSimilarityFilter(RetrievalMode retrievalMode, boolean reranked) {
|
||||
return !reranked && retrievalMode == RetrievalMode.VECTOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentCollection getDetail(String idOrAlias) {
|
||||
|
||||
DocumentCollection knowledge = null;
|
||||
|
||||
if (idOrAlias.matches(RegexUtils.ALL_NUMBER)) {
|
||||
@@ -188,15 +287,11 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
|
||||
@Override
|
||||
public DocumentCollection getByAlias(String idOrAlias) {
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.eq(DocumentCollection::getAlias, idOrAlias);
|
||||
|
||||
return getOne(queryWrapper);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean updateById(DocumentCollection entity) {
|
||||
DocumentCollection documentCollection = getById(entity.getId());
|
||||
@@ -210,38 +305,29 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
documentCollection.setAlias(null);
|
||||
}
|
||||
|
||||
|
||||
return super.updateById(documentCollection, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文档列表
|
||||
*
|
||||
* @param documents 文档列表
|
||||
* @param minSimilarity 最小相似度
|
||||
* @return 格式化后的文档列表
|
||||
*/
|
||||
public List<Document> formatDocuments(List<Document> documents, float minSimilarity, int maxResults) {
|
||||
public List<Document> formatDocuments(List<Document> documents,
|
||||
boolean applyMinSimilarity,
|
||||
float minSimilarity,
|
||||
int maxResults) {
|
||||
return documents.stream()
|
||||
// 过滤掉分数为空 或 分数低于最小值的文档
|
||||
.filter(document -> {
|
||||
Double score = document.getScore();
|
||||
return score != null && score >= minSimilarity;
|
||||
})
|
||||
// 格式化保留四位小数
|
||||
.map(document -> {
|
||||
Double score = document.getScore();
|
||||
BigDecimal bd = new BigDecimal(score.toString());
|
||||
bd = bd.setScale(4, RoundingMode.HALF_UP);
|
||||
Double roundedScore = bd.doubleValue();
|
||||
document.setScore(roundedScore);
|
||||
return document;
|
||||
})
|
||||
// 按score降序排序(分数最高的排前面)
|
||||
.sorted(Comparator.comparing(Document::getScore, Comparator.reverseOrder()))
|
||||
// 限制只保留前maxResults条
|
||||
.limit(maxResults)
|
||||
.collect(Collectors.toList());
|
||||
.filter(Objects::nonNull)
|
||||
.filter(document -> !applyMinSimilarity
|
||||
|| (document.getScore() != null && document.getScore() >= minSimilarity))
|
||||
.map(this::roundDocumentScore)
|
||||
.sorted(Comparator.comparing(Document::getScore, Comparator.nullsLast(Comparator.reverseOrder())))
|
||||
.limit(maxResults)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private Document roundDocumentScore(Document document) {
|
||||
if (document == null || document.getScore() == null) {
|
||||
return document;
|
||||
}
|
||||
document.setScore(roundDouble(document.getScore()));
|
||||
return document;
|
||||
}
|
||||
|
||||
private void fillSearchContent(DocumentCollection documentCollection, List<Document> searchDocuments) {
|
||||
@@ -249,33 +335,42 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
return;
|
||||
}
|
||||
List<Serializable> ids = searchDocuments.stream()
|
||||
.map(item -> (Serializable) item.getId())
|
||||
.collect(Collectors.toList());
|
||||
.map(item -> (Serializable) item.getId())
|
||||
.collect(Collectors.toList());
|
||||
if (documentCollection.isFaqCollection()) {
|
||||
Map<String, FaqItem> faqItemMap = faqItemMapper.selectListByIds(ids).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(FaqItem::getId, ids);
|
||||
queryWrapper.eq(FaqItem::getCollectionId, documentCollection.getId());
|
||||
Map<String, FaqItem> faqItemMap = faqItemMapper.selectListByQuery(queryWrapper).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
searchDocuments.removeIf(item -> !faqItemMap.containsKey(String.valueOf(item.getId())));
|
||||
searchDocuments.forEach(item -> {
|
||||
FaqItem faqItem = faqItemMap.get(String.valueOf(item.getId()));
|
||||
if (faqItem != null) {
|
||||
List<Map<String, String>> faqImages = readFaqImages(faqItem);
|
||||
item.setContent(buildFaqPromptContent(faqItem, faqImages));
|
||||
|
||||
Map<String, Object> metadataMap = item.getMetadataMap() == null
|
||||
? new HashMap<>()
|
||||
: new HashMap<>(item.getMetadataMap());
|
||||
List<String> imageUrls = faqImages.stream()
|
||||
.map(image -> image.get("url"))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
metadataMap.put("imageUrls", imageUrls);
|
||||
item.setMetadataMap(metadataMap);
|
||||
if (faqItem == null) {
|
||||
return;
|
||||
}
|
||||
List<Map<String, String>> faqImages = readFaqImages(faqItem);
|
||||
item.setContent(buildFaqPromptContent(faqItem, faqImages));
|
||||
|
||||
Map<String, Object> metadataMap = item.getMetadataMap() == null
|
||||
? new HashMap<String, Object>()
|
||||
: new HashMap<String, Object>(item.getMetadataMap());
|
||||
List<String> imageUrls = faqImages.stream()
|
||||
.map(image -> image.get("url"))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
metadataMap.put("imageUrls", imageUrls);
|
||||
item.setMetadataMap(metadataMap);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, DocumentChunk> chunkMap = documentChunkMapper.selectListByIds(ids).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
QueryWrapper queryWrapper = QueryWrapper.create();
|
||||
queryWrapper.in(DocumentChunk::getId, ids);
|
||||
queryWrapper.eq(DocumentChunk::getDocumentCollectionId, documentCollection.getId());
|
||||
Map<String, DocumentChunk> chunkMap = documentChunkMapper.selectListByQuery(queryWrapper).stream()
|
||||
.collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a));
|
||||
searchDocuments.removeIf(item -> !chunkMap.containsKey(String.valueOf(item.getId())));
|
||||
searchDocuments.forEach(item -> {
|
||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(item.getId()));
|
||||
if (documentChunk != null && !StringUtil.noText(documentChunk.getContent())) {
|
||||
@@ -354,4 +449,43 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private int readIntegerOption(DocumentCollection documentCollection, String key, int defaultValue) {
|
||||
Object value = documentCollection.getOptionsByKey(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
if (value instanceof String && StringUtil.hasText((String) value)) {
|
||||
try {
|
||||
return Integer.parseInt((String) value);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private float readFloatOption(DocumentCollection documentCollection, String key, float defaultValue) {
|
||||
Object value = documentCollection.getOptionsByKey(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).floatValue();
|
||||
}
|
||||
if (value instanceof String && StringUtil.hasText((String) value)) {
|
||||
try {
|
||||
return Float.parseFloat((String) value);
|
||||
} catch (NumberFormatException ignore) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Double roundDouble(Double value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return new BigDecimal(String.valueOf(value))
|
||||
.setScale(4, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.easyagents.rag.ingestion.RagIngestionService;
|
||||
import com.easyagents.rag.ingestion.model.AnalysisResult;
|
||||
import com.easyagents.rag.ingestion.model.StrategyConfig;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.easyagents.search.engine.service.KeywordSearchMetadataKeys;
|
||||
import com.mybatisflex.core.keygen.impl.FlexIDKeyGenerator;
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
import com.mybatisflex.core.query.QueryMethods;
|
||||
@@ -56,7 +57,6 @@ import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_SEARCH_ENGINE_TYPE;
|
||||
import static tech.easyflow.ai.entity.table.DocumentChunkTableDef.DOCUMENT_CHUNK;
|
||||
import static tech.easyflow.ai.entity.table.DocumentTableDef.DOCUMENT;
|
||||
|
||||
@@ -157,8 +157,8 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
List<BigInteger> chunkIds = documentChunkMapper.selectListByQueryAs(queryWrapper, BigInteger.class);
|
||||
documentStore.delete(chunkIds, options);
|
||||
// 删除搜索引擎中的数据
|
||||
if (searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)) != null) {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (searcher != null) {
|
||||
chunkIds.forEach(searcher::deleteDocument);
|
||||
}
|
||||
int ck = documentChunkMapper.deleteByQuery(QueryWrapper.create().eq(DocumentChunk::getDocumentId, id));
|
||||
@@ -691,9 +691,7 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
options.setIndexName(options.getCollectionName());
|
||||
|
||||
DocumentSearcher searcher = null;
|
||||
if (knowledge.isSearchEngineEnabled()) {
|
||||
searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
}
|
||||
searcher = searcherFactory.getSearcher();
|
||||
return new StoreExecutionContext(knowledge, model, embeddingModel, documentStore, options, searcher);
|
||||
}
|
||||
|
||||
@@ -703,6 +701,8 @@ public class DocumentServiceImpl extends ServiceImpl<DocumentMapper, Document> i
|
||||
com.easyagents.core.document.Document document = new com.easyagents.core.document.Document();
|
||||
document.setId(item.getId());
|
||||
document.setContent(item.getContent());
|
||||
document.addMetadata(KeywordSearchMetadataKeys.KNOWLEDGE_ID,
|
||||
storeContext.knowledge.getId() == null ? null : storeContext.knowledge.getId().toString());
|
||||
documents.add(document);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.core.store.StoreResult;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.easyagents.search.engine.service.KeywordSearchMetadataKeys;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.jsoup.Jsoup;
|
||||
@@ -52,7 +53,6 @@ import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_SEARCH_ENGINE_TYPE;
|
||||
|
||||
@Service
|
||||
public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> implements FaqItemService {
|
||||
@@ -356,14 +356,12 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
throw new BusinessException("FAQ向量化失败");
|
||||
}
|
||||
|
||||
if (collection.isSearchEngineEnabled()) {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher((String) collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
if (searcher != null) {
|
||||
if (isUpdate) {
|
||||
searcher.deleteDocument(entity.getId());
|
||||
}
|
||||
searcher.addDocument(doc);
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (searcher != null) {
|
||||
if (isUpdate) {
|
||||
searcher.deleteDocument(entity.getId());
|
||||
}
|
||||
searcher.addDocument(doc);
|
||||
}
|
||||
markCollectionEmbedded(collection, preparedStore.embeddingModel);
|
||||
}
|
||||
@@ -375,14 +373,11 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
throw new BusinessException("FAQ向量删除失败");
|
||||
}
|
||||
|
||||
if (collection.isSearchEngineEnabled()) {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher((String) collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
if (searcher != null) {
|
||||
boolean removed = searcher.deleteDocument(entity.getId());
|
||||
if (!removed) {
|
||||
LOG.warn("Delete faq search index failed. faqId={}, searcherType={}",
|
||||
entity.getId(), collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE));
|
||||
}
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (searcher != null) {
|
||||
boolean removed = searcher.deleteDocument(entity.getId());
|
||||
if (!removed) {
|
||||
LOG.warn("Delete faq search index failed. faqId={}", entity.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,6 +438,7 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
metadata.put("answerText", entity.getAnswerText());
|
||||
metadata.put("categoryId", entity.getCategoryId());
|
||||
metadata.put("imageUrls", readImageUrls(entity.getOptions()));
|
||||
metadata.put(KeywordSearchMetadataKeys.KNOWLEDGE_ID, entity.getCollectionId() == null ? null : entity.getCollectionId().toString());
|
||||
doc.setMetadataMap(metadata);
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package tech.easyflow.auth.service;
|
||||
import tech.easyflow.auth.entity.LoginDTO;
|
||||
import tech.easyflow.auth.entity.LoginVO;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface AuthService {
|
||||
/**
|
||||
* 登录
|
||||
@@ -13,4 +15,14 @@ public interface AuthService {
|
||||
* 开发模式免登录
|
||||
*/
|
||||
LoginVO devLogin(String account);
|
||||
|
||||
/**
|
||||
* 通过访问令牌登录
|
||||
*/
|
||||
LoginVO loginByApiKey(String apiKey);
|
||||
|
||||
/**
|
||||
* 通过账号ID登录
|
||||
*/
|
||||
LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package tech.easyflow.auth.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.SaLoginModel;
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.auth.entity.LoginDTO;
|
||||
import tech.easyflow.auth.entity.LoginVO;
|
||||
import tech.easyflow.auth.service.AuthService;
|
||||
@@ -7,22 +14,19 @@ import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysApiKey;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysMenu;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysApiKeyService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysMenuService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.core.tenant.TenantManager;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -35,6 +39,8 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
||||
private SysRoleService sysRoleService;
|
||||
@Resource
|
||||
private SysMenuService sysMenuService;
|
||||
@Resource
|
||||
private SysApiKeyService sysApiKeyService;
|
||||
|
||||
@Override
|
||||
public LoginVO login(LoginDTO loginDTO) {
|
||||
@@ -63,6 +69,29 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginVO loginByApiKey(String apiKey) {
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
sysApiKeyService.checkApikeyPermission(apiKey, "/public-api/bot/chat");
|
||||
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apiKey);
|
||||
BigInteger accountId = sysApiKey.getCreatedBy();
|
||||
if (accountId == null) {
|
||||
throw new BusinessException("访问令牌未绑定创建用户");
|
||||
}
|
||||
Long timeoutSeconds = resolveApiKeyLoginTimeoutSeconds(sysApiKey);
|
||||
return loginByAccountId(accountId, timeoutSeconds);
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoginVO loginByAccountId(BigInteger accountId, Long timeoutSeconds) {
|
||||
SysAccount record = getAvailableAccount(accountId, "账号不存在或不可用");
|
||||
return createLoginVO(record, timeoutSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
||||
@@ -79,7 +108,17 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
||||
}
|
||||
|
||||
private LoginVO createLoginVO(SysAccount record) {
|
||||
StpUtil.login(record.getId());
|
||||
return createLoginVO(record, null);
|
||||
}
|
||||
|
||||
private LoginVO createLoginVO(SysAccount record, Long timeoutSeconds) {
|
||||
if (timeoutSeconds != null) {
|
||||
SaLoginModel loginModel = new SaLoginModel();
|
||||
loginModel.setTimeout(timeoutSeconds);
|
||||
StpUtil.login(record.getId(), loginModel);
|
||||
} else {
|
||||
StpUtil.login(record.getId());
|
||||
}
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
BeanUtil.copyProperties(record, loginAccount);
|
||||
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
||||
@@ -92,10 +131,32 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
||||
return res;
|
||||
}
|
||||
|
||||
private Long resolveApiKeyLoginTimeoutSeconds(SysApiKey sysApiKey) {
|
||||
Date expiredAt = sysApiKey.getExpiredAt();
|
||||
if (expiredAt == null) {
|
||||
return null;
|
||||
}
|
||||
long remainingMs = expiredAt.getTime() - System.currentTimeMillis();
|
||||
if (remainingMs <= 0) {
|
||||
throw new BusinessException("apiKey 已过期");
|
||||
}
|
||||
long timeoutSeconds = (remainingMs + 999) / 1000;
|
||||
return Math.max(timeoutSeconds, 1L);
|
||||
}
|
||||
|
||||
private SysAccount getAvailableAccount(String account, String accountNotFoundMessage) {
|
||||
QueryWrapper w = QueryWrapper.create();
|
||||
w.eq(SysAccount::getLoginName, account);
|
||||
SysAccount record = sysAccountService.getOne(w);
|
||||
return validateAvailableAccount(record, accountNotFoundMessage);
|
||||
}
|
||||
|
||||
private SysAccount getAvailableAccount(BigInteger accountId, String accountNotFoundMessage) {
|
||||
SysAccount record = sysAccountService.getById(accountId);
|
||||
return validateAvailableAccount(record, accountNotFoundMessage);
|
||||
}
|
||||
|
||||
private SysAccount validateAvailableAccount(SysAccount record, String accountNotFoundMessage) {
|
||||
if (record == null) {
|
||||
throw new BusinessException(accountNotFoundMessage);
|
||||
}
|
||||
|
||||
56
easyflow-modules/easyflow-module-chatlog/pom.xml
Normal file
56
easyflow-modules/easyflow-module-chatlog/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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-chatlog</name>
|
||||
<artifactId>easyflow-module-chatlog</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-base</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>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,432 @@
|
||||
package tech.easyflow.chatlog.cache;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.chatlog.config.ChatCacheProperties;
|
||||
import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand;
|
||||
import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class ChatHotStateService {
|
||||
|
||||
private static final TypeReference<List<ChatMessageRecord>> TAIL_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ChatCacheProperties properties;
|
||||
|
||||
public ChatHotStateService(StringRedisTemplate stringRedisTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
ChatCacheProperties properties) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public ChatSessionSummary touchSession(ChatSessionUpsertCommand command) {
|
||||
ChatSessionSummary summary = getSessionSummary(command.getSessionId());
|
||||
if (summary == null) {
|
||||
summary = new ChatSessionSummary();
|
||||
summary.setId(command.getSessionId());
|
||||
summary.setTenantId(command.getTenantId());
|
||||
summary.setDeptId(command.getDeptId());
|
||||
summary.setUserId(command.getUserId());
|
||||
summary.setCreated(command.getOperateAt());
|
||||
summary.setCreatedBy(command.getOperatorId());
|
||||
summary.setMessageCount(0);
|
||||
summary.setLastMessagePreview("");
|
||||
}
|
||||
summary.setUserAccount(command.getUserAccount());
|
||||
summary.setAssistantId(command.getAssistantId());
|
||||
summary.setAssistantCode(command.getAssistantCode());
|
||||
summary.setAssistantName(command.getAssistantName());
|
||||
if (command.getTitle() != null && !command.getTitle().isBlank()) {
|
||||
summary.setTitle(command.getTitle());
|
||||
}
|
||||
summary.setAccessAt(defaultDate(command.getOperateAt()));
|
||||
summary.setModified(defaultDate(command.getOperateAt()));
|
||||
summary.setModifiedBy(command.getOperatorId());
|
||||
summary.setIsDeleted(0);
|
||||
cacheSessionSummaryStrict(summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId, Date operateAt) {
|
||||
ChatSessionSummary summary = getSessionSummary(sessionId);
|
||||
if (summary == null) {
|
||||
summary = new ChatSessionSummary();
|
||||
summary.setId(sessionId);
|
||||
summary.setUserId(userId);
|
||||
summary.setCreated(defaultDate(operateAt));
|
||||
summary.setCreatedBy(operatorId);
|
||||
summary.setMessageCount(0);
|
||||
}
|
||||
summary.setTitle(title);
|
||||
summary.setModified(defaultDate(operateAt));
|
||||
summary.setModifiedBy(operatorId);
|
||||
summary.setIsDeleted(0);
|
||||
cacheSessionSummaryStrict(summary);
|
||||
}
|
||||
|
||||
public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId, Date operateAt) {
|
||||
ChatSessionSummary summary = getSessionSummary(sessionId);
|
||||
if (summary != null) {
|
||||
summary.setIsDeleted(1);
|
||||
summary.setModified(defaultDate(operateAt));
|
||||
summary.setModifiedBy(operatorId);
|
||||
writeValueStrict(keySessionSummary(sessionId), summary, properties.getSessionSummaryTtl());
|
||||
removeFromSessionIndexStrict(userId, sessionId);
|
||||
} else {
|
||||
removeFromSessionIndexStrict(userId, sessionId);
|
||||
}
|
||||
deleteStrict(keySessionTail(sessionId));
|
||||
}
|
||||
|
||||
public void appendMessage(ChatAppendMessageCommand command) {
|
||||
ChatSessionSummary summary = getSessionSummary(command.getSessionId());
|
||||
if (summary == null) {
|
||||
summary = new ChatSessionSummary();
|
||||
summary.setId(command.getSessionId());
|
||||
summary.setUserId(command.getUserId());
|
||||
summary.setAssistantId(command.getAssistantId());
|
||||
summary.setCreated(defaultDate(command.getCreated()));
|
||||
summary.setCreatedBy(command.getCreatedBy());
|
||||
summary.setMessageCount(0);
|
||||
}
|
||||
summary.setLastSenderId(command.getSenderId());
|
||||
summary.setLastSenderName(command.getSenderName());
|
||||
summary.setLastMessagePreview(trimPreview(command.getContentText()));
|
||||
summary.setLastMessageAt(defaultDate(command.getCreated()));
|
||||
summary.setAccessAt(defaultDate(command.getCreated()));
|
||||
summary.setModified(defaultDate(command.getCreated()));
|
||||
summary.setModifiedBy(command.getCreatedBy());
|
||||
summary.setIsDeleted(0);
|
||||
summary.setMessageCount((summary.getMessageCount() == null ? 0 : summary.getMessageCount()) + 1);
|
||||
cacheSessionSummaryStrict(summary);
|
||||
appendTail(toMessageRecord(command));
|
||||
}
|
||||
|
||||
public List<BigInteger> listSessionIds(BigInteger userId, long offset, long limit) {
|
||||
if (userId == null || limit <= 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
Set<String> values = stringRedisTemplate.opsForZSet()
|
||||
.reverseRange(keySessionIndex(userId), offset, offset + limit - 1);
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BigInteger> sessionIds = new ArrayList<>(values.size());
|
||||
for (String value : values) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
sessionIds.add(new BigInteger(value));
|
||||
}
|
||||
}
|
||||
return sessionIds;
|
||||
} catch (DataAccessException ignored) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public long countSessions(BigInteger userId) {
|
||||
if (userId == null) {
|
||||
return 0L;
|
||||
}
|
||||
try {
|
||||
Long count = stringRedisTemplate.opsForZSet().zCard(keySessionIndex(userId));
|
||||
return count == null ? 0L : count;
|
||||
} catch (DataAccessException ignored) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSessionIndex(BigInteger userId) {
|
||||
if (userId == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Boolean exists = stringRedisTemplate.hasKey(keySessionIndex(userId));
|
||||
return Boolean.TRUE.equals(exists);
|
||||
} catch (DataAccessException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ChatSessionSummary getSessionSummary(BigInteger sessionId) {
|
||||
return readValue(keySessionSummary(sessionId), ChatSessionSummary.class);
|
||||
}
|
||||
|
||||
public List<ChatSessionSummary> getSessionSummaries(List<BigInteger> sessionIds) {
|
||||
if (sessionIds == null || sessionIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
List<String> keys = sessionIds.stream().map(this::keySessionSummary).toList();
|
||||
List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<BigInteger, ChatSessionSummary> summaryMap = new LinkedHashMap<>();
|
||||
for (String value : values) {
|
||||
if (value == null || value.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
ChatSessionSummary summary = objectMapper.readValue(value, ChatSessionSummary.class);
|
||||
if (summary != null && summary.getId() != null) {
|
||||
summaryMap.put(summary.getId(), summary);
|
||||
}
|
||||
}
|
||||
List<ChatSessionSummary> results = new ArrayList<>(sessionIds.size());
|
||||
for (BigInteger sessionId : sessionIds) {
|
||||
ChatSessionSummary summary = summaryMap.get(sessionId);
|
||||
if (summary != null) {
|
||||
results.add(summary);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (IOException | DataAccessException ignored) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public void cacheSessionSummary(ChatSessionSummary summary) {
|
||||
try {
|
||||
cacheSessionSummaryStrict(summary);
|
||||
} catch (IllegalStateException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void cacheSessionSummaryStrict(ChatSessionSummary summary) {
|
||||
if (summary == null || summary.getId() == null) {
|
||||
return;
|
||||
}
|
||||
writeValueStrict(keySessionSummary(summary.getId()), summary, properties.getSessionSummaryTtl());
|
||||
BigInteger userId = summary.getUserId();
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
if (Integer.valueOf(1).equals(summary.getIsDeleted())) {
|
||||
removeFromSessionIndexStrict(userId, summary.getId());
|
||||
return;
|
||||
}
|
||||
Date scoreTime = summary.getAccessAt() != null ? summary.getAccessAt() : summary.getModified();
|
||||
double score = scoreTime == null ? System.currentTimeMillis() : scoreTime.getTime();
|
||||
try {
|
||||
stringRedisTemplate.opsForZSet().add(keySessionIndex(userId), summary.getId().toString(), score);
|
||||
} catch (DataAccessException ex) {
|
||||
throw new IllegalStateException("写入聊天会话索引失败", ex);
|
||||
}
|
||||
expireStrict(keySessionIndex(userId), properties.getSessionListTtl());
|
||||
}
|
||||
|
||||
public void cacheSessionSummaries(List<ChatSessionSummary> sessions) {
|
||||
if (sessions == null || sessions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<BigInteger> dedup = new LinkedHashSet<>();
|
||||
for (ChatSessionSummary session : sessions) {
|
||||
if (session == null || session.getId() == null || !dedup.add(session.getId())) {
|
||||
continue;
|
||||
}
|
||||
cacheSessionSummary(session);
|
||||
}
|
||||
}
|
||||
|
||||
public void evictSessionSummary(BigInteger sessionId) {
|
||||
delete(keySessionSummary(sessionId));
|
||||
}
|
||||
|
||||
public List<ChatMessageRecord> getSessionTail(BigInteger sessionId) {
|
||||
return readList(keySessionTail(sessionId), TAIL_TYPE);
|
||||
}
|
||||
|
||||
public void setSessionTail(BigInteger sessionId, List<ChatMessageRecord> records) {
|
||||
writeValue(keySessionTail(sessionId), trimTail(records), properties.getSessionTailTtl());
|
||||
}
|
||||
|
||||
public void appendTail(ChatMessageRecord record) {
|
||||
if (record == null || record.getSessionId() == null) {
|
||||
return;
|
||||
}
|
||||
List<ChatMessageRecord> current = getSessionTail(record.getSessionId());
|
||||
List<ChatMessageRecord> updated = new ArrayList<>();
|
||||
updated.add(record);
|
||||
if (current != null) {
|
||||
updated.addAll(current);
|
||||
}
|
||||
writeValueStrict(keySessionTail(record.getSessionId()), trimTail(updated), properties.getSessionTailTtl());
|
||||
}
|
||||
|
||||
public void evictSessionTail(BigInteger sessionId) {
|
||||
delete(keySessionTail(sessionId));
|
||||
}
|
||||
|
||||
public int tailSize() {
|
||||
return properties.getTailSize();
|
||||
}
|
||||
|
||||
private ChatMessageRecord toMessageRecord(ChatAppendMessageCommand command) {
|
||||
ChatMessageRecord record = new ChatMessageRecord();
|
||||
record.setId(command.getMessageId());
|
||||
record.setSessionId(command.getSessionId());
|
||||
record.setUserId(command.getUserId());
|
||||
record.setAssistantId(command.getAssistantId());
|
||||
record.setSenderId(command.getSenderId());
|
||||
record.setSenderName(command.getSenderName());
|
||||
record.setSenderRole(command.getSenderRole());
|
||||
record.setContentType(command.getContentType());
|
||||
record.setContentText(command.getContentText());
|
||||
record.setContentPayload(command.getContentPayload());
|
||||
record.setCreated(defaultDate(command.getCreated()));
|
||||
record.setCreatedBy(command.getCreatedBy());
|
||||
record.setSyncVersion(record.getCreated().getTime());
|
||||
return record;
|
||||
}
|
||||
|
||||
private List<ChatMessageRecord> trimTail(List<ChatMessageRecord> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
int maxSize = Math.max(properties.getTailSize(), 1);
|
||||
int size = Math.min(records.size(), maxSize);
|
||||
List<ChatMessageRecord> result = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
result.add(records.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String keySessionIndex(BigInteger userId) {
|
||||
return "chat:session:index:" + userId;
|
||||
}
|
||||
|
||||
private String keySessionSummary(BigInteger sessionId) {
|
||||
return "chat:session:summary:" + sessionId;
|
||||
}
|
||||
|
||||
private String keySessionTail(BigInteger sessionId) {
|
||||
return "chat:session:tail:" + sessionId;
|
||||
}
|
||||
|
||||
private void removeFromSessionIndex(BigInteger userId, BigInteger sessionId) {
|
||||
if (userId == null || sessionId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString());
|
||||
} catch (DataAccessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFromSessionIndexStrict(BigInteger userId, BigInteger sessionId) {
|
||||
if (userId == null || sessionId == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString());
|
||||
} catch (DataAccessException ex) {
|
||||
throw new IllegalStateException("移除聊天会话索引失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void expire(String key, Duration ttl) {
|
||||
try {
|
||||
stringRedisTemplate.expire(key, ttl);
|
||||
} catch (DataAccessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void expireStrict(String key, Duration ttl) {
|
||||
try {
|
||||
Boolean success = stringRedisTemplate.expire(key, ttl);
|
||||
if (Boolean.FALSE.equals(success)) {
|
||||
throw new IllegalStateException("设置聊天缓存过期时间失败");
|
||||
}
|
||||
} catch (DataAccessException ex) {
|
||||
throw new IllegalStateException("设置聊天缓存过期时间失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void delete(String key) {
|
||||
try {
|
||||
stringRedisTemplate.delete(key);
|
||||
} catch (DataAccessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStrict(String key) {
|
||||
try {
|
||||
stringRedisTemplate.delete(key);
|
||||
} catch (DataAccessException ex) {
|
||||
throw new IllegalStateException("删除聊天缓存失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeValue(String key, Object value, Duration ttl) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl);
|
||||
} catch (IOException | DataAccessException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeValueStrict(String key, Object value, Duration ttl) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl);
|
||||
} catch (IOException | DataAccessException ex) {
|
||||
throw new IllegalStateException("写入聊天缓存失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T readValue(String key, Class<T> clazz) {
|
||||
try {
|
||||
String value = stringRedisTemplate.opsForValue().get(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.readValue(value, clazz);
|
||||
} catch (IOException | DataAccessException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T readList(String key, TypeReference<T> typeReference) {
|
||||
try {
|
||||
String value = stringRedisTemplate.opsForValue().get(key);
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return objectMapper.readValue(value, typeReference);
|
||||
} catch (IOException | DataAccessException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String trimPreview(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.length() <= 200 ? text : text.substring(0, 200);
|
||||
}
|
||||
|
||||
private Date defaultDate(Date value) {
|
||||
return value == null ? new Date() : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package tech.easyflow.chatlog.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "easyflow.chat.cache")
|
||||
public class ChatCacheProperties {
|
||||
|
||||
private Duration sessionListTtl = Duration.ofMinutes(5);
|
||||
private Duration sessionSummaryTtl = Duration.ofMinutes(10);
|
||||
private Duration sessionTailTtl = Duration.ofMinutes(30);
|
||||
private int tailSize = 50;
|
||||
|
||||
public Duration getSessionListTtl() {
|
||||
return sessionListTtl;
|
||||
}
|
||||
|
||||
public void setSessionListTtl(Duration sessionListTtl) {
|
||||
this.sessionListTtl = sessionListTtl;
|
||||
}
|
||||
|
||||
public Duration getSessionSummaryTtl() {
|
||||
return sessionSummaryTtl;
|
||||
}
|
||||
|
||||
public void setSessionSummaryTtl(Duration sessionSummaryTtl) {
|
||||
this.sessionSummaryTtl = sessionSummaryTtl;
|
||||
}
|
||||
|
||||
public Duration getSessionTailTtl() {
|
||||
return sessionTailTtl;
|
||||
}
|
||||
|
||||
public void setSessionTailTtl(Duration sessionTailTtl) {
|
||||
this.sessionTailTtl = sessionTailTtl;
|
||||
}
|
||||
|
||||
public int getTailSize() {
|
||||
return tailSize;
|
||||
}
|
||||
|
||||
public void setTailSize(int tailSize) {
|
||||
this.tailSize = tailSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.easyflow.chatlog.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "easyflow.chat.sync")
|
||||
public class ChatSyncProperties {
|
||||
|
||||
private boolean enabled = false;
|
||||
private int batchSize = 500;
|
||||
private long fixedDelay = 30000L;
|
||||
private int repairLookbackDays = 3;
|
||||
private int retentionMonths = 3;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getBatchSize() {
|
||||
return batchSize;
|
||||
}
|
||||
|
||||
public void setBatchSize(int batchSize) {
|
||||
this.batchSize = batchSize;
|
||||
}
|
||||
|
||||
public long getFixedDelay() {
|
||||
return fixedDelay;
|
||||
}
|
||||
|
||||
public void setFixedDelay(long fixedDelay) {
|
||||
this.fixedDelay = fixedDelay;
|
||||
}
|
||||
|
||||
public int getRepairLookbackDays() {
|
||||
return repairLookbackDays;
|
||||
}
|
||||
|
||||
public void setRepairLookbackDays(int repairLookbackDays) {
|
||||
this.repairLookbackDays = repairLookbackDays;
|
||||
}
|
||||
|
||||
public int getRetentionMonths() {
|
||||
return retentionMonths;
|
||||
}
|
||||
|
||||
public void setRetentionMonths(int retentionMonths) {
|
||||
this.retentionMonths = retentionMonths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package tech.easyflow.chatlog.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
|
||||
@AutoConfiguration
|
||||
@MapperScan("tech.easyflow.chatlog.mapper")
|
||||
public class ChatlogModuleConfig {
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package tech.easyflow.chatlog.domain.command;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatAppendMessageCommand implements Serializable {
|
||||
|
||||
private BigInteger messageId;
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger sessionId;
|
||||
private BigInteger userId;
|
||||
private BigInteger assistantId;
|
||||
private BigInteger senderId;
|
||||
private String senderName;
|
||||
private String senderRole;
|
||||
private String contentType;
|
||||
private String contentText;
|
||||
private Map<String, Object> contentPayload;
|
||||
private BigInteger createdBy;
|
||||
private Date created = new Date();
|
||||
|
||||
public BigInteger getMessageId() {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
public void setMessageId(BigInteger messageId) {
|
||||
this.messageId = messageId;
|
||||
}
|
||||
|
||||
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 BigInteger getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(BigInteger sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public BigInteger getAssistantId() {
|
||||
return assistantId;
|
||||
}
|
||||
|
||||
public void setAssistantId(BigInteger assistantId) {
|
||||
this.assistantId = assistantId;
|
||||
}
|
||||
|
||||
public BigInteger getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public void setSenderId(BigInteger senderId) {
|
||||
this.senderId = senderId;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
public void setSenderName(String senderName) {
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public String getSenderRole() {
|
||||
return senderRole;
|
||||
}
|
||||
|
||||
public void setSenderRole(String senderRole) {
|
||||
this.senderRole = senderRole;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentText() {
|
||||
return contentText;
|
||||
}
|
||||
|
||||
public void setContentText(String contentText) {
|
||||
this.contentText = contentText;
|
||||
}
|
||||
|
||||
public Map<String, Object> getContentPayload() {
|
||||
return contentPayload;
|
||||
}
|
||||
|
||||
public void setContentPayload(Map<String, Object> contentPayload) {
|
||||
this.contentPayload = contentPayload;
|
||||
}
|
||||
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package tech.easyflow.chatlog.domain.command;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
public class ChatSessionSummaryCommand implements Serializable {
|
||||
|
||||
private BigInteger sessionId;
|
||||
private BigInteger userId;
|
||||
private BigInteger lastSenderId;
|
||||
private String lastSenderName;
|
||||
private String lastMessagePreview;
|
||||
private Date lastMessageAt = new Date();
|
||||
private BigInteger operatorId;
|
||||
private int messageIncrement = 1;
|
||||
|
||||
public BigInteger getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(BigInteger sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public BigInteger getLastSenderId() {
|
||||
return lastSenderId;
|
||||
}
|
||||
|
||||
public void setLastSenderId(BigInteger lastSenderId) {
|
||||
this.lastSenderId = lastSenderId;
|
||||
}
|
||||
|
||||
public String getLastSenderName() {
|
||||
return lastSenderName;
|
||||
}
|
||||
|
||||
public void setLastSenderName(String lastSenderName) {
|
||||
this.lastSenderName = lastSenderName;
|
||||
}
|
||||
|
||||
public String getLastMessagePreview() {
|
||||
return lastMessagePreview;
|
||||
}
|
||||
|
||||
public void setLastMessagePreview(String lastMessagePreview) {
|
||||
this.lastMessagePreview = lastMessagePreview;
|
||||
}
|
||||
|
||||
public Date getLastMessageAt() {
|
||||
return lastMessageAt;
|
||||
}
|
||||
|
||||
public void setLastMessageAt(Date lastMessageAt) {
|
||||
this.lastMessageAt = lastMessageAt;
|
||||
}
|
||||
|
||||
public BigInteger getOperatorId() {
|
||||
return operatorId;
|
||||
}
|
||||
|
||||
public void setOperatorId(BigInteger operatorId) {
|
||||
this.operatorId = operatorId;
|
||||
}
|
||||
|
||||
public int getMessageIncrement() {
|
||||
return messageIncrement;
|
||||
}
|
||||
|
||||
public void setMessageIncrement(int messageIncrement) {
|
||||
this.messageIncrement = Math.max(messageIncrement, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package tech.easyflow.chatlog.domain.command;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
public class ChatSessionUpsertCommand implements Serializable {
|
||||
|
||||
private BigInteger sessionId;
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger userId;
|
||||
private String userAccount;
|
||||
private BigInteger assistantId;
|
||||
private String assistantCode;
|
||||
private String assistantName;
|
||||
private String title;
|
||||
private BigInteger operatorId;
|
||||
private Date operateAt = new Date();
|
||||
|
||||
public BigInteger getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(BigInteger sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
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 BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserAccount() {
|
||||
return userAccount;
|
||||
}
|
||||
|
||||
public void setUserAccount(String userAccount) {
|
||||
this.userAccount = userAccount;
|
||||
}
|
||||
|
||||
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 BigInteger getOperatorId() {
|
||||
return operatorId;
|
||||
}
|
||||
|
||||
public void setOperatorId(BigInteger operatorId) {
|
||||
this.operatorId = operatorId;
|
||||
}
|
||||
|
||||
public Date getOperateAt() {
|
||||
return operateAt;
|
||||
}
|
||||
|
||||
public void setOperateAt(Date operateAt) {
|
||||
this.operateAt = operateAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatHistoryPage implements Serializable {
|
||||
|
||||
private long total;
|
||||
private long pageNumber;
|
||||
private long pageSize;
|
||||
private List<ChatMessageRecord> 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<ChatMessageRecord> getRecords() {
|
||||
return records;
|
||||
}
|
||||
|
||||
public void setRecords(List<ChatMessageRecord> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatMessageRecord implements Serializable {
|
||||
|
||||
private BigInteger id;
|
||||
private BigInteger sessionId;
|
||||
private BigInteger userId;
|
||||
private BigInteger assistantId;
|
||||
private BigInteger senderId;
|
||||
private String senderName;
|
||||
private String senderRole;
|
||||
private String contentType;
|
||||
private String contentText;
|
||||
private Map<String, Object> contentPayload;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Long syncVersion;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigInteger getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(BigInteger sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public BigInteger getAssistantId() {
|
||||
return assistantId;
|
||||
}
|
||||
|
||||
public void setAssistantId(BigInteger assistantId) {
|
||||
this.assistantId = assistantId;
|
||||
}
|
||||
|
||||
public BigInteger getSenderId() {
|
||||
return senderId;
|
||||
}
|
||||
|
||||
public void setSenderId(BigInteger senderId) {
|
||||
this.senderId = senderId;
|
||||
}
|
||||
|
||||
public String getSenderName() {
|
||||
return senderName;
|
||||
}
|
||||
|
||||
public void setSenderName(String senderName) {
|
||||
this.senderName = senderName;
|
||||
}
|
||||
|
||||
public String getSenderRole() {
|
||||
return senderRole;
|
||||
}
|
||||
|
||||
public void setSenderRole(String senderRole) {
|
||||
this.senderRole = senderRole;
|
||||
}
|
||||
|
||||
public String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public void setContentType(String contentType) {
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public String getContentText() {
|
||||
return contentText;
|
||||
}
|
||||
|
||||
public void setContentText(String contentText) {
|
||||
this.contentText = contentText;
|
||||
}
|
||||
|
||||
public Map<String, Object> getContentPayload() {
|
||||
return contentPayload;
|
||||
}
|
||||
|
||||
public void setContentPayload(Map<String, Object> contentPayload) {
|
||||
this.contentPayload = contentPayload;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Long getSyncVersion() {
|
||||
return syncVersion;
|
||||
}
|
||||
|
||||
public void setSyncVersion(Long syncVersion) {
|
||||
this.syncVersion = syncVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatSessionPage implements Serializable {
|
||||
|
||||
private long total;
|
||||
private long pageNumber;
|
||||
private long pageSize;
|
||||
private List<ChatSessionSummary> 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<ChatSessionSummary> getRecords() {
|
||||
return records;
|
||||
}
|
||||
|
||||
public void setRecords(List<ChatSessionSummary> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
public class ChatSessionSummary implements Serializable {
|
||||
|
||||
private BigInteger id;
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private BigInteger userId;
|
||||
private String userAccount;
|
||||
private BigInteger assistantId;
|
||||
private String assistantCode;
|
||||
private String assistantName;
|
||||
private String title;
|
||||
private String lastMessagePreview;
|
||||
private BigInteger lastSenderId;
|
||||
private String lastSenderName;
|
||||
private Integer messageCount;
|
||||
private Date accessAt;
|
||||
private Date lastMessageAt;
|
||||
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 getDeptId() {
|
||||
return deptId;
|
||||
}
|
||||
|
||||
public void setDeptId(BigInteger deptId) {
|
||||
this.deptId = deptId;
|
||||
}
|
||||
|
||||
public BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserAccount() {
|
||||
return userAccount;
|
||||
}
|
||||
|
||||
public void setUserAccount(String userAccount) {
|
||||
this.userAccount = userAccount;
|
||||
}
|
||||
|
||||
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 BigInteger getLastSenderId() {
|
||||
return lastSenderId;
|
||||
}
|
||||
|
||||
public void setLastSenderId(BigInteger lastSenderId) {
|
||||
this.lastSenderId = lastSenderId;
|
||||
}
|
||||
|
||||
public String getLastSenderName() {
|
||||
return lastSenderName;
|
||||
}
|
||||
|
||||
public void setLastSenderName(String lastSenderName) {
|
||||
this.lastSenderName = lastSenderName;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
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,36 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ChatSyncResult implements Serializable {
|
||||
|
||||
private String syncCode;
|
||||
private int syncedRows;
|
||||
private List<String> touchedDates = new ArrayList<>();
|
||||
|
||||
public String getSyncCode() {
|
||||
return syncCode;
|
||||
}
|
||||
|
||||
public void setSyncCode(String syncCode) {
|
||||
this.syncCode = syncCode;
|
||||
}
|
||||
|
||||
public int getSyncedRows() {
|
||||
return syncedRows;
|
||||
}
|
||||
|
||||
public void setSyncedRows(int syncedRows) {
|
||||
this.syncedRows = syncedRows;
|
||||
}
|
||||
|
||||
public List<String> getTouchedDates() {
|
||||
return touchedDates;
|
||||
}
|
||||
|
||||
public void setTouchedDates(List<String> touchedDates) {
|
||||
this.touchedDates = touchedDates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PublicChatSessionRestoreResult implements Serializable {
|
||||
|
||||
private boolean sessionExists;
|
||||
private String conversationId;
|
||||
private ChatSessionSummary session;
|
||||
private List<ChatMessageRecord> messages = new ArrayList<>();
|
||||
|
||||
public boolean isSessionExists() {
|
||||
return sessionExists;
|
||||
}
|
||||
|
||||
public void setSessionExists(boolean sessionExists) {
|
||||
this.sessionExists = sessionExists;
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
|
||||
public ChatSessionSummary getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public void setSession(ChatSessionSummary session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public List<ChatMessageRecord> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
|
||||
public void setMessages(List<ChatMessageRecord> messages) {
|
||||
this.messages = messages == null ? new ArrayList<>() : messages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.chatlog.domain.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.chatlog.domain.entity.base.ChatPersistDeadLetterBase;
|
||||
|
||||
@Table(value = "chat_persist_dead_letter", comment = "聊天持久化死信")
|
||||
public class ChatPersistDeadLetter extends ChatPersistDeadLetterBase {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.chatlog.domain.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.chatlog.domain.entity.base.ChatSyncCheckpointBase;
|
||||
|
||||
@Table(value = "chat_sync_checkpoint", comment = "聊天同步检查点")
|
||||
public class ChatSyncCheckpoint extends ChatSyncCheckpointBase {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user