feat: 接入聊天历史界面与外链会话恢复

- 新增管理端与用户端聊天历史接口和页面

- 外链聊天支持访问令牌登录、身份保活与当前会话恢复

- 聊天执行链路切到统一 runtime 与 chatlog 查询接口
This commit is contained in:
2026-04-05 11:37:25 +08:00
parent 25e80433a5
commit a4f75a5e4c
48 changed files with 3724 additions and 972 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();