feat: 接入聊天历史界面与外链会话恢复
- 新增管理端与用户端聊天历史接口和页面 - 外链聊天支持访问令牌登录、身份保活与当前会话恢复 - 聊天执行链路切到统一 runtime 与 chatlog 查询接口
This commit is contained in:
@@ -16,6 +16,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-auth</artifactId>
|
<artifactId>easyflow-module-auth</artifactId>
|
||||||
@@ -29,4 +33,4 @@
|
|||||||
<artifactId>easyflow-common-captcha</artifactId>
|
<artifactId>easyflow-common-captcha</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ import tech.easyflow.ai.service.*;
|
|||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
@@ -158,7 +162,15 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
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")
|
@PostMapping("updateLlmId")
|
||||||
@@ -319,6 +331,24 @@ public class BotController extends BaseCurdController<BotService, Bot> {
|
|||||||
return result;
|
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
|
@Override
|
||||||
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
protected Result<?> onRemoveBefore(Collection<Serializable> ids) {
|
||||||
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
QueryWrapper queryWrapperKnowledge = QueryWrapper.create().in(BotDocumentCollection::getBotId, ids);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
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.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.LoginDTO;
|
||||||
import tech.easyflow.auth.entity.LoginVO;
|
import tech.easyflow.auth.entity.LoginVO;
|
||||||
import tech.easyflow.auth.service.AuthService;
|
import tech.easyflow.auth.service.AuthService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
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 javax.annotation.Resource;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -27,6 +28,12 @@ public class AuthController {
|
|||||||
return Result.ok(res);
|
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")
|
@PostMapping("logout")
|
||||||
public Result<Void> logout() {
|
public Result<Void> logout() {
|
||||||
StpUtil.logout();
|
StpUtil.logout();
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import tech.easyflow.ai.service.BotService;
|
|||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
import tech.easyflow.system.entity.SysApiKey;
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
import tech.easyflow.system.service.SysApiKeyService;
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* bot 接口
|
* bot 接口
|
||||||
@@ -51,6 +54,7 @@ public class PublicBotController {
|
|||||||
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
return ChatSseUtil.sendSystemError(null, "Apikey不能为空!");
|
||||||
}
|
}
|
||||||
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
sysApiKeyService.checkApikeyPermission(apikey, requestURI);
|
||||||
|
SysApiKey sysApiKey = sysApiKeyService.getSysApiKey(apikey);
|
||||||
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
BotServiceImpl.ChatCheckResult chatCheckResult = new BotServiceImpl.ChatCheckResult();
|
||||||
int size = chatRequestParams.getMessages().size();
|
int size = chatRequestParams.getMessages().size();
|
||||||
String prompt = null;
|
String prompt = null;
|
||||||
@@ -62,7 +66,30 @@ public class PublicBotController {
|
|||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
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>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-captcha</artifactId>
|
<artifactId>easyflow-common-captcha</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -23,10 +23,13 @@ import tech.easyflow.ai.service.impl.BotServiceImpl;
|
|||||||
import tech.easyflow.common.annotation.UsePermission;
|
import tech.easyflow.common.annotation.UsePermission;
|
||||||
import tech.easyflow.common.audio.core.AudioServiceManager;
|
import tech.easyflow.common.audio.core.AudioServiceManager;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
|
import tech.easyflow.core.runtime.ChatChannel;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
|
||||||
import tech.easyflow.system.service.CategoryPermissionService;
|
import tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
@@ -75,8 +78,6 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
@Resource
|
@Resource
|
||||||
private BotPluginService botPluginService;
|
private BotPluginService botPluginService;
|
||||||
@Resource
|
@Resource
|
||||||
private BotConversationService conversationMessageService;
|
|
||||||
@Resource
|
|
||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
|
||||||
@GetMapping("/generateConversationId")
|
@GetMapping("/generateConversationId")
|
||||||
@@ -161,27 +162,16 @@ public class UcBotController extends BaseCurdController<BotService, Bot> {
|
|||||||
if (errorEmitter != null) {
|
if (errorEmitter != null) {
|
||||||
return errorEmitter;
|
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);
|
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() {
|
private Map<String, Object> getDefaultLlmOptions() {
|
||||||
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
Map<String, Object> defaultLlmOptions = new HashMap<>();
|
||||||
defaultLlmOptions.put("temperature", 0.7);
|
defaultLlmOptions.put("temperature", 0.7);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,12 @@ import tech.easyflow.core.chat.protocol.ChatType;
|
|||||||
import tech.easyflow.core.chat.protocol.MessageRole;
|
import tech.easyflow.core.chat.protocol.MessageRole;
|
||||||
import tech.easyflow.core.chat.protocol.payload.ErrorPayload;
|
import tech.easyflow.core.chat.protocol.payload.ErrorPayload;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
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.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -33,6 +38,9 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
private final MemoryPrompt memoryPrompt;
|
private final MemoryPrompt memoryPrompt;
|
||||||
private final ChatSseEmitter sseEmitter;
|
private final ChatSseEmitter sseEmitter;
|
||||||
private final ChatOptions chatOptions;
|
private final ChatOptions chatOptions;
|
||||||
|
private final ChatRuntimeManager chatRuntimeManager;
|
||||||
|
private final ChatRuntimeContext runtimeContext;
|
||||||
|
private final ChatAssistantAccumulator assistantAccumulator;
|
||||||
// 核心标记:是否允许执行onStop业务逻辑(仅最后一次无后续工具调用时为true)
|
// 核心标记:是否允许执行onStop业务逻辑(仅最后一次无后续工具调用时为true)
|
||||||
private boolean canStop = true;
|
private boolean canStop = true;
|
||||||
// 辅助标记:是否进入过工具调用(避免重复递归判断)
|
// 辅助标记:是否进入过工具调用(避免重复递归判断)
|
||||||
@@ -40,12 +48,17 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
// 流式响应只能结束一次,避免重复发送导致 IllegalStateException
|
// 流式响应只能结束一次,避免重复发送导致 IllegalStateException
|
||||||
private final AtomicBoolean completed = new AtomicBoolean(false);
|
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.conversationId = conversationId;
|
||||||
this.chatModel = chatModel;
|
this.chatModel = chatModel;
|
||||||
this.memoryPrompt = memoryPrompt;
|
this.memoryPrompt = memoryPrompt;
|
||||||
this.sseEmitter = sseEmitter;
|
this.sseEmitter = sseEmitter;
|
||||||
this.chatOptions = chatOptions;
|
this.chatOptions = chatOptions;
|
||||||
|
this.chatRuntimeManager = chatRuntimeManager;
|
||||||
|
this.runtimeContext = runtimeContext;
|
||||||
|
this.assistantAccumulator = assistantAccumulator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -70,6 +83,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
List<ToolCall> toolCalls = aiMessage.getToolCalls();
|
List<ToolCall> toolCalls = aiMessage.getToolCalls();
|
||||||
if (toolCalls != null) {
|
if (toolCalls != null) {
|
||||||
for (ToolCall toolCall : toolCalls) {
|
for (ToolCall toolCall : toolCalls) {
|
||||||
|
assistantAccumulator.appendToolCall(toolCall.getId(), toolCall.getName(), toolCall.getArguments());
|
||||||
sendToolCallEnvelope(toolCall);
|
sendToolCallEnvelope(toolCall);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +92,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages();
|
List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages();
|
||||||
for (ToolMessage toolMessage : toolMessages) {
|
for (ToolMessage toolMessage : toolMessages) {
|
||||||
memoryPrompt.addMessage(toolMessage);
|
memoryPrompt.addMessage(toolMessage);
|
||||||
|
assistantAccumulator.appendToolResult(toolMessage.getToolCallId(), null, toolMessage.getContent());
|
||||||
sendToolResultEnvelope(toolMessage);
|
sendToolResultEnvelope(toolMessage);
|
||||||
}
|
}
|
||||||
chatModel.chatStream(memoryPrompt, this, chatOptions);
|
chatModel.chatStream(memoryPrompt, this, chatOptions);
|
||||||
@@ -87,10 +102,14 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
}
|
}
|
||||||
String reasoningContent = aiMessage.getReasoningContent();
|
String reasoningContent = aiMessage.getReasoningContent();
|
||||||
if (reasoningContent != null && !reasoningContent.isEmpty()) {
|
if (reasoningContent != null && !reasoningContent.isEmpty()) {
|
||||||
|
assistantAccumulator.appendReasoning(reasoningContent);
|
||||||
|
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(reasoningContent, ChatType.THINKING));
|
||||||
sendChatEnvelope(sseEmitter, reasoningContent, ChatType.THINKING);
|
sendChatEnvelope(sseEmitter, reasoningContent, ChatType.THINKING);
|
||||||
} else {
|
} else {
|
||||||
String delta = aiMessage.getContent();
|
String delta = aiMessage.getContent();
|
||||||
if (delta != null && !delta.isEmpty()) {
|
if (delta != null && !delta.isEmpty()) {
|
||||||
|
assistantAccumulator.appendContent(delta);
|
||||||
|
chatRuntimeManager.recordAssistantDelta(runtimeContext, buildAssistantDeltaMessage(delta, ChatType.MESSAGE));
|
||||||
sendChatEnvelope(sseEmitter, delta, ChatType.MESSAGE);
|
sendChatEnvelope(sseEmitter, delta, ChatType.MESSAGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,10 +130,13 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
// 仅当canStop为true(最后一次无后续工具调用的响应)时,执行业务逻辑
|
// 仅当canStop为true(最后一次无后续工具调用的响应)时,执行业务逻辑
|
||||||
if (this.canStop && completed.compareAndSet(false, true)) {
|
if (this.canStop && completed.compareAndSet(false, true)) {
|
||||||
if (context.getThrowable() != null) {
|
if (context.getThrowable() != null) {
|
||||||
|
chatRuntimeManager.recordFailure(runtimeContext, context.getThrowable());
|
||||||
sendSystemError(sseEmitter, context.getThrowable().getMessage(), context.getThrowable());
|
sendSystemError(sseEmitter, context.getThrowable().getMessage(), context.getThrowable());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
memoryPrompt.addMessage(context.getFullMessage());
|
memoryPrompt.addMessage(context.getFullMessage());
|
||||||
|
chatRuntimeManager.recordAssistantCompleted(runtimeContext, buildAssistantCompletedMessage(context));
|
||||||
|
chatRuntimeManager.recordCompleted(runtimeContext);
|
||||||
ChatEnvelope<Map<String, String>> chatEnvelope = new ChatEnvelope<>();
|
ChatEnvelope<Map<String, String>> chatEnvelope = new ChatEnvelope<>();
|
||||||
chatEnvelope.setDomain(ChatDomain.SYSTEM);
|
chatEnvelope.setDomain(ChatDomain.SYSTEM);
|
||||||
boolean doneSent = sseEmitter.sendDone(chatEnvelope);
|
boolean doneSent = sseEmitter.sendDone(chatEnvelope);
|
||||||
@@ -133,6 +155,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
|||||||
conversationId, throwable.getMessage(), throwable.toString(), throwable);
|
conversationId, throwable.getMessage(), throwable.toString(), throwable);
|
||||||
}
|
}
|
||||||
if (throwable != null && completed.compareAndSet(false, true)) {
|
if (throwable != null && completed.compareAndSet(false, true)) {
|
||||||
|
chatRuntimeManager.recordFailure(runtimeContext, throwable);
|
||||||
sendSystemError(sseEmitter, throwable.getMessage(), throwable);
|
sendSystemError(sseEmitter, throwable.getMessage(), throwable);
|
||||||
}
|
}
|
||||||
stopStreamClient(context, "on_failure", 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import tech.easyflow.ai.entity.Bot;
|
|||||||
import com.mybatisflex.core.service.IService;
|
import com.mybatisflex.core.service.IService;
|
||||||
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
import tech.easyflow.ai.service.impl.BotServiceImpl;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||||
|
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.List;
|
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 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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import org.springframework.web.context.request.RequestContextHolder;
|
|||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import tech.easyflow.ai.easyagents.listener.ChatStreamListener;
|
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.DefaultBotMessageMemory;
|
||||||
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
||||||
|
import tech.easyflow.ai.easyagents.memory.RuntimeChatMemory;
|
||||||
import tech.easyflow.ai.entity.*;
|
import tech.easyflow.ai.entity.*;
|
||||||
import tech.easyflow.ai.mapper.BotMapper;
|
import tech.easyflow.ai.mapper.BotMapper;
|
||||||
import tech.easyflow.ai.service.*;
|
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.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||||
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
import tech.easyflow.core.chat.protocol.sse.ChatSseUtil;
|
||||||
|
import tech.easyflow.core.runtime.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 tech.easyflow.system.service.CategoryPermissionService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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;}
|
public void setConversationIdStr(String conversationIdStr) {this.conversationIdStr = conversationIdStr;}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
|
||||||
private BotMessageService botMessageService;
|
|
||||||
|
|
||||||
@Resource(name = "sseThreadPool")
|
@Resource(name = "sseThreadPool")
|
||||||
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
|
||||||
@Resource
|
@Resource
|
||||||
@@ -110,6 +113,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
FileStorageService storageService;
|
FileStorageService storageService;
|
||||||
@Resource
|
@Resource
|
||||||
private CategoryPermissionService categoryPermissionService;
|
private CategoryPermissionService categoryPermissionService;
|
||||||
|
@Resource
|
||||||
|
private ChatRuntimeManager chatRuntimeManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bot getDetail(String id) {
|
public Bot getDetail(String id) {
|
||||||
@@ -189,7 +194,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SseEmitter startChat(BigInteger botId, String prompt, BigInteger conversationId, List<Map<String, String>> messages,
|
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();
|
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||||
final MemoryPrompt memoryPrompt = new MemoryPrompt();
|
final MemoryPrompt memoryPrompt = new MemoryPrompt();
|
||||||
@@ -214,19 +219,33 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
chatOptions.setThinkingEnabled(enableDeepThinking);
|
chatOptions.setThinkingEnabled(enableDeepThinking);
|
||||||
ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
|
ChatSseEmitter chatSseEmitter = new ChatSseEmitter();
|
||||||
SseEmitter emitter = chatSseEmitter.getEmitter();
|
SseEmitter emitter = chatSseEmitter.getEmitter();
|
||||||
|
int historyLimit = resolveHistoryLimit(maxMessageCount);
|
||||||
if (messages != null && !messages.isEmpty()) {
|
if (messages != null && !messages.isEmpty()) {
|
||||||
ChatMemory defaultChatMemory = new DefaultBotMessageMemory(conversationId, chatSseEmitter, messages);
|
ChatMemory defaultChatMemory = new DefaultBotMessageMemory(conversationId, chatSseEmitter, messages);
|
||||||
memoryPrompt.setMemory(defaultChatMemory);
|
memoryPrompt.setMemory(defaultChatMemory);
|
||||||
} else {
|
} else {
|
||||||
BotMessageMemory memory = new BotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(), conversationId, botMessageService);
|
memoryPrompt.setMemory(new RuntimeChatMemory(
|
||||||
memoryPrompt.setMemory(memory);
|
conversationId,
|
||||||
|
chatRuntimeManager.loadMessages(runtimeContext, historyLimit)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
chatRuntimeManager.prepareSession(runtimeContext);
|
||||||
|
chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, attachments));
|
||||||
memoryPrompt.addMessage(userMessage);
|
memoryPrompt.addMessage(userMessage);
|
||||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||||
threadPoolTaskExecutor.execute(() -> {
|
threadPoolTaskExecutor.execute(() -> {
|
||||||
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
||||||
RequestContextHolder.setRequestAttributes(sra, true);
|
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);
|
chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -239,7 +258,8 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Override
|
@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();
|
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||||
ChatOptions chatOptions = getChatOptions(modelOptions);
|
ChatOptions chatOptions = getChatOptions(modelOptions);
|
||||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||||
@@ -260,11 +280,22 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
memoryPrompt.setSystemMessage(SystemMessage.of(systemPrompt));
|
memoryPrompt.setSystemMessage(SystemMessage.of(systemPrompt));
|
||||||
}
|
}
|
||||||
memoryPrompt.addMessage(userMessage);
|
memoryPrompt.addMessage(userMessage);
|
||||||
|
chatRuntimeManager.prepareSession(runtimeContext);
|
||||||
|
chatRuntimeManager.recordUserMessage(runtimeContext, buildUserRuntimeMessage(runtimeContext, prompt, null));
|
||||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||||
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
|
||||||
threadPoolTaskExecutor.execute(() -> {
|
threadPoolTaskExecutor.execute(() -> {
|
||||||
RequestContextHolder.setRequestAttributes(sra, true);
|
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);
|
chatModel.chatStream(memoryPrompt, streamResponseListener, chatOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -425,6 +456,36 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
|||||||
return messageBuilder.toString();
|
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) {
|
private String buildSystemPromptWithFaqImageRule(String systemPrompt) {
|
||||||
if (!StringUtils.hasLength(systemPrompt)) {
|
if (!StringUtils.hasLength(systemPrompt)) {
|
||||||
return FAQ_IMAGE_SYSTEM_RULE;
|
return FAQ_IMAGE_SYSTEM_RULE;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package tech.easyflow.auth.service;
|
|||||||
import tech.easyflow.auth.entity.LoginDTO;
|
import tech.easyflow.auth.entity.LoginDTO;
|
||||||
import tech.easyflow.auth.entity.LoginVO;
|
import tech.easyflow.auth.entity.LoginVO;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
public interface AuthService {
|
public interface AuthService {
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
@@ -13,4 +15,14 @@ public interface AuthService {
|
|||||||
* 开发模式免登录
|
* 开发模式免登录
|
||||||
*/
|
*/
|
||||||
LoginVO devLogin(String account);
|
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;
|
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.LoginDTO;
|
||||||
import tech.easyflow.auth.entity.LoginVO;
|
import tech.easyflow.auth.entity.LoginVO;
|
||||||
import tech.easyflow.auth.service.AuthService;
|
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.constant.enums.EnumDataStatus;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.entity.SysApiKey;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
import tech.easyflow.system.entity.SysMenu;
|
import tech.easyflow.system.entity.SysMenu;
|
||||||
import tech.easyflow.system.entity.SysRole;
|
import tech.easyflow.system.entity.SysRole;
|
||||||
|
import tech.easyflow.system.service.SysApiKeyService;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
import tech.easyflow.system.service.SysMenuService;
|
import tech.easyflow.system.service.SysMenuService;
|
||||||
import tech.easyflow.system.service.SysRoleService;
|
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 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 javax.annotation.Resource;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -35,6 +39,8 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
|||||||
private SysRoleService sysRoleService;
|
private SysRoleService sysRoleService;
|
||||||
@Resource
|
@Resource
|
||||||
private SysMenuService sysMenuService;
|
private SysMenuService sysMenuService;
|
||||||
|
@Resource
|
||||||
|
private SysApiKeyService sysApiKeyService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginVO login(LoginDTO loginDTO) {
|
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
|
@Override
|
||||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||||
List<SysMenu> menus = sysMenuService.getMenusByAccountId(new SysMenu(), BigInteger.valueOf(Long.parseLong(loginId.toString())));
|
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) {
|
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();
|
LoginAccount loginAccount = new LoginAccount();
|
||||||
BeanUtil.copyProperties(record, loginAccount);
|
BeanUtil.copyProperties(record, loginAccount);
|
||||||
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
StpUtil.getSession().set(Constants.LOGIN_USER_KEY, loginAccount);
|
||||||
@@ -92,10 +131,32 @@ public class AuthServiceImpl implements AuthService, StpInterface {
|
|||||||
return res;
|
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) {
|
private SysAccount getAvailableAccount(String account, String accountNotFoundMessage) {
|
||||||
QueryWrapper w = QueryWrapper.create();
|
QueryWrapper w = QueryWrapper.create();
|
||||||
w.eq(SysAccount::getLoginName, account);
|
w.eq(SysAccount::getLoginName, account);
|
||||||
SysAccount record = sysAccountService.getOne(w);
|
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) {
|
if (record == null) {
|
||||||
throw new BusinessException(accountNotFoundMessage);
|
throw new BusinessException(accountNotFoundMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ export const api = createRequestClient(apiURL, {
|
|||||||
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
|
||||||
|
|
||||||
export interface SseOptions {
|
export interface SseOptions {
|
||||||
|
headers?: HeadersInit;
|
||||||
onMessage?: (message: ServerSentEventMessage) => void;
|
onMessage?: (message: ServerSentEventMessage) => void;
|
||||||
onError?: (err: any) => void;
|
onError?: (err: any) => void;
|
||||||
onFinished?: () => void;
|
onFinished?: () => void;
|
||||||
@@ -186,7 +187,7 @@ export class SseClient {
|
|||||||
const res = await fetch(apiURL + url, {
|
const res = await fetch(apiURL + url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
signal, // 使用局部变量 signal
|
signal, // 使用局部变量 signal
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(options?.headers),
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,13 +234,20 @@ export class SseClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getHeaders() {
|
private getHeaders(extraHeaders?: HeadersInit) {
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
return {
|
const headers: Record<string, string> = {
|
||||||
Accept: 'text/event-stream',
|
Accept: 'text/event-stream',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'easyflow-token': accessStore.accessToken || '',
|
'easyflow-token': accessStore.accessToken || '',
|
||||||
};
|
};
|
||||||
|
if (!extraHeaders) {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
new Headers(extraHeaders).forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
return headers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
registerLoadingDirective,
|
registerLoadingDirective,
|
||||||
setDefaultModalProps,
|
setDefaultModalProps,
|
||||||
} from '@easyflow/common-ui';
|
} from '@easyflow/common-ui';
|
||||||
|
import '@easyflow/icons';
|
||||||
import { preferences } from '@easyflow/preferences';
|
import { preferences } from '@easyflow/preferences';
|
||||||
import { initStores } from '@easyflow/stores';
|
import { initStores } from '@easyflow/stores';
|
||||||
import '@easyflow/styles';
|
import '@easyflow/styles';
|
||||||
|
|||||||
@@ -0,0 +1,557 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ChatThinkingBlock } from '@easyflow/common-ui';
|
||||||
|
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@easyflow/icons';
|
||||||
|
|
||||||
|
import { CircleCheck, Close } from '@element-plus/icons-vue';
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElCollapse,
|
||||||
|
ElCollapseItem,
|
||||||
|
ElEmpty,
|
||||||
|
ElIcon,
|
||||||
|
ElScrollbar,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import ShowJson from '#/components/json/ShowJson.vue';
|
||||||
|
|
||||||
|
interface ChatHistoryDetailDrawerProps {
|
||||||
|
visible?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
session?: any;
|
||||||
|
messages?: any[];
|
||||||
|
hasMore?: boolean;
|
||||||
|
onLoadMore?: (() => void | Promise<void>) | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ChatHistoryDetailDrawerProps>(), {
|
||||||
|
visible: false,
|
||||||
|
loading: false,
|
||||||
|
session: undefined,
|
||||||
|
messages: () => [],
|
||||||
|
hasMore: false,
|
||||||
|
onLoadMore: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function formatTime(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const time = new Date(value);
|
||||||
|
if (Number.isNaN(time.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const year = time.getFullYear();
|
||||||
|
const month = String(time.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(time.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(time.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(time.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSenderName(item: any) {
|
||||||
|
if (item?.senderName) {
|
||||||
|
return item.senderName;
|
||||||
|
}
|
||||||
|
return item?.role === 'assistant' ? '聊天助手' : '聊天用户';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isThinkingChain(chain: any) {
|
||||||
|
return !chain?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolStatusText(status?: string) {
|
||||||
|
return status === 'TOOL_RESULT' ? '调用成功' : '工具调用中';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoadMore() {
|
||||||
|
await props.onLoadMore?.();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-loading="loading"
|
||||||
|
class="chat-history-detail"
|
||||||
|
:class="{ 'is-visible': visible }"
|
||||||
|
>
|
||||||
|
<div class="chat-history-detail__summary">
|
||||||
|
<div class="chat-history-detail__summary-main">
|
||||||
|
<div class="chat-history-detail__title-row">
|
||||||
|
<h2 class="chat-history-detail__title">
|
||||||
|
{{ session?.title || '未命名会话' }}
|
||||||
|
</h2>
|
||||||
|
<span class="chat-history-detail__assistant-tag">
|
||||||
|
{{ session?.assistantName || '聊天助手' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-history-detail__meta-row">
|
||||||
|
<span class="chat-history-detail__meta-inline">
|
||||||
|
<span class="chat-history-detail__meta-label">聊天用户</span>
|
||||||
|
<span class="chat-history-detail__meta-value">
|
||||||
|
{{ session?.userAccount || '-' }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="chat-history-detail__meta-divider"></span>
|
||||||
|
<span class="chat-history-detail__meta-inline">
|
||||||
|
<span class="chat-history-detail__meta-label">最近活跃</span>
|
||||||
|
<span class="chat-history-detail__meta-value">
|
||||||
|
{{ formatTime(session?.lastMessageAt || session?.accessAt) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="chat-history-detail__meta-divider"></span>
|
||||||
|
<span class="chat-history-detail__meta-inline">
|
||||||
|
<span class="chat-history-detail__meta-label">消息数</span>
|
||||||
|
<span class="chat-history-detail__meta-value">
|
||||||
|
{{ session?.messageCount ?? messages.length ?? 0 }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chat-history-detail__close"
|
||||||
|
aria-label="关闭聊天详情"
|
||||||
|
@click="emit('close')"
|
||||||
|
>
|
||||||
|
<ElIcon size="18">
|
||||||
|
<Close />
|
||||||
|
</ElIcon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-history-detail__stream">
|
||||||
|
<div class="chat-history-detail__stream-toolbar">
|
||||||
|
<ElButton
|
||||||
|
v-if="hasMore"
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
@click="handleLoadMore"
|
||||||
|
>
|
||||||
|
加载更早消息
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElScrollbar class="chat-history-detail__scrollbar">
|
||||||
|
<div
|
||||||
|
v-if="messages.length > 0"
|
||||||
|
class="chat-history-detail__message-list"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
v-for="item in messages"
|
||||||
|
:key="item.key"
|
||||||
|
class="chat-history-detail__message"
|
||||||
|
:class="`is-${item.role}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-history-detail__message-meta"
|
||||||
|
:class="`is-${item.role}`"
|
||||||
|
>
|
||||||
|
<span class="chat-history-detail__message-author">
|
||||||
|
{{ resolveSenderName(item) }}
|
||||||
|
</span>
|
||||||
|
<span class="chat-history-detail__message-time">
|
||||||
|
{{ formatTime(item.created) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="chat-history-detail__message-bubble"
|
||||||
|
:class="`is-${item.role}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="item.chains?.length"
|
||||||
|
class="chat-history-detail__message-chains"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(chain, index) in item.chains"
|
||||||
|
:key="chain.id || index"
|
||||||
|
>
|
||||||
|
<ChatThinkingBlock
|
||||||
|
v-if="isThinkingChain(chain)"
|
||||||
|
v-model:expanded="chain.thinkingExpanded"
|
||||||
|
:content="chain.reasoning_content"
|
||||||
|
readonly
|
||||||
|
:status="chain.thinkingStatus"
|
||||||
|
class="chat-history-detail__thinking"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ElCollapse v-else class="chat-history-detail__tool-panel">
|
||||||
|
<ElCollapseItem :title="chain.name" :name="chain.id">
|
||||||
|
<template #title>
|
||||||
|
<div class="chat-history-detail__tool-title">
|
||||||
|
<ElIcon size="16">
|
||||||
|
<IconifyIcon icon="svg:wrench" />
|
||||||
|
</ElIcon>
|
||||||
|
<span class="chat-history-detail__tool-name">
|
||||||
|
{{ chain.name }}
|
||||||
|
</span>
|
||||||
|
<div class="chat-history-detail__tool-status">
|
||||||
|
<ElIcon
|
||||||
|
v-if="chain.status === 'TOOL_RESULT'"
|
||||||
|
size="14"
|
||||||
|
color="var(--el-color-success)"
|
||||||
|
>
|
||||||
|
<CircleCheck />
|
||||||
|
</ElIcon>
|
||||||
|
<IconifyIcon
|
||||||
|
v-else
|
||||||
|
icon="mdi:clock-time-five-outline"
|
||||||
|
/>
|
||||||
|
<span>{{ toolStatusText(chain.status) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ShowJson :value="chain.result" />
|
||||||
|
</ElCollapseItem>
|
||||||
|
</ElCollapse>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item.content && String(item.content).trim()"
|
||||||
|
class="chat-history-detail__markdown"
|
||||||
|
>
|
||||||
|
<ElXMarkdown :markdown="item.content" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ElEmpty v-else description="暂无聊天消息" />
|
||||||
|
</ElScrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-history-detail {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at top right,
|
||||||
|
hsl(var(--nav-ambient) / 0.1),
|
||||||
|
transparent 26%
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--glass-border) / 0.18) 0%,
|
||||||
|
hsl(var(--glass-tint) / 0.34) 10%,
|
||||||
|
hsl(var(--surface-panel) / 0.88) 28%,
|
||||||
|
hsl(var(--surface-panel) / 0.96) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 24px 12px;
|
||||||
|
border-bottom: 1px solid hsl(var(--divider-faint) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__summary-main {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__title-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__title {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__assistant-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid hsl(var(--glass-border) / 0.38);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--glass-tint) / 0.54);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--nav-item-active-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-value {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-divider {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 12px;
|
||||||
|
width: 1px;
|
||||||
|
background: hsl(var(--divider-faint) / 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__close {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 36px;
|
||||||
|
width: 36px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--glass-tint) / 0.46);
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease,
|
||||||
|
background-color 0.18s ease,
|
||||||
|
color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__close:hover {
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.92);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__stream {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 14px 24px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__stream-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__scrollbar {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message.is-user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message.is-assistant {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-meta {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 88%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-meta.is-user {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-author {
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-bubble {
|
||||||
|
width: min(88%, 760px);
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.34);
|
||||||
|
border-radius: 22px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.72;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-bubble.is-assistant {
|
||||||
|
background: hsl(var(--glass-tint) / 0.88);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.48),
|
||||||
|
0 12px 26px -24px hsl(var(--foreground) / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-bubble.is-user {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--primary) / 0.14) 0%,
|
||||||
|
hsl(var(--surface-panel) / 0.96) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.42),
|
||||||
|
0 16px 30px -26px hsl(var(--primary) / 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-chains {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__thinking {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-panel {
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.24);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: hsl(var(--surface-panel) / 0.68);
|
||||||
|
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-title {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-name {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.92);
|
||||||
|
font-size: 12px;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown {
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown :deep(.markdown-body) {
|
||||||
|
background: transparent;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown :deep(.markdown-body > :first-child) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown :deep(.markdown-body > :last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown :deep(.markdown-body p) {
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__markdown :deep(pre) {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-panel :deep(.el-collapse-item__wrap) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__tool-panel :deep(.el-collapse-item__header) {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-right: 14px;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: hsl(var(--divider-faint) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-bubble :deep(.el-collapse-item__content) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.chat-history-detail__summary {
|
||||||
|
padding: 18px 18px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__stream {
|
||||||
|
padding: 12px 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-row {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__meta-inline {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-detail__message-bubble,
|
||||||
|
.chat-history-detail__message-meta {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,16 +3,18 @@ import type {
|
|||||||
BubbleListInstance,
|
BubbleListInstance,
|
||||||
BubbleListProps,
|
BubbleListProps,
|
||||||
} from 'vue-element-plus-x/types/BubbleList';
|
} from 'vue-element-plus-x/types/BubbleList';
|
||||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
|
||||||
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';
|
import type { TypewriterInstance } from 'vue-element-plus-x/types/Typewriter';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ChatThinkingBlock,
|
||||||
|
type ChatThinkingBlockStatus,
|
||||||
|
} from '@easyflow/common-ui';
|
||||||
import type { BotInfo, ChatMessage } from '@easyflow/types';
|
import type { BotInfo, ChatMessage } from '@easyflow/types';
|
||||||
|
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue';
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch, watchEffect } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import ElBubbleList from 'vue-element-plus-x/es/BubbleList/index.js';
|
import ElBubbleList from 'vue-element-plus-x/es/BubbleList/index.js';
|
||||||
import ElSender from 'vue-element-plus-x/es/Sender/index.js';
|
import ElSender from 'vue-element-plus-x/es/Sender/index.js';
|
||||||
import ElThinking from 'vue-element-plus-x/es/Thinking/index.js';
|
|
||||||
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
|
import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js';
|
||||||
|
|
||||||
import { IconifyIcon } from '@easyflow/icons';
|
import { IconifyIcon } from '@easyflow/icons';
|
||||||
@@ -49,8 +51,8 @@ import SendingIcon from '../icons/SendingIcon.vue';
|
|||||||
|
|
||||||
type Think = {
|
type Think = {
|
||||||
reasoning_content?: string;
|
reasoning_content?: string;
|
||||||
thinkingStatus?: ThinkingStatus;
|
thinkingStatus?: ChatThinkingBlockStatus;
|
||||||
thinlCollapse?: boolean;
|
thinkingExpanded?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Tool = {
|
type Tool = {
|
||||||
@@ -329,7 +331,7 @@ const handleSubmit = async (refreshContent: string) => {
|
|||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
chains.push({
|
chains.push({
|
||||||
thinkingStatus: 'thinking',
|
thinkingStatus: 'thinking',
|
||||||
thinlCollapse: true,
|
thinkingExpanded: false,
|
||||||
reasoning_content: delta,
|
reasoning_content: delta,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -525,13 +527,14 @@ onBeforeUnmount(() => {
|
|||||||
v-for="(chain, index) in item.chains"
|
v-for="(chain, index) in item.chains"
|
||||||
:key="chain.id || index"
|
:key="chain.id || index"
|
||||||
>
|
>
|
||||||
<ElThinking
|
<ChatThinkingBlock
|
||||||
v-if="isThink(chain)"
|
v-if="isThink(chain)"
|
||||||
v-model="chain.thinlCollapse"
|
v-model:expanded="chain.thinkingExpanded"
|
||||||
:content="chain.reasoning_content"
|
:content="chain.reasoning_content"
|
||||||
:status="chain.thinkingStatus"
|
:status="chain.thinkingStatus"
|
||||||
|
class="chat-thinking-block-item"
|
||||||
/>
|
/>
|
||||||
<ElCollapse v-else class="mb-2">
|
<ElCollapse v-else class="chat-tool-panel">
|
||||||
<ElCollapseItem :title="chain.name" :name="chain.id">
|
<ElCollapseItem :title="chain.name" :name="chain.id">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center gap-2 pl-5">
|
<div class="flex items-center gap-2 pl-5">
|
||||||
@@ -569,42 +572,6 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <ElThinking
|
|
||||||
v-if="item.reasoning_content"
|
|
||||||
v-model="item.thinlCollapse"
|
|
||||||
:content="item.reasoning_content"
|
|
||||||
:status="item.thinkingStatus"
|
|
||||||
class="mb-3"
|
|
||||||
/> -->
|
|
||||||
<!-- <ElCollapse v-if="item.tools" class="mb-2">
|
|
||||||
<ElCollapseItem
|
|
||||||
class="mb-2"
|
|
||||||
v-for="tool in item.tools"
|
|
||||||
:key="tool.id"
|
|
||||||
:title="tool.name"
|
|
||||||
:name="tool.id"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center gap-2 pl-5">
|
|
||||||
<ElIcon size="16">
|
|
||||||
<IconifyIcon icon="svg:wrench" />
|
|
||||||
</ElIcon>
|
|
||||||
<span>{{ tool.name }}</span>
|
|
||||||
<template v-if="tool.status === 'TOOL_CALL'">
|
|
||||||
<ElIcon size="16">
|
|
||||||
<IconifyIcon icon="svg:spinner" />
|
|
||||||
</ElIcon>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<ElIcon size="16" color="var(--el-color-success)">
|
|
||||||
<CircleCheck />
|
|
||||||
</ElIcon>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<ShowJson :value="tool.result" />
|
|
||||||
</ElCollapseItem>
|
|
||||||
</ElCollapse> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 自定义头像 -->
|
<!-- 自定义头像 -->
|
||||||
@@ -799,22 +766,38 @@ onBeforeUnmount(() => {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-thinking) {
|
.chat-thinking-block-item {
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-thinking .content-wrapper) {
|
|
||||||
--el-thinking-content-wrapper-width: var(--bubble-content-max-width);
|
|
||||||
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-collapse-item) {
|
.chat-tool-panel {
|
||||||
overflow: hidden;
|
margin-bottom: 8px;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-collapse-item__content) {
|
:deep(.chat-tool-panel.el-collapse) {
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.26);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: hsl(var(--surface-panel) / 0.7);
|
||||||
|
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item) {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item__wrap) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item__header) {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-right: 14px;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: hsl(var(--divider-faint) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item__content) {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
"notConfigured": "NotConfigured",
|
"notConfigured": "NotConfigured",
|
||||||
"chatPublishBaseUrlMissing": "Publish base URL is not configured. Please set it in system settings first.",
|
"chatPublishBaseUrlMissing": "Publish base URL is not configured. Please set it in system settings first.",
|
||||||
"chatExternalLink": "Chat External Link",
|
"chatExternalLink": "Chat External Link",
|
||||||
|
"chatAccessToken": "Access Token",
|
||||||
|
"chatAccessTokenPlaceholder": "Optional. Selected links will include the token",
|
||||||
|
"chatAccessTokenHint": "Only enabled access tokens with public-api chat permission are listed. After selection, copied links and iframe code will automatically include the token.",
|
||||||
"iframeEmbedCode": "Iframe Embed Code",
|
"iframeEmbedCode": "Iframe Embed Code",
|
||||||
"copyLink": "Copy Link",
|
"copyLink": "Copy Link",
|
||||||
"copyIframeCode": "Copy Code",
|
"copyIframeCode": "Copy Code",
|
||||||
@@ -36,6 +39,7 @@
|
|||||||
"publicChatLoading": "Initializing chat environment...",
|
"publicChatLoading": "Initializing chat environment...",
|
||||||
"publicChatThinking": "Thinking...",
|
"publicChatThinking": "Thinking...",
|
||||||
"publicChatInitError": "Initialization failed, please try again later",
|
"publicChatInitError": "Initialization failed, please try again later",
|
||||||
|
"publicChatTokenInvalid": "The access token is invalid or expired",
|
||||||
"publicChatAssistantReply": "Assistant Reply",
|
"publicChatAssistantReply": "Assistant Reply",
|
||||||
"publicChatToolCalling": "Calling tool",
|
"publicChatToolCalling": "Calling tool",
|
||||||
"publicChatToolDone": "Tool completed",
|
"publicChatToolDone": "Tool completed",
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
"notConfigured": "未配置",
|
"notConfigured": "未配置",
|
||||||
"chatPublishBaseUrlMissing": "未配置发布域名,请先到系统设置中配置",
|
"chatPublishBaseUrlMissing": "未配置发布域名,请先到系统设置中配置",
|
||||||
"chatExternalLink": "聊天外链",
|
"chatExternalLink": "聊天外链",
|
||||||
|
"chatAccessToken": "访问令牌",
|
||||||
|
"chatAccessTokenPlaceholder": "可选,选择后外链将携带访问令牌",
|
||||||
|
"chatAccessTokenHint": "仅展示已启用且具备 public-api 聊天权限的访问令牌。选中后,复制链接和 iframe 代码会自动附带该令牌。",
|
||||||
"iframeEmbedCode": "iframe 嵌入代码",
|
"iframeEmbedCode": "iframe 嵌入代码",
|
||||||
"copyLink": "复制链接",
|
"copyLink": "复制链接",
|
||||||
"copyIframeCode": "复制代码",
|
"copyIframeCode": "复制代码",
|
||||||
@@ -36,6 +39,7 @@
|
|||||||
"publicChatLoading": "正在初始化聊天环境...",
|
"publicChatLoading": "正在初始化聊天环境...",
|
||||||
"publicChatThinking": "思考中...",
|
"publicChatThinking": "思考中...",
|
||||||
"publicChatInitError": "初始化失败,请稍后重试",
|
"publicChatInitError": "初始化失败,请稍后重试",
|
||||||
|
"publicChatTokenInvalid": "访问令牌无效或已过期",
|
||||||
"publicChatAssistantReply": "助手回复",
|
"publicChatAssistantReply": "助手回复",
|
||||||
"publicChatToolCalling": "工具调用中",
|
"publicChatToolCalling": "工具调用中",
|
||||||
"publicChatToolDone": "工具已返回",
|
"publicChatToolDone": "工具已返回",
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {AiLlm, BotInfo} from '@easyflow/types';
|
import type { AiLlm, BotInfo } from '@easyflow/types';
|
||||||
|
|
||||||
import {computed, onMounted, ref, watch} from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import {useRoute} from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import {$t} from '@easyflow/locales';
|
import { $t } from '@easyflow/locales';
|
||||||
import {useBotStore} from '@easyflow/stores';
|
import { useBotStore } from '@easyflow/stores';
|
||||||
|
|
||||||
import {CopyDocument, Delete, InfoFilled, Link, Plus, Setting} from '@element-plus/icons-vue';
|
import {
|
||||||
import {useDebounceFn} from '@vueuse/core';
|
CopyDocument,
|
||||||
|
Delete,
|
||||||
|
InfoFilled,
|
||||||
|
Link,
|
||||||
|
Plus,
|
||||||
|
Setting,
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import { useDebounceFn } from '@vueuse/core';
|
||||||
import {
|
import {
|
||||||
ElAlert,
|
ElAlert,
|
||||||
ElButton,
|
ElButton,
|
||||||
@@ -19,6 +26,7 @@ import {
|
|||||||
ElInput,
|
ElInput,
|
||||||
ElInputNumber,
|
ElInputNumber,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
ElRow,
|
ElRow,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSkeleton,
|
ElSkeleton,
|
||||||
@@ -26,7 +34,7 @@ import {
|
|||||||
ElSwitch,
|
ElSwitch,
|
||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
import {tryit} from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getPerQuestions,
|
getPerQuestions,
|
||||||
@@ -35,7 +43,7 @@ import {
|
|||||||
updateLlmId,
|
updateLlmId,
|
||||||
updateLlmOptions,
|
updateLlmOptions,
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
import {api} from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import ProblemPresupposition from '#/components/chat/ProblemPresupposition.vue';
|
import ProblemPresupposition from '#/components/chat/ProblemPresupposition.vue';
|
||||||
import PublishWxOfficalAccount from '#/components/chat/PublishWxOfficalAccount.vue';
|
import PublishWxOfficalAccount from '#/components/chat/PublishWxOfficalAccount.vue';
|
||||||
import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue';
|
import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue';
|
||||||
@@ -47,6 +55,14 @@ interface SelectedMcpTool {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApiKeyOption {
|
||||||
|
id: string;
|
||||||
|
apiKey: string;
|
||||||
|
expiredAt?: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
bot?: BotInfo;
|
bot?: BotInfo;
|
||||||
hasSavePermission?: boolean;
|
hasSavePermission?: boolean;
|
||||||
@@ -68,6 +84,8 @@ const dialogueSettings = ref({
|
|||||||
enableDeepThinking: false,
|
enableDeepThinking: false,
|
||||||
anonymousEnabled: false,
|
anonymousEnabled: false,
|
||||||
});
|
});
|
||||||
|
const selectedPublishApiKey = ref('');
|
||||||
|
const publishApiKeyOptions = ref<ApiKeyOption[]>([]);
|
||||||
const publishBaseUrl = ref('');
|
const publishBaseUrl = ref('');
|
||||||
const routerHistoryMode = import.meta.env.VITE_ROUTER_HISTORY;
|
const routerHistoryMode = import.meta.env.VITE_ROUTER_HISTORY;
|
||||||
const normalizePublishBaseUrl = (value: string) => {
|
const normalizePublishBaseUrl = (value: string) => {
|
||||||
@@ -102,18 +120,29 @@ const publicChatPath = computed(() =>
|
|||||||
? `/#/embed/chat/${botId.value}`
|
? `/#/embed/chat/${botId.value}`
|
||||||
: `/embed/chat/${botId.value}`,
|
: `/embed/chat/${botId.value}`,
|
||||||
);
|
);
|
||||||
const publicChatUrl = computed(() => {
|
const buildPublicChatUrl = (embed = false) => {
|
||||||
if (!hasPublishBaseUrl.value) {
|
if (!hasPublishBaseUrl.value) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const base = normalizePublishBaseUrl(publishBaseUrl.value);
|
const base = normalizePublishBaseUrl(publishBaseUrl.value);
|
||||||
return `${base}${publicChatPath.value}`;
|
const query = new URLSearchParams();
|
||||||
|
if (selectedPublishApiKey.value) {
|
||||||
|
query.set('token', selectedPublishApiKey.value);
|
||||||
|
}
|
||||||
|
if (embed) {
|
||||||
|
query.set('embed', '1');
|
||||||
|
}
|
||||||
|
const queryString = query.toString();
|
||||||
|
if (!queryString) {
|
||||||
|
return `${base}${publicChatPath.value}`;
|
||||||
|
}
|
||||||
|
return `${base}${publicChatPath.value}?${queryString}`;
|
||||||
|
};
|
||||||
|
const publicChatUrl = computed(() => {
|
||||||
|
return buildPublicChatUrl(false);
|
||||||
});
|
});
|
||||||
const publicChatEmbedUrl = computed(() => {
|
const publicChatEmbedUrl = computed(() => {
|
||||||
if (!publicChatUrl.value) {
|
return buildPublicChatUrl(true);
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return `${publicChatUrl.value}?embed=1`;
|
|
||||||
});
|
});
|
||||||
const iframeCode = computed(() => {
|
const iframeCode = computed(() => {
|
||||||
if (!publicChatEmbedUrl.value) {
|
if (!publicChatEmbedUrl.value) {
|
||||||
@@ -233,7 +262,9 @@ const updatingBotIcon = ref(false);
|
|||||||
const updatingBasicInfo = ref(false);
|
const updatingBasicInfo = ref(false);
|
||||||
const syncingBasicInfoForm = ref(false);
|
const syncingBasicInfoForm = ref(false);
|
||||||
const getPublishBaseUrl = async () => {
|
const getPublishBaseUrl = async () => {
|
||||||
const [, res] = await tryit(api.get)('/api/v1/sysOption/list?keys=chat_publish_base_url');
|
const [, res] = await tryit(api.get)(
|
||||||
|
'/api/v1/sysOption/list?keys=chat_publish_base_url',
|
||||||
|
);
|
||||||
if (res?.errorCode === 0) {
|
if (res?.errorCode === 0) {
|
||||||
publishBaseUrl.value = (res.data?.chat_publish_base_url || '').trim();
|
publishBaseUrl.value = (res.data?.chat_publish_base_url || '').trim();
|
||||||
}
|
}
|
||||||
@@ -267,6 +298,84 @@ const getBotDetail = async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const formatApiKeyOptionLabel = (apiKey: string, expiredAt?: string) => {
|
||||||
|
const normalized = String(apiKey || '').trim();
|
||||||
|
if (!normalized) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const prefix = normalized.slice(0, 8);
|
||||||
|
const suffix = normalized.slice(-6);
|
||||||
|
const baseLabel =
|
||||||
|
normalized.length > 18 ? `${prefix}...${suffix}` : normalized;
|
||||||
|
return expiredAt ? `${baseLabel} · ${expiredAt}` : baseLabel;
|
||||||
|
};
|
||||||
|
const getPublishApiKeyOptions = async () => {
|
||||||
|
const [resourceErr, resourceRes] = await tryit(api.get)(
|
||||||
|
'/api/v1/sysApiKeyResourcePermission/list',
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
resourceErr ||
|
||||||
|
resourceRes?.errorCode !== 0 ||
|
||||||
|
!Array.isArray(resourceRes?.data)
|
||||||
|
) {
|
||||||
|
publishApiKeyOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const publicChatResource = resourceRes.data.find(
|
||||||
|
(item: any) => item?.requestInterface === '/public-api/bot/chat',
|
||||||
|
);
|
||||||
|
if (!publicChatResource?.id) {
|
||||||
|
publishApiKeyOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [apiKeyErr, apiKeyRes] = await tryit(api.get)(
|
||||||
|
'/api/v1/sysApiKey/page',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 200,
|
||||||
|
sortKey: 'created',
|
||||||
|
sortType: 'desc',
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (apiKeyErr || apiKeyRes?.errorCode !== 0) {
|
||||||
|
publishApiKeyOptions.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = apiKeyRes?.data?.records || [];
|
||||||
|
const now = Date.now();
|
||||||
|
publishApiKeyOptions.value = records
|
||||||
|
.filter((item: any) => {
|
||||||
|
if (item?.status !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (item?.expiredAt && new Date(item.expiredAt).getTime() <= now) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const permissionIds = Array.isArray(item?.permissionIds)
|
||||||
|
? item.permissionIds.map(String)
|
||||||
|
: [];
|
||||||
|
return permissionIds.includes(String(publicChatResource.id));
|
||||||
|
})
|
||||||
|
.map((item: any) => ({
|
||||||
|
id: String(item.id || ''),
|
||||||
|
apiKey: String(item.apiKey || ''),
|
||||||
|
expiredAt: item.expiredAt,
|
||||||
|
label: formatApiKeyOptionLabel(item.apiKey, item.expiredAt),
|
||||||
|
}));
|
||||||
|
if (
|
||||||
|
selectedPublishApiKey.value &&
|
||||||
|
!publishApiKeyOptions.value.some(
|
||||||
|
(item) => item.apiKey === selectedPublishApiKey.value,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
selectedPublishApiKey.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
const getLlmListData = async () => {
|
const getLlmListData = async () => {
|
||||||
const url = `/api/v1/model/list?modelType=chatModel&added=true`;
|
const url = `/api/v1/model/list?modelType=chatModel&added=true`;
|
||||||
api.get(url, {}).then((res) => {
|
api.get(url, {}).then((res) => {
|
||||||
@@ -277,6 +386,7 @@ const getLlmListData = async () => {
|
|||||||
};
|
};
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
getPublishBaseUrl();
|
getPublishBaseUrl();
|
||||||
|
getPublishApiKeyOptions();
|
||||||
getAiBotPluginToolList();
|
getAiBotPluginToolList();
|
||||||
getAiBotKnowledgeList();
|
getAiBotKnowledgeList();
|
||||||
getAiBotWorkflowList();
|
getAiBotWorkflowList();
|
||||||
@@ -285,9 +395,7 @@ onMounted(async () => {
|
|||||||
getLlmListData();
|
getLlmListData();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAnonymousAccessChange = (
|
const handleAnonymousAccessChange = (value: boolean | number | string) => {
|
||||||
value: boolean | number | string,
|
|
||||||
) => {
|
|
||||||
handleDialogOptionsStrChange('anonymousEnabled', value);
|
handleDialogOptionsStrChange('anonymousEnabled', value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -681,14 +789,19 @@ const handleBasicInfoChange = async (
|
|||||||
key: 'alias' | 'categoryId' | 'title',
|
key: 'alias' | 'categoryId' | 'title',
|
||||||
value: any,
|
value: any,
|
||||||
) => {
|
) => {
|
||||||
if (!botInfo.value || !props.hasSavePermission || syncingBasicInfoForm.value) {
|
if (
|
||||||
|
!botInfo.value ||
|
||||||
|
!props.hasSavePermission ||
|
||||||
|
syncingBasicInfoForm.value
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (updatingBasicInfo.value) {
|
if (updatingBasicInfo.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedValue = key === 'categoryId' ? value : String(value || '').trim();
|
const normalizedValue =
|
||||||
|
key === 'categoryId' ? value : String(value || '').trim();
|
||||||
if ((key === 'title' || key === 'alias') && !normalizedValue) {
|
if ((key === 'title' || key === 'alias') && !normalizedValue) {
|
||||||
ElMessage.warning($t('message.required'));
|
ElMessage.warning($t('message.required'));
|
||||||
basicInfoForm.value[key] = botInfo.value[key] as string;
|
basicInfoForm.value[key] = botInfo.value[key] as string;
|
||||||
@@ -729,7 +842,7 @@ const handleBasicInfoChange = async (
|
|||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'bot-avatar-upload-wrap',
|
'bot-avatar-upload-wrap',
|
||||||
(!hasSavePermission || updatingBotIcon) ? 'is-disabled' : '',
|
!hasSavePermission || updatingBotIcon ? 'is-disabled' : '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<UploadAvatar
|
<UploadAvatar
|
||||||
@@ -742,7 +855,9 @@ const handleBasicInfoChange = async (
|
|||||||
</div>
|
</div>
|
||||||
<div class="bot-basic-form-panel">
|
<div class="bot-basic-form-panel">
|
||||||
<div class="bot-basic-form-item">
|
<div class="bot-basic-form-item">
|
||||||
<span class="bot-basic-form-label">{{ $t('aiWorkflow.title') }}</span>
|
<span class="bot-basic-form-label">{{
|
||||||
|
$t('aiWorkflow.title')
|
||||||
|
}}</span>
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="basicInfoForm.title"
|
v-model="basicInfoForm.title"
|
||||||
:disabled="!hasSavePermission || updatingBasicInfo"
|
:disabled="!hasSavePermission || updatingBasicInfo"
|
||||||
@@ -758,12 +873,16 @@ const handleBasicInfoChange = async (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="bot-basic-form-item">
|
<div class="bot-basic-form-item">
|
||||||
<span class="bot-basic-form-label">{{ $t('aiWorkflow.categoryId') }}</span>
|
<span class="bot-basic-form-label">{{
|
||||||
|
$t('aiWorkflow.categoryId')
|
||||||
|
}}</span>
|
||||||
<DictSelect
|
<DictSelect
|
||||||
v-model="basicInfoForm.categoryId"
|
v-model="basicInfoForm.categoryId"
|
||||||
dict-code="aiBotCategory"
|
dict-code="aiBotCategory"
|
||||||
:disabled="!hasSavePermission || updatingBasicInfo"
|
:disabled="!hasSavePermission || updatingBasicInfo"
|
||||||
@change="(value: any) => handleBasicInfoChange('categoryId', value)"
|
@change="
|
||||||
|
(value: any) => handleBasicInfoChange('categoryId', value)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1165,6 +1284,25 @@ const handleBasicInfoChange = async (
|
|||||||
:closable="false"
|
:closable="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="publish-external-item">
|
||||||
|
<label class="publish-external-label">
|
||||||
|
{{ $t('bot.chatAccessToken') }}
|
||||||
|
</label>
|
||||||
|
<ElSelect
|
||||||
|
v-model="selectedPublishApiKey"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
class="w-full"
|
||||||
|
:placeholder="$t('bot.chatAccessTokenPlaceholder')"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="item in publishApiKeyOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.apiKey"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</div>
|
||||||
<div class="publish-external-item">
|
<div class="publish-external-item">
|
||||||
<label class="publish-external-label">
|
<label class="publish-external-label">
|
||||||
{{ $t('bot.chatExternalLink') }}
|
{{ $t('bot.chatExternalLink') }}
|
||||||
@@ -1182,11 +1320,7 @@ const handleBasicInfoChange = async (
|
|||||||
</ElIcon>
|
</ElIcon>
|
||||||
{{ $t('bot.copyLink') }}
|
{{ $t('bot.copyLink') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton size="small" type="primary" @click="openPublicPage">
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
@click="openPublicPage"
|
|
||||||
>
|
|
||||||
<ElIcon class="mr-1">
|
<ElIcon class="mr-1">
|
||||||
<Link />
|
<Link />
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
@@ -1197,15 +1331,16 @@ const handleBasicInfoChange = async (
|
|||||||
<div class="publish-external-item">
|
<div class="publish-external-item">
|
||||||
<label class="publish-external-label flex items-center gap-1">
|
<label class="publish-external-label flex items-center gap-1">
|
||||||
<span>{{ $t('bot.iframeEmbedCode') }}</span>
|
<span>{{ $t('bot.iframeEmbedCode') }}</span>
|
||||||
<ElTooltip
|
<ElTooltip effect="dark" placement="top">
|
||||||
effect="dark"
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div>{{ $t('bot.embedUsageTip1') }}</div>
|
<div>{{ $t('bot.embedUsageTip1') }}</div>
|
||||||
<div class="mt-1">{{ $t('bot.embedUsageTip2') }}</div>
|
<div class="mt-1">{{ $t('bot.embedUsageTip2') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<ElIcon class="text-gray-400 hover:text-gray-600 cursor-pointer transition-colors"><InfoFilled /></ElIcon>
|
<ElIcon
|
||||||
|
class="cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<InfoFilled />
|
||||||
|
</ElIcon>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</label>
|
</label>
|
||||||
<div class="publish-external-code-preview">
|
<div class="publish-external-code-preview">
|
||||||
@@ -1447,6 +1582,12 @@ const handleBasicInfoChange = async (
|
|||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.publish-external-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.publish-external-actions {
|
.publish-external-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -1490,7 +1631,8 @@ const handleBasicInfoChange = async (
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
|
||||||
'Courier New', monospace;
|
'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|||||||
646
easyflow-ui-admin/app/src/views/ai/chatHistory/index.vue
Normal file
646
easyflow-ui-admin/app/src/views/ai/chatHistory/index.vue
Normal file
@@ -0,0 +1,646 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useEasyFlowDrawer } from '@easyflow/common-ui';
|
||||||
|
|
||||||
|
import { Search } from '@element-plus/icons-vue';
|
||||||
|
import {
|
||||||
|
ElButton,
|
||||||
|
ElDatePicker,
|
||||||
|
ElEmpty,
|
||||||
|
ElInput,
|
||||||
|
ElPagination,
|
||||||
|
ElSelect,
|
||||||
|
ElTable,
|
||||||
|
ElTableColumn,
|
||||||
|
} from 'element-plus';
|
||||||
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
|
import { api } from '#/api/request';
|
||||||
|
import ChatHistoryDetailDrawer from '#/components/chat-history/ChatHistoryDetailDrawer.vue';
|
||||||
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||||
|
|
||||||
|
const assistantList = ref<any[]>([]);
|
||||||
|
const sessions = ref<any[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const query = ref({
|
||||||
|
assistantId: undefined as number | undefined,
|
||||||
|
userAccount: '',
|
||||||
|
timeRange: [] as string[],
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
const quickRange = ref<'' | 'last7' | 'last30' | 'today'>('');
|
||||||
|
const pageState = ref({
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
const pageSizeOptions = [20, 50, 100];
|
||||||
|
const quickRangeOptions = [
|
||||||
|
{ label: '今天', value: 'today' as const },
|
||||||
|
{ label: '最近 7 天', value: 'last7' as const },
|
||||||
|
{ label: '最近 30 天', value: 'last30' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
const drawerLoading = ref(false);
|
||||||
|
const currentSession = ref<any>();
|
||||||
|
const messageList = ref<any[]>([]);
|
||||||
|
const messagePage = ref({
|
||||||
|
total: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Drawer, drawerApi] = useEasyFlowDrawer({
|
||||||
|
appendToMain: false,
|
||||||
|
class:
|
||||||
|
'!w-[820px] max-w-[calc(100vw-24px)] !border-0 !bg-[hsl(var(--glass-tint))/0.28] shadow-[0_24px_48px_-36px_hsl(var(--foreground)/0.12)] supports-[backdrop-filter]:!bg-[hsl(var(--glass-tint))/0.2]',
|
||||||
|
closable: false,
|
||||||
|
contentClass: 'p-0',
|
||||||
|
footer: false,
|
||||||
|
header: false,
|
||||||
|
modal: false,
|
||||||
|
placement: 'right',
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMoreMessages = computed(
|
||||||
|
() => messageList.value.length < messagePage.value.total,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedSessionId = computed(() =>
|
||||||
|
String(currentSession.value?.id || ''),
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([fetchAssistants(), fetchSessions()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchAssistants() {
|
||||||
|
const [, res] = await tryit(api.get)('/api/v1/bot/list', {
|
||||||
|
params: { status: 1 },
|
||||||
|
});
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
assistantList.value = (res.data || []).map((item: any) => ({
|
||||||
|
label: item.title,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSessions() {
|
||||||
|
loading.value = true;
|
||||||
|
const [, res] = await tryit(api.get)('/api/v1/chatHistory/sessions', {
|
||||||
|
params: {
|
||||||
|
assistantId: query.value.assistantId,
|
||||||
|
userAccount: query.value.userAccount || undefined,
|
||||||
|
startTime: query.value.timeRange?.[0],
|
||||||
|
endTime: query.value.timeRange?.[1],
|
||||||
|
pageNumber: query.value.pageNumber,
|
||||||
|
pageSize: query.value.pageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
sessions.value = res.data?.records || [];
|
||||||
|
pageState.value.total = res.data?.total || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSession(sessionId: number | string) {
|
||||||
|
drawerLoading.value = true;
|
||||||
|
currentSession.value =
|
||||||
|
sessions.value.find((item: any) => String(item.id) === String(sessionId)) ||
|
||||||
|
undefined;
|
||||||
|
messageList.value = [];
|
||||||
|
messagePage.value = {
|
||||||
|
total: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
};
|
||||||
|
drawerApi.open();
|
||||||
|
|
||||||
|
const [, summaryRes] = await tryit(api.get)(
|
||||||
|
`/api/v1/chatHistory/sessions/${sessionId}`,
|
||||||
|
);
|
||||||
|
if (summaryRes?.errorCode !== 0) {
|
||||||
|
drawerLoading.value = false;
|
||||||
|
drawerApi.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSession.value = summaryRes.data;
|
||||||
|
|
||||||
|
await loadMessages(true);
|
||||||
|
drawerLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMessages(reset = false) {
|
||||||
|
if (!currentSession.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextPageNumber = reset ? 1 : messagePage.value.pageNumber + 1;
|
||||||
|
const [, res] = await tryit(api.get)(
|
||||||
|
`/api/v1/chatHistory/sessions/${currentSession.value.id}/messages`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
pageNumber: nextPageNumber,
|
||||||
|
pageSize: messagePage.value.pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res?.errorCode !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizeMessages(res.data?.records || []);
|
||||||
|
messageList.value = reset
|
||||||
|
? normalized
|
||||||
|
: [...normalized, ...messageList.value];
|
||||||
|
messagePage.value.total = res.data?.total || 0;
|
||||||
|
messagePage.value.pageNumber = nextPageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessages(records: any[]) {
|
||||||
|
return [...records].reverse().map((item: any) => ({
|
||||||
|
key: String(item.id),
|
||||||
|
role: item.senderRole === 'assistant' ? 'assistant' : 'user',
|
||||||
|
content:
|
||||||
|
item.senderRole === 'assistant'
|
||||||
|
? String(item.contentText || '').replace(/^Final Answer:\s*/i, '')
|
||||||
|
: item.contentText,
|
||||||
|
created: item.created,
|
||||||
|
senderName: item.senderName,
|
||||||
|
chains: Array.isArray(item.contentPayload?.chains)
|
||||||
|
? item.contentPayload.chains.map((chain: any) =>
|
||||||
|
chain?.id
|
||||||
|
? chain
|
||||||
|
: {
|
||||||
|
...chain,
|
||||||
|
thinkingExpanded: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(pageNumber: number) {
|
||||||
|
query.value.pageNumber = pageNumber;
|
||||||
|
fetchSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePageSize(pageSize: number) {
|
||||||
|
query.value.pageNumber = 1;
|
||||||
|
query.value.pageSize = pageSize;
|
||||||
|
fetchSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const time = new Date(value);
|
||||||
|
if (Number.isNaN(time.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const year = time.getFullYear();
|
||||||
|
const month = String(time.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(time.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(time.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(time.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
query.value.pageNumber = 1;
|
||||||
|
fetchSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyQuickRange(range: 'last7' | 'last30' | 'today') {
|
||||||
|
quickRange.value = range;
|
||||||
|
query.value.timeRange = resolveQuickRange(range);
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTimeRangeChange(value?: string[]) {
|
||||||
|
quickRange.value = '';
|
||||||
|
query.value.timeRange = normalizeTimeRange(value);
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveQuickRange(range: 'last7' | 'last30' | 'today') {
|
||||||
|
const now = new Date();
|
||||||
|
const end = endOfDay(now);
|
||||||
|
let start = startOfDay(now);
|
||||||
|
|
||||||
|
if (range === 'last7') {
|
||||||
|
start = startOfDay(addDays(now, -6));
|
||||||
|
} else if (range === 'last30') {
|
||||||
|
start = startOfDay(addDays(now, -29));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [formatDateTime(start), formatDateTime(end)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTimeRange(value?: string[]) {
|
||||||
|
if (!value?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const [startValue, endValue] = value;
|
||||||
|
if (!startValue || !endValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
formatDateTime(startOfDay(new Date(startValue))),
|
||||||
|
formatDateTime(endOfDay(new Date(endValue))),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function startOfDay(date: Date) {
|
||||||
|
const value = new Date(date);
|
||||||
|
value.setHours(0, 0, 0, 0);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endOfDay(date: Date) {
|
||||||
|
const value = new Date(date);
|
||||||
|
value.setHours(23, 59, 59, 0);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDays(date: Date, days: number) {
|
||||||
|
const value = new Date(date);
|
||||||
|
value.setDate(value.getDate() + days);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: Date) {
|
||||||
|
const year = value.getFullYear();
|
||||||
|
const month = String(value.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(value.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(value.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(value.getMinutes()).padStart(2, '0');
|
||||||
|
const second = String(value.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetail() {
|
||||||
|
drawerApi.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="chat-history-page flex h-full flex-col gap-6 p-6">
|
||||||
|
<ListPageShell class="flex-1" :content-padding="20">
|
||||||
|
<template #filters>
|
||||||
|
<div class="chat-history-page__filters">
|
||||||
|
<ElSelect
|
||||||
|
v-model="query.assistantId"
|
||||||
|
clearable
|
||||||
|
placeholder="筛选聊天助手"
|
||||||
|
:options="assistantList"
|
||||||
|
class="chat-history-page__filter-control is-select"
|
||||||
|
@change="handleSearch"
|
||||||
|
/>
|
||||||
|
<ElInput
|
||||||
|
v-model="query.userAccount"
|
||||||
|
class="chat-history-page__filter-control is-input"
|
||||||
|
placeholder="搜索聊天用户"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<div class="chat-history-page__quick-ranges">
|
||||||
|
<ElButton
|
||||||
|
v-for="item in quickRangeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:type="quickRange === item.value ? 'primary' : 'default'"
|
||||||
|
class="chat-history-page__quick-range-button"
|
||||||
|
@click="applyQuickRange(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElDatePicker
|
||||||
|
v-model="query.timeRange"
|
||||||
|
class="chat-history-page__filter-control is-range"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
start-placeholder="开始时间"
|
||||||
|
end-placeholder="结束时间"
|
||||||
|
@change="handleTimeRangeChange"
|
||||||
|
/>
|
||||||
|
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="chat-history-page__content">
|
||||||
|
<div v-if="sessions.length > 0" class="chat-history-page__table-shell">
|
||||||
|
<ElTable
|
||||||
|
:data="sessions"
|
||||||
|
:current-row-key="selectedSessionId || undefined"
|
||||||
|
row-key="id"
|
||||||
|
class="chat-history-page__table"
|
||||||
|
height="100%"
|
||||||
|
highlight-current-row
|
||||||
|
empty-text=""
|
||||||
|
@row-click="(row) => openSession(row.id)"
|
||||||
|
>
|
||||||
|
<ElTableColumn label="会话信息" min-width="280">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="chat-history-page__session-cell">
|
||||||
|
<div class="chat-history-page__session-title">
|
||||||
|
{{ row.title || '未命名会话' }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="row.lastMessagePreview"
|
||||||
|
class="chat-history-page__session-preview"
|
||||||
|
>
|
||||||
|
{{ row.lastMessagePreview }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
|
||||||
|
<ElTableColumn label="聊天助手" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="chat-history-page__assistant-chip">
|
||||||
|
{{ row.assistantName || '聊天助手' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
|
||||||
|
<ElTableColumn prop="userAccount" label="聊天用户" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="chat-history-page__user-cell">
|
||||||
|
{{ row.userAccount || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
|
||||||
|
<ElTableColumn label="消息数" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="chat-history-page__count-pill">
|
||||||
|
{{ row.messageCount || 0 }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
|
||||||
|
<ElTableColumn label="最近活跃" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="chat-history-page__time-cell">
|
||||||
|
{{ formatTime(row.lastMessageAt || row.accessAt) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
|
||||||
|
<ElTableColumn width="108" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ElButton
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
class="chat-history-page__detail-action"
|
||||||
|
@click.stop="openSession(row.id)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
|
</ElTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="chat-history-page__empty">
|
||||||
|
<ElEmpty description="暂无聊天历史" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="pageState.total > 0" class="chat-history-page__pagination">
|
||||||
|
<ElPagination
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:current-page="query.pageNumber"
|
||||||
|
:page-size="query.pageSize"
|
||||||
|
:page-sizes="pageSizeOptions"
|
||||||
|
:total="pageState.total"
|
||||||
|
@current-change="changePage"
|
||||||
|
@size-change="changePageSize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListPageShell>
|
||||||
|
|
||||||
|
<Drawer>
|
||||||
|
<ChatHistoryDetailDrawer
|
||||||
|
:visible="true"
|
||||||
|
:loading="drawerLoading"
|
||||||
|
:session="currentSession"
|
||||||
|
:messages="messageList"
|
||||||
|
:has-more="hasMoreMessages"
|
||||||
|
:on-load-more="() => loadMessages(false)"
|
||||||
|
@close="closeDetail"
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-history-page__filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__filter-control {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__filter-control.is-select,
|
||||||
|
.chat-history-page__filter-control.is-input {
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__filter-control.is-range {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__quick-ranges {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__quick-range-button {
|
||||||
|
min-width: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content {
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__table-shell {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid hsl(var(--glass-border) / 0.42);
|
||||||
|
border-radius: 24px;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--glass-border) / 0.28) 0%,
|
||||||
|
hsl(var(--glass-tint) / 0.4) 14%,
|
||||||
|
hsl(var(--surface-panel) / 0.94) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.54),
|
||||||
|
0 24px 42px -36px hsl(var(--foreground) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__table {
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__session-cell {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__session-title {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__session-preview {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
word-break: break-word;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__assistant-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid hsl(var(--glass-border) / 0.48);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--glass-tint) / 0.76);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--nav-item-active-foreground));
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__user-cell,
|
||||||
|
.chat-history-page__time-cell {
|
||||||
|
font-size: 13px;
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__count-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 42px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.88);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__detail-action {
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__empty {
|
||||||
|
display: flex;
|
||||||
|
min-height: 360px;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content :deep(.el-table),
|
||||||
|
.chat-history-page__content :deep(.el-table__inner-wrapper),
|
||||||
|
.chat-history-page__content :deep(.el-table__body-wrapper),
|
||||||
|
.chat-history-page__content :deep(.el-scrollbar),
|
||||||
|
.chat-history-page__content :deep(.el-scrollbar__wrap),
|
||||||
|
.chat-history-page__content :deep(.el-scrollbar__view) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content :deep(.el-table::before) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content :deep(.el-table tr) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content :deep(.el-table th.el-table__cell) {
|
||||||
|
height: 48px;
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.54);
|
||||||
|
border-bottom-color: hsl(var(--divider-faint) / 0.28);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content :deep(.el-table td.el-table__cell) {
|
||||||
|
padding: 14px 0;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: hsl(var(--divider-faint) / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content
|
||||||
|
:deep(.el-table__body tr:hover > td.el-table__cell) {
|
||||||
|
background: hsl(var(--primary) / 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__content
|
||||||
|
:deep(.el-table__body tr.current-row > td.el-table__cell) {
|
||||||
|
background: hsl(var(--primary) / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.chat-history-page__filter-control.is-range {
|
||||||
|
width: min(100%, 360px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-history-page {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__filters {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-page__filter-control.is-select,
|
||||||
|
.chat-history-page__filter-control.is-input,
|
||||||
|
.chat-history-page__filter-control.is-range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,279 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import type { ChatThinkingBlockProps } from './types';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChatThinkingBlock',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ChatThinkingBlockProps>(), {
|
||||||
|
content: '',
|
||||||
|
disabled: false,
|
||||||
|
emptyBehavior: 'hide',
|
||||||
|
expanded: false,
|
||||||
|
label: '',
|
||||||
|
readonly: false,
|
||||||
|
status: 'end',
|
||||||
|
summary: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:expanded': [boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const normalizedContent = computed(() =>
|
||||||
|
String(props.content || '')
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/^\s*\n+/, '')
|
||||||
|
.trimEnd(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldRender = computed(
|
||||||
|
() =>
|
||||||
|
normalizedContent.value.length > 0 || props.emptyBehavior === 'placeholder',
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandedModel = computed({
|
||||||
|
get: () => props.expanded,
|
||||||
|
set: (value: boolean) => emit('update:expanded', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedLabel = computed(() => {
|
||||||
|
if (props.label) {
|
||||||
|
return props.label;
|
||||||
|
}
|
||||||
|
if (props.status === 'thinking') {
|
||||||
|
return '思考中';
|
||||||
|
}
|
||||||
|
if (props.status === 'error') {
|
||||||
|
return '思考异常';
|
||||||
|
}
|
||||||
|
return '已思考';
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedSummary = computed(() => {
|
||||||
|
if (props.summary) {
|
||||||
|
return props.summary;
|
||||||
|
}
|
||||||
|
const source = normalizedContent.value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find(Boolean);
|
||||||
|
if (!source) {
|
||||||
|
return props.emptyBehavior === 'placeholder' ? '暂无思考内容' : '';
|
||||||
|
}
|
||||||
|
return source.length > 56 ? `${source.slice(0, 56)}...` : source;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canToggle = computed(
|
||||||
|
() => !props.disabled && normalizedContent.value.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleExpanded() {
|
||||||
|
if (!canToggle.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expandedModel.value = !expandedModel.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="shouldRender"
|
||||||
|
class="chat-thinking-block"
|
||||||
|
:class="[
|
||||||
|
`is-${status}`,
|
||||||
|
{
|
||||||
|
'is-disabled': disabled,
|
||||||
|
'is-expanded': expandedModel,
|
||||||
|
'is-readonly': readonly,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chat-thinking-block__trigger"
|
||||||
|
:disabled="!canToggle"
|
||||||
|
@click="toggleExpanded"
|
||||||
|
>
|
||||||
|
<span class="chat-thinking-block__leading">
|
||||||
|
<span class="chat-thinking-block__indicator" aria-hidden="true"></span>
|
||||||
|
<span class="chat-thinking-block__label">{{ computedLabel }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!expandedModel && computedSummary"
|
||||||
|
class="chat-thinking-block__summary"
|
||||||
|
>
|
||||||
|
{{ computedSummary }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="chat-thinking-block__chevron"
|
||||||
|
:class="{ 'is-open': expandedModel }"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="chat-thinking-block__body-transition">
|
||||||
|
<div
|
||||||
|
v-if="expandedModel && normalizedContent"
|
||||||
|
class="chat-thinking-block__body"
|
||||||
|
>
|
||||||
|
<div class="chat-thinking-block__content">
|
||||||
|
{{ normalizedContent }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-thinking-block {
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.18);
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--glass-tint) / 0.48) 0%,
|
||||||
|
hsl(var(--surface-panel) / 0.74) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.24),
|
||||||
|
0 10px 24px -24px hsl(var(--foreground) / 0.18);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
transition: background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:not(:disabled):hover {
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__leading {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__indicator {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--text-muted) / 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-thinking .chat-thinking-block__indicator {
|
||||||
|
background: hsl(var(--primary) / 0.82);
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
animation: chat-thinking-pulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-error .chat-thinking-block__indicator {
|
||||||
|
background: hsl(var(--destructive) / 0.86);
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--destructive) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__summary {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__chevron {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-right: 1.5px solid hsl(var(--text-muted));
|
||||||
|
border-bottom: 1.5px solid hsl(var(--text-muted));
|
||||||
|
transform: rotate(45deg) translateY(-1px);
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__chevron.is-open {
|
||||||
|
transform: rotate(225deg) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: hsl(var(--surface-panel) / 0.72);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.68;
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-disabled {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body-transition-enter-active,
|
||||||
|
.chat-thinking-block__body-transition-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.18s ease,
|
||||||
|
transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body-transition-enter-from,
|
||||||
|
.chat-thinking-block__body-transition-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chat-thinking-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 7px hsl(var(--primary) / 0.04);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as ChatThinkingBlock } from './ChatThinkingBlock.vue';
|
||||||
|
export type {
|
||||||
|
ChatThinkingBlockProps,
|
||||||
|
ChatThinkingBlockStatus,
|
||||||
|
} from './types';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type ChatThinkingBlockStatus = 'end' | 'error' | 'thinking';
|
||||||
|
|
||||||
|
export interface ChatThinkingBlockProps {
|
||||||
|
content?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
emptyBehavior?: 'hide' | 'placeholder';
|
||||||
|
expanded?: boolean;
|
||||||
|
label?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
status?: ChatThinkingBlockStatus;
|
||||||
|
summary?: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './api-component';
|
export * from './api-component';
|
||||||
export * from './captcha';
|
export * from './captcha';
|
||||||
|
export * from './chat-thinking';
|
||||||
export * from './col-page';
|
export * from './col-page';
|
||||||
export * from './count-to';
|
export * from './count-to';
|
||||||
export * from './ellipsis-text';
|
export * from './ellipsis-text';
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3.5h10v6.1a1.4 1.4 0 0 1-1.4 1.4H8.1l-2.2 2V11H4.4A1.4 1.4 0 0 1 3 9.6V3.5Z"/>
|
||||||
|
<path d="M5.4 6h3.1M5.4 8.1h1.9"/>
|
||||||
|
<circle cx="10.4" cy="7.4" r="1.8"/>
|
||||||
|
<path d="M10.4 6.6v.95l.72.48"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 394 B |
@@ -10,6 +10,7 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
|
|||||||
const SvgCardIcon = createIconifyIcon('svg:card');
|
const SvgCardIcon = createIconifyIcon('svg:card');
|
||||||
const SvgBellIcon = createIconifyIcon('svg:bell');
|
const SvgBellIcon = createIconifyIcon('svg:bell');
|
||||||
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
||||||
|
const SvgChatHistoryIcon = createIconifyIcon('svg:chat-history');
|
||||||
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
||||||
const SvgGithubIcon = createIconifyIcon('svg:github');
|
const SvgGithubIcon = createIconifyIcon('svg:github');
|
||||||
const SvgGoogleIcon = createIconifyIcon('svg:google');
|
const SvgGoogleIcon = createIconifyIcon('svg:google');
|
||||||
@@ -44,6 +45,7 @@ export {
|
|||||||
SvgBellIcon,
|
SvgBellIcon,
|
||||||
SvgCakeIcon,
|
SvgCakeIcon,
|
||||||
SvgCardIcon,
|
SvgCardIcon,
|
||||||
|
SvgChatHistoryIcon,
|
||||||
SvgDataCenterIcon,
|
SvgDataCenterIcon,
|
||||||
SvgDepartmentIcon,
|
SvgDepartmentIcon,
|
||||||
SvgDingDingIcon,
|
SvgDingDingIcon,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IconifyIconStructure } from '@easyflow-core/icons';
|
import type { IconifyIconStructure } from '@easyflow-core/icons';
|
||||||
|
|
||||||
import { addIcon } from '@easyflow-core/icons';
|
import { addIcon } from '@easyflow-core/icons';
|
||||||
|
import chatHistorySvg from './icons/chat-history.svg?raw';
|
||||||
|
|
||||||
let loaded = false;
|
let loaded = false;
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
@@ -39,6 +40,14 @@ function parseSvg(svgData: string): IconifyIconStructure {
|
|||||||
* <Icon icon="svg:avatar"></Icon>
|
* <Icon icon="svg:avatar"></Icon>
|
||||||
*/
|
*/
|
||||||
async function loadSvgIcons() {
|
async function loadSvgIcons() {
|
||||||
|
addIcon('svg:chat-history', {
|
||||||
|
...parseSvg(
|
||||||
|
typeof chatHistorySvg === 'object'
|
||||||
|
? chatHistorySvg.default
|
||||||
|
: chatHistorySvg,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const svgEagers = import.meta.glob('./icons/**', {
|
const svgEagers = import.meta.glob('./icons/**', {
|
||||||
eager: true,
|
eager: true,
|
||||||
query: '?raw',
|
query: '?raw',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createApp, watchEffect } from 'vue';
|
import { createApp, watchEffect } from 'vue';
|
||||||
import { BubbleList, Sender, Thinking, XMarkdown } from 'vue-element-plus-x';
|
import { BubbleList, Sender, XMarkdown } from 'vue-element-plus-x';
|
||||||
|
|
||||||
import { registerAccessDirective } from '@easyflow/access';
|
import { registerAccessDirective } from '@easyflow/access';
|
||||||
import { registerLoadingDirective } from '@easyflow/common-ui';
|
import { registerLoadingDirective } from '@easyflow/common-ui';
|
||||||
@@ -41,7 +41,6 @@ async function bootstrap(namespace: string) {
|
|||||||
app.component('ElBubbleList', BubbleList);
|
app.component('ElBubbleList', BubbleList);
|
||||||
app.component('ElSender', Sender);
|
app.component('ElSender', Sender);
|
||||||
app.component('ElXMarkdown', XMarkdown);
|
app.component('ElXMarkdown', XMarkdown);
|
||||||
app.component('ElThinking', Thinking);
|
|
||||||
|
|
||||||
// 注册EasyFlow提供的v-loading和v-spinning指令
|
// 注册EasyFlow提供的v-loading和v-spinning指令
|
||||||
registerLoadingDirective(app, {
|
registerLoadingDirective(app, {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ChatThinkingBlock } from '@easyflow/common-ui';
|
||||||
import { IconifyIcon } from '@easyflow/icons';
|
import { IconifyIcon } from '@easyflow/icons';
|
||||||
import { useUserStore } from '@easyflow/stores';
|
import { useUserStore } from '@easyflow/stores';
|
||||||
|
|
||||||
@@ -48,13 +49,14 @@ function getUserAvatar() {
|
|||||||
v-for="(chain, index) in item.chains"
|
v-for="(chain, index) in item.chains"
|
||||||
:key="chain.id || index"
|
:key="chain.id || index"
|
||||||
>
|
>
|
||||||
<ElThinking
|
<ChatThinkingBlock
|
||||||
v-if="!('id' in chain)"
|
v-if="!('id' in chain)"
|
||||||
v-model="chain.thinlCollapse"
|
v-model:expanded="chain.thinkingExpanded"
|
||||||
:content="chain.reasoning_content"
|
:content="chain.reasoning_content"
|
||||||
:status="chain.thinkingStatus"
|
:status="chain.thinkingStatus"
|
||||||
|
class="chat-thinking-block-item"
|
||||||
/>
|
/>
|
||||||
<ElCollapse v-else class="mb-2">
|
<ElCollapse v-else class="chat-tool-panel">
|
||||||
<ElCollapseItem :title="chain.name" :name="chain.id">
|
<ElCollapseItem :title="chain.name" :name="chain.id">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center gap-2 pl-5">
|
<div class="flex items-center gap-2 pl-5">
|
||||||
@@ -90,41 +92,6 @@ function getUserAvatar() {
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <ElThinking
|
|
||||||
v-if="item.reasoning_content"
|
|
||||||
v-model="item.thinlCollapse"
|
|
||||||
:content="item.reasoning_content"
|
|
||||||
:status="item.thinkingStatus"
|
|
||||||
/> -->
|
|
||||||
<!-- <ElCollapse v-if="item.tools" class="mb-2">
|
|
||||||
<ElCollapseItem
|
|
||||||
class="mb-2"
|
|
||||||
v-for="tool in item.tools"
|
|
||||||
:key="tool.id"
|
|
||||||
:title="tool.name"
|
|
||||||
:name="tool.id"
|
|
||||||
>
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center gap-2 pl-5">
|
|
||||||
<ElIcon size="16">
|
|
||||||
<IconifyIcon icon="svg:wrench" />
|
|
||||||
</ElIcon>
|
|
||||||
<span>{{ tool.name }}</span>
|
|
||||||
<template v-if="tool.status === 'TOOL_CALL'">
|
|
||||||
<ElIcon size="16">
|
|
||||||
<IconifyIcon icon="svg:spinner" />
|
|
||||||
</ElIcon>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<ElIcon size="16" color="var(--el-color-success)">
|
|
||||||
<CircleCheck />
|
|
||||||
</ElIcon>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<ShowJson :value="tool.result" />
|
|
||||||
</ElCollapseItem>
|
|
||||||
</ElCollapse> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -162,23 +129,30 @@ function getUserAvatar() {
|
|||||||
--bubble-content-max-width: 100%;
|
--bubble-content-max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-thinking) {
|
.chat-thinking-block-item {
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-thinking .content-wrapper) {
|
|
||||||
--el-thinking-content-wrapper-width: 100%;
|
|
||||||
|
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-collapse) {
|
:deep(.chat-tool-panel.el-collapse) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--el-collapse-border-color);
|
border: 1px solid hsl(var(--divider-faint) / 0.26);
|
||||||
border-radius: 8px;
|
border-radius: 14px;
|
||||||
|
background: hsl(var(--surface-panel) / 0.7);
|
||||||
|
box-shadow: inset 0 1px 0 hsl(var(--glass-border) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-collapse-item__content) {
|
:deep(.chat-tool-panel .el-collapse-item__wrap) {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item__header) {
|
||||||
|
min-height: 44px;
|
||||||
|
padding-right: 14px;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: hsl(var(--divider-faint) / 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-tool-panel .el-collapse-item__content) {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -49,14 +49,16 @@ defineExpose({
|
|||||||
|
|
||||||
function getSessionList(resetSession = false) {
|
function getSessionList(resetSession = false) {
|
||||||
api
|
api
|
||||||
.get('/userCenter/botConversation/list', {
|
.get('/userCenter/chatHistory/sessions', {
|
||||||
params: {
|
params: {
|
||||||
botId: props.bot.id,
|
assistantId: props.bot.id,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 100,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
sessionList.value = res.data;
|
sessionList.value = res.data.records || [];
|
||||||
if (resetSession) {
|
if (resetSession) {
|
||||||
currentSession.value = {};
|
currentSession.value = {};
|
||||||
}
|
}
|
||||||
@@ -92,15 +94,27 @@ function clickSession(session: any) {
|
|||||||
}
|
}
|
||||||
function getMessageList() {
|
function getMessageList() {
|
||||||
api
|
api
|
||||||
.get('/userCenter/botMessage/getMessages', {
|
.get(`/userCenter/chatHistory/sessions/${currentSession.value.id}/messages`, {
|
||||||
params: {
|
params: { pageNumber: 1, pageSize: 100 },
|
||||||
botId: props.bot.id,
|
|
||||||
conversationId: currentSession.value.id,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
props.onMessageList?.(res.data);
|
const records = Array.isArray(res.data?.records) ? [...res.data.records] : [];
|
||||||
|
props.onMessageList?.(
|
||||||
|
records.reverse().map((item: any) => ({
|
||||||
|
key: String(item.id),
|
||||||
|
role: item.senderRole === 'assistant' ? 'assistant' : 'user',
|
||||||
|
content:
|
||||||
|
item.senderRole === 'assistant'
|
||||||
|
? String(item.contentText || '').replace(/^Final Answer:\s*/i, '')
|
||||||
|
: item.contentText,
|
||||||
|
placement: item.senderRole === 'assistant' ? 'start' : 'end',
|
||||||
|
created: item.created,
|
||||||
|
chains: Array.isArray(item.contentPayload?.chains)
|
||||||
|
? item.contentPayload.chains
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,12 +139,8 @@ const updateLoading = ref(false);
|
|||||||
function updateTitle() {
|
function updateTitle() {
|
||||||
updateLoading.value = true;
|
updateLoading.value = true;
|
||||||
api
|
api
|
||||||
.get('/userCenter/botConversation/updateConversation', {
|
.post(`/userCenter/chatHistory/sessions/${currentSession.value.id}/rename`, {
|
||||||
params: {
|
|
||||||
botId: props.bot.id,
|
|
||||||
conversationId: currentSession.value.id,
|
|
||||||
title: currentSession.value.title,
|
title: currentSession.value.title,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
updateLoading.value = false;
|
updateLoading.value = false;
|
||||||
@@ -150,12 +160,7 @@ function remove(row: any) {
|
|||||||
if (action === 'confirm') {
|
if (action === 'confirm') {
|
||||||
instance.confirmButtonLoading = true;
|
instance.confirmButtonLoading = true;
|
||||||
api
|
api
|
||||||
.get('/userCenter/botConversation/deleteConversation', {
|
.post(`/userCenter/chatHistory/sessions/${row.id}/delete`)
|
||||||
params: {
|
|
||||||
botId: props.bot.id,
|
|
||||||
conversationId: row.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
instance.confirmButtonLoading = false;
|
instance.confirmButtonLoading = false;
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
@@ -175,6 +180,11 @@ function remove(row: any) {
|
|||||||
},
|
},
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRenameDialog(row: any) {
|
||||||
|
currentSession.value = { ...row };
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -230,7 +240,7 @@ function remove(row: any) {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ formatCreatedTime(conversation.created) }}
|
{{ formatCreatedTime(conversation.lastMessageAt || conversation.accessAt || conversation.created) }}
|
||||||
</span>
|
</span>
|
||||||
<ElDropdown
|
<ElDropdown
|
||||||
:class="
|
:class="
|
||||||
@@ -249,7 +259,7 @@ function remove(row: any) {
|
|||||||
@mouseenter="handleMouseEvent(conversation.id)"
|
@mouseenter="handleMouseEvent(conversation.id)"
|
||||||
@mouseleave="handleMouseEvent()"
|
@mouseleave="handleMouseEvent()"
|
||||||
>
|
>
|
||||||
<ElDropdownItem @click="dialogVisible = true">
|
<ElDropdownItem @click="openRenameDialog(conversation)">
|
||||||
<ElButton link :icon="Edit">编辑</ElButton>
|
<ElButton link :icon="Edit">编辑</ElButton>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
<ElDropdownItem>
|
<ElDropdownItem>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { ChatThinkingBlockStatus } from '@easyflow/common-ui';
|
||||||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
|
||||||
|
|
||||||
import { inject, ref } from 'vue';
|
import { inject, ref } from 'vue';
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ import ChatFileUploader from '#/components/upload/ChatFileUploader.vue';
|
|||||||
|
|
||||||
type Think = {
|
type Think = {
|
||||||
reasoning_content?: string;
|
reasoning_content?: string;
|
||||||
thinkingStatus?: ThinkingStatus;
|
thinkingExpanded?: boolean;
|
||||||
thinlCollapse?: boolean;
|
thinkingStatus?: ChatThinkingBlockStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Tool = {
|
type Tool = {
|
||||||
@@ -152,7 +152,7 @@ function sendMessage() {
|
|||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
chains.push({
|
chains.push({
|
||||||
thinkingStatus: 'thinking',
|
thinkingStatus: 'thinking',
|
||||||
thinlCollapse: true,
|
thinkingExpanded: false,
|
||||||
reasoning_content: delta,
|
reasoning_content: delta,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -92,20 +92,6 @@ const coreRoutes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'ChatHistoryShare',
|
|
||||||
path: '/share/:id',
|
|
||||||
component: () => import('#/views/chatHistory/share/index.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '分享',
|
|
||||||
noBasicLayout: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
hideInTab: true,
|
|
||||||
hideInBreadcrumb: true,
|
|
||||||
ignoreAccess: true,
|
|
||||||
loaded: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export { coreRoutes, fallbackNotFoundRoute };
|
export { coreRoutes, fallbackNotFoundRoute };
|
||||||
|
|||||||
@@ -28,15 +28,20 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
icon: 'svg:chat-history',
|
icon: 'svg:chat-history',
|
||||||
order: 80,
|
order: 80,
|
||||||
title: '聊天记录',
|
title: '聊天历史',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ChatHistoryDetails',
|
name: 'ChatHistoryDetails',
|
||||||
path: '/chatHistory/:id',
|
path: '/chatHistory/:id',
|
||||||
component: () => import('#/views/chatHistory/details/index.vue'),
|
redirect: (to) => ({
|
||||||
|
path: '/chatHistory',
|
||||||
|
query: {
|
||||||
|
sessionId: String(to.params.id || ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
meta: {
|
meta: {
|
||||||
title: '聊天记录',
|
title: '聊天历史',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
hideInTab: true,
|
hideInTab: true,
|
||||||
hideInBreadcrumb: true,
|
hideInBreadcrumb: true,
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
import { IconifyIcon } from '@easyflow/icons';
|
|
||||||
import { copyToClipboard } from '@easyflow/utils';
|
|
||||||
|
|
||||||
import { ArrowLeft, Delete, MoreFilled } from '@element-plus/icons-vue';
|
|
||||||
import {
|
|
||||||
ElButton,
|
|
||||||
ElContainer,
|
|
||||||
ElDropdown,
|
|
||||||
ElDropdownItem,
|
|
||||||
ElHeader,
|
|
||||||
ElMain,
|
|
||||||
ElMessage,
|
|
||||||
} from 'element-plus';
|
|
||||||
import { tryit } from 'radash';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
|
||||||
import { ChatBubbleList } from '#/components/chat';
|
|
||||||
import { router } from '#/router';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const ids = reactive({
|
|
||||||
botId: '',
|
|
||||||
conversationId: '',
|
|
||||||
});
|
|
||||||
const conversationInfo = ref<any>();
|
|
||||||
const messageList = ref<any[]>([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (route.params.id) {
|
|
||||||
ids.conversationId = route.params.id as string;
|
|
||||||
getConversationDetails();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getConversationDetails() {
|
|
||||||
api
|
|
||||||
.get('/userCenter/botConversation/detail', {
|
|
||||||
params: {
|
|
||||||
id: ids.conversationId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
conversationInfo.value = res.data;
|
|
||||||
ids.botId = res.data.botId;
|
|
||||||
getMessageList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getMessageList() {
|
|
||||||
api
|
|
||||||
.get('/userCenter/botMessage/getMessages', {
|
|
||||||
params: ids,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
messageList.value = res.data;
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function handleShare() {
|
|
||||||
const shareLink = import.meta.env.DEV
|
|
||||||
? `${location.origin}/share/${ids.conversationId}`
|
|
||||||
: `${location.origin}/#/share/${ids.conversationId}`;
|
|
||||||
const { success, error } = await copyToClipboard(shareLink);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
ElMessage.success('分享链接复制成功!');
|
|
||||||
} else {
|
|
||||||
ElMessage.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function handleDelete() {
|
|
||||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
|
||||||
id: ids.conversationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.errorCode === 0) {
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ElContainer class="bg-background h-full">
|
|
||||||
<ElHeader height="100px" class="border-border border-b !pr-10">
|
|
||||||
<div class="flex h-full w-full items-center justify-between">
|
|
||||||
<!-- Left -->
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<ElButton
|
|
||||||
link
|
|
||||||
style="font-size: 20px"
|
|
||||||
:icon="ArrowLeft"
|
|
||||||
@click="router.back()"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-lg font-medium">{{
|
|
||||||
conversationInfo?.title
|
|
||||||
}}</span>
|
|
||||||
<div
|
|
||||||
v-if="conversationInfo?.bot.title"
|
|
||||||
class="text-foreground/70 rounded bg-[var(--el-fill-color-light)] p-1 text-xs"
|
|
||||||
>
|
|
||||||
{{ conversationInfo.bot.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-foreground/50 text-sm">{{
|
|
||||||
conversationInfo?.created
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right -->
|
|
||||||
<div class="flex items-center gap-5">
|
|
||||||
<ElButton link style="font-size: 20px" @click="handleShare">
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="svg:share" />
|
|
||||||
</template>
|
|
||||||
</ElButton>
|
|
||||||
<ElDropdown>
|
|
||||||
<ElButton link style="font-size: 20px" :icon="MoreFilled" />
|
|
||||||
|
|
||||||
<template #dropdown>
|
|
||||||
<ElDropdownItem
|
|
||||||
style="color: var(--el-color-danger)"
|
|
||||||
:icon="Delete"
|
|
||||||
@click="handleDelete"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</ElDropdownItem>
|
|
||||||
</template>
|
|
||||||
</ElDropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ElHeader>
|
|
||||||
<ElMain class="relative" v-loading="loading">
|
|
||||||
<div
|
|
||||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2"
|
|
||||||
>
|
|
||||||
<ChatBubbleList
|
|
||||||
:bot="conversationInfo?.bot"
|
|
||||||
:messages="messageList"
|
|
||||||
:editable="false"
|
|
||||||
:open-editor="() => {}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElMain>
|
|
||||||
</ElContainer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
:deep(.el-bubble-list) {
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
|
||||||
--bubble-content-max-width: calc(100% - 52px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,177 +1,367 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Delete, MoreFilled, Search } from '@element-plus/icons-vue';
|
import { Delete, Edit, Search } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
ElContainer,
|
ElContainer,
|
||||||
ElDropdown,
|
ElDrawer,
|
||||||
ElDropdownItem,
|
ElEmpty,
|
||||||
ElDropdownMenu,
|
|
||||||
ElHeader,
|
ElHeader,
|
||||||
ElInput,
|
ElInput,
|
||||||
ElMain,
|
ElMain,
|
||||||
ElMessage,
|
ElMessage,
|
||||||
|
ElMessageBox,
|
||||||
|
ElPagination,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSpace,
|
ElSpace,
|
||||||
ElText,
|
ElTag,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
import { tryit } from 'radash';
|
import { tryit } from 'radash';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import PageData from '#/components/page/PageData.vue';
|
import { ChatBubbleList } from '#/components/chat';
|
||||||
|
|
||||||
const listTitles = ['聊天助理名称', '话题', '创建时间', '操作'];
|
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const assistantList = ref<any[]>([]);
|
|
||||||
const queryParams = ref<any>({});
|
|
||||||
const pageRef = ref();
|
|
||||||
|
|
||||||
onMounted(() => {
|
const assistantList = ref<any[]>([]);
|
||||||
getAssistantList();
|
const sessions = ref<any[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const queryParams = ref({
|
||||||
|
assistantId: undefined as number | undefined,
|
||||||
|
keyword: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getAssistantList() {
|
const pageState = ref({
|
||||||
api
|
total: 0,
|
||||||
.get('/userCenter/bot/list', {
|
});
|
||||||
params: { ...queryParams.value, status: 1 },
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
assistantList.value = res.data.map((item: any) => ({
|
|
||||||
label: item.title,
|
|
||||||
value: item.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function search() {
|
|
||||||
pageRef.value.setQuery({ ...queryParams.value, status: 1 });
|
|
||||||
}
|
|
||||||
function toDetail(record: any) {
|
|
||||||
router.push({ path: `/chatHistory/${record.id}` });
|
|
||||||
}
|
|
||||||
async function handleDelete(id: string) {
|
|
||||||
const [, res] = await tryit(api.post)('/userCenter/botConversation/remove', {
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.errorCode === 0) {
|
const drawerVisible = ref(false);
|
||||||
search();
|
const drawerLoading = ref(false);
|
||||||
ElMessage.success('删除成功');
|
const currentSession = ref<any>();
|
||||||
|
const messageList = ref<any[]>([]);
|
||||||
|
const messagePage = ref({
|
||||||
|
total: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredSessions = computed(() => {
|
||||||
|
const keyword = queryParams.value.keyword.trim().toLowerCase();
|
||||||
|
if (!keyword) {
|
||||||
|
return sessions.value;
|
||||||
}
|
}
|
||||||
|
return sessions.value.filter((item) => {
|
||||||
|
const title = String(item.title || '').toLowerCase();
|
||||||
|
const preview = String(item.lastMessagePreview || '').toLowerCase();
|
||||||
|
const assistantName = String(item.assistantName || '').toLowerCase();
|
||||||
|
return title.includes(keyword) || preview.includes(keyword) || assistantName.includes(keyword);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([fetchAssistants(), fetchSessions()]);
|
||||||
|
const sessionId = route.query.sessionId ? String(route.query.sessionId) : '';
|
||||||
|
if (sessionId) {
|
||||||
|
await openSession(sessionId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.sessionId,
|
||||||
|
async (sessionId) => {
|
||||||
|
if (!sessionId) {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
currentSession.value = undefined;
|
||||||
|
messageList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentSession.value || String(currentSession.value.id) !== String(sessionId)) {
|
||||||
|
await openSession(String(sessionId));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetchAssistants() {
|
||||||
|
const [, res] = await tryit(api.get)('/userCenter/bot/list', {
|
||||||
|
params: { status: 1 },
|
||||||
|
});
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
assistantList.value = (res.data || []).map((item: any) => ({
|
||||||
|
label: item.title,
|
||||||
|
value: item.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSessions() {
|
||||||
|
loading.value = true;
|
||||||
|
const [, res] = await tryit(api.get)('/userCenter/chatHistory/sessions', {
|
||||||
|
params: {
|
||||||
|
assistantId: queryParams.value.assistantId,
|
||||||
|
pageNumber: queryParams.value.pageNumber,
|
||||||
|
pageSize: queryParams.value.pageSize,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
loading.value = false;
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
sessions.value = res.data?.records || [];
|
||||||
|
pageState.value.total = res.data?.total || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSession(sessionId: string | number) {
|
||||||
|
drawerLoading.value = true;
|
||||||
|
const [, summaryRes] = await tryit(api.get)(`/userCenter/chatHistory/sessions/${sessionId}`);
|
||||||
|
if (summaryRes?.errorCode !== 0) {
|
||||||
|
drawerLoading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentSession.value = summaryRes.data;
|
||||||
|
messageList.value = [];
|
||||||
|
messagePage.value = {
|
||||||
|
total: 0,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
};
|
||||||
|
drawerVisible.value = true;
|
||||||
|
await loadMessages(true);
|
||||||
|
drawerLoading.value = false;
|
||||||
|
if (String(route.query.sessionId || '') !== String(sessionId)) {
|
||||||
|
router.replace({
|
||||||
|
path: '/chatHistory',
|
||||||
|
query: { ...route.query, sessionId: String(sessionId) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMessages(reset = false) {
|
||||||
|
if (!currentSession.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextPageNumber = reset ? 1 : messagePage.value.pageNumber + 1;
|
||||||
|
const [, res] = await tryit(api.get)(
|
||||||
|
`/userCenter/chatHistory/sessions/${currentSession.value.id}/messages`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
pageNumber: nextPageNumber,
|
||||||
|
pageSize: messagePage.value.pageSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res?.errorCode !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizeMessages(res.data?.records || []);
|
||||||
|
if (reset) {
|
||||||
|
messageList.value = normalized;
|
||||||
|
} else {
|
||||||
|
messageList.value = [...normalized, ...messageList.value];
|
||||||
|
}
|
||||||
|
messagePage.value.total = res.data?.total || 0;
|
||||||
|
messagePage.value.pageNumber = nextPageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessages(records: any[]) {
|
||||||
|
return [...records]
|
||||||
|
.reverse()
|
||||||
|
.map((item: any) => ({
|
||||||
|
key: String(item.id),
|
||||||
|
role: item.senderRole === 'assistant' ? 'assistant' : 'user',
|
||||||
|
content:
|
||||||
|
item.senderRole === 'assistant'
|
||||||
|
? String(item.contentText || '').replace(/^Final Answer:\s*/i, '')
|
||||||
|
: item.contentText,
|
||||||
|
placement: item.senderRole === 'assistant' ? 'start' : 'end',
|
||||||
|
created: item.created,
|
||||||
|
chains: Array.isArray(item.contentPayload?.chains)
|
||||||
|
? item.contentPayload.chains
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
drawerVisible.value = false;
|
||||||
|
currentSession.value = undefined;
|
||||||
|
messageList.value = [];
|
||||||
|
router.replace({ path: '/chatHistory', query: {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(pageNumber: number) {
|
||||||
|
queryParams.value.pageNumber = pageNumber;
|
||||||
|
fetchSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renameSession(session: any) {
|
||||||
|
const [, promptRes] = await tryit(ElMessageBox.prompt)(
|
||||||
|
'请输入新的会话名称',
|
||||||
|
'重命名会话',
|
||||||
|
{
|
||||||
|
inputValue: session.title || '',
|
||||||
|
inputPlaceholder: '请输入会话名称',
|
||||||
|
confirmButtonText: '确认',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const value = promptRes?.value?.trim();
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [, res] = await tryit(api.post)(`/userCenter/chatHistory/sessions/${session.id}/rename`, {
|
||||||
|
title: value,
|
||||||
|
});
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
ElMessage.success('重命名成功');
|
||||||
|
if (currentSession.value?.id === session.id) {
|
||||||
|
currentSession.value.title = value;
|
||||||
|
}
|
||||||
|
await fetchSessions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSession(session: any) {
|
||||||
|
ElMessageBox.confirm('删除后将不再出现在聊天历史中,是否继续?', '删除会话', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
}).then(async () => {
|
||||||
|
const [, res] = await tryit(api.post)(`/userCenter/chatHistory/sessions/${session.id}/delete`, {});
|
||||||
|
if (res?.errorCode === 0) {
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
if (currentSession.value?.id === session.id) {
|
||||||
|
closeDrawer();
|
||||||
|
}
|
||||||
|
await fetchSessions();
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(value?: string) {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const time = new Date(value);
|
||||||
|
if (Number.isNaN(time.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return `${time.getMonth() + 1}-${time.getDate()} ${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElContainer class="bg-background-deep h-full">
|
<ElContainer class="bg-background-deep h-full">
|
||||||
<ElHeader class="!h-auto !p-8 !pb-0">
|
<ElHeader class="!h-auto !px-8 !pb-0 !pt-8">
|
||||||
<ElSpace direction="vertical" :size="24" alignment="flex-start">
|
<ElSpace direction="vertical" :size="20" alignment="flex-start" class="w-full">
|
||||||
<h1 class="text-2xl font-medium">聊天记录</h1>
|
<div>
|
||||||
<div class="flex items-center gap-5">
|
<h1 class="text-2xl font-medium">聊天历史</h1>
|
||||||
<div class="flex items-center gap-4">
|
<p class="text-foreground/60 mt-2 text-sm">
|
||||||
<span class="text-nowrap text-sm">聊天助理</span>
|
查看最近会话,并在右侧抽屉中回溯完整聊天内容。
|
||||||
<ElSelect
|
</p>
|
||||||
clearable
|
</div>
|
||||||
v-model="queryParams.botId"
|
<div class="flex w-full flex-wrap items-center gap-4">
|
||||||
:options="assistantList"
|
<ElSelect
|
||||||
placeholder="请选择聊天助理"
|
v-model="queryParams.assistantId"
|
||||||
@change="search"
|
clearable
|
||||||
/>
|
placeholder="筛选聊天助理"
|
||||||
</div>
|
:options="assistantList"
|
||||||
|
class="!w-[220px]"
|
||||||
|
@change="fetchSessions"
|
||||||
|
/>
|
||||||
<ElInput
|
<ElInput
|
||||||
placeholder="搜索关键词"
|
v-model="queryParams.keyword"
|
||||||
v-model="queryParams.title"
|
class="max-w-[320px]"
|
||||||
@keyup.enter="search"
|
placeholder="搜索标题或最近消息"
|
||||||
@change="search"
|
|
||||||
:prefix-icon="Search"
|
:prefix-icon="Search"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ElSpace>
|
</ElSpace>
|
||||||
</ElHeader>
|
</ElHeader>
|
||||||
<ElMain class="!px-8">
|
|
||||||
<ElContainer class="bg-background rounded-lg p-5">
|
<ElMain class="!px-8 !pb-8">
|
||||||
<ElHeader
|
<div class="bg-background border-border min-h-full rounded-2xl border p-5">
|
||||||
class="dark:bg-accent grid grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center rounded-lg bg-[#f7f9fd] !p-0"
|
<div v-if="filteredSessions.length > 0" class="flex flex-col gap-3">
|
||||||
height="54px"
|
<button
|
||||||
>
|
v-for="item in filteredSessions"
|
||||||
<span
|
:key="item.id"
|
||||||
class="text-accent-foreground text-sm"
|
class="border-border hover:border-primary/30 hover:bg-accent/40 flex w-full items-start justify-between gap-4 rounded-2xl border px-5 py-4 text-left transition-colors"
|
||||||
v-for="title in listTitles"
|
@click="openSession(item.id)"
|
||||||
:key="title"
|
|
||||||
>
|
>
|
||||||
{{ title }}
|
<div class="min-w-0 flex-1">
|
||||||
</span>
|
<div class="flex items-center gap-3">
|
||||||
</ElHeader>
|
<span class="truncate text-base font-medium">{{ item.title || '未命名会话' }}</span>
|
||||||
<ElMain class="!p-0">
|
<ElTag size="small" effect="plain">{{ item.assistantName || '聊天助理' }}</ElTag>
|
||||||
<div class="flex flex-col items-center gap-5">
|
</div>
|
||||||
<div class="w-full">
|
<div class="text-foreground/65 mt-2 line-clamp-2 text-sm">
|
||||||
<PageData
|
{{ item.lastMessagePreview || '暂无消息内容' }}
|
||||||
page-url="/userCenter/botConversation/pageList"
|
</div>
|
||||||
ref="pageRef"
|
<div class="text-foreground/50 mt-3 flex flex-wrap items-center gap-4 text-xs">
|
||||||
>
|
<span>最近发送人:{{ item.lastSenderName || '未知' }}</span>
|
||||||
<template #default="{ pageList }">
|
<span>消息数:{{ item.messageCount || 0 }}</span>
|
||||||
<div
|
<span>活跃时间:{{ formatTime(item.lastMessageAt || item.accessAt) }}</span>
|
||||||
class="text-foreground/90 grid h-[60px] grid-cols-[repeat(3,minmax(0,1fr))_120px] place-items-center text-sm hover:bg-[var(--el-fill-color-light)]"
|
</div>
|
||||||
v-for="record in pageList"
|
|
||||||
:key="record.id"
|
|
||||||
>
|
|
||||||
<ElText truncated>{{ record.bot.title }}</ElText>
|
|
||||||
<ElText line-clamp="2">{{ record.title }}</ElText>
|
|
||||||
<span>{{ record.created }}</span>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<ElButton
|
|
||||||
class="[--el-font-weight-primary:400]"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="toDetail(record)"
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</ElButton>
|
|
||||||
|
|
||||||
<ElDropdown>
|
|
||||||
<ElButton :icon="MoreFilled" link />
|
|
||||||
|
|
||||||
<template #dropdown>
|
|
||||||
<ElDropdownMenu>
|
|
||||||
<ElDropdownItem>
|
|
||||||
<ElButton
|
|
||||||
link
|
|
||||||
type="danger"
|
|
||||||
:icon="Delete"
|
|
||||||
@click="handleDelete(record.id)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</ElButton>
|
|
||||||
</ElDropdownItem>
|
|
||||||
</ElDropdownMenu>
|
|
||||||
</template>
|
|
||||||
</ElDropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</PageData>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
</ElMain>
|
<ElButton link :icon="Edit" @click.stop="renameSession(item)">重命名</ElButton>
|
||||||
</ElContainer>
|
<ElButton link type="danger" :icon="Delete" @click.stop="deleteSession(item)">删除</ElButton>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ElEmpty v-else description="暂无聊天历史" />
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end" v-if="pageState.total > queryParams.pageSize">
|
||||||
|
<ElPagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next"
|
||||||
|
:current-page="queryParams.pageNumber"
|
||||||
|
:page-size="queryParams.pageSize"
|
||||||
|
:total="pageState.total"
|
||||||
|
@current-change="changePage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</ElMain>
|
</ElMain>
|
||||||
</ElContainer>
|
</ElContainer>
|
||||||
|
|
||||||
|
<ElDrawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
:title="currentSession?.title || '聊天详情'"
|
||||||
|
size="760px"
|
||||||
|
destroy-on-close
|
||||||
|
@close="closeDrawer"
|
||||||
|
>
|
||||||
|
<div v-loading="drawerLoading" class="flex h-full flex-col">
|
||||||
|
<div class="border-border mb-4 flex items-center justify-between rounded-2xl border px-4 py-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate text-base font-medium">{{ currentSession?.title || '聊天详情' }}</div>
|
||||||
|
<div class="text-foreground/55 mt-1 text-sm">
|
||||||
|
{{ currentSession?.assistantName || '聊天助理' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-foreground/50 text-xs">
|
||||||
|
{{ formatTime(currentSession?.lastMessageAt || currentSession?.accessAt) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<div class="mb-4 flex justify-center">
|
||||||
|
<ElButton
|
||||||
|
v-if="messageList.length < messagePage.total"
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
@click="loadMessages(false)"
|
||||||
|
>
|
||||||
|
加载更早消息
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<ChatBubbleList
|
||||||
|
:bot="{ icon: '', title: currentSession?.assistantName || '' }"
|
||||||
|
:messages="messageList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ElDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.el-select {
|
|
||||||
--el-select-width: 165px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select.bot-select {
|
|
||||||
--el-select-width: 343px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select :deep(.el-select__wrapper) {
|
|
||||||
--el-border-radius-base: 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
|
||||||
|
|
||||||
import { ElContainer, ElHeader, ElMain } from 'element-plus';
|
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
|
||||||
import { ChatBubbleList } from '#/components/chat';
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
const ids = reactive({
|
|
||||||
botId: '',
|
|
||||||
conversationId: '',
|
|
||||||
});
|
|
||||||
const conversationInfo = ref<any>();
|
|
||||||
const messageList = ref<any[]>([]);
|
|
||||||
const loading = ref(true);
|
|
||||||
const logoUrl = `${import.meta.env.BASE_URL || '/'}logo.svg`;
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (route.params.id) {
|
|
||||||
ids.conversationId = route.params.id as string;
|
|
||||||
getConversationDetails();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getConversationDetails() {
|
|
||||||
api
|
|
||||||
.get('/userCenter/botConversation/detail', {
|
|
||||||
params: {
|
|
||||||
id: ids.conversationId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
conversationInfo.value = res.data;
|
|
||||||
ids.botId = res.data.botId;
|
|
||||||
getMessageList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getMessageList() {
|
|
||||||
api
|
|
||||||
.get('/userCenter/botMessage/getMessages', {
|
|
||||||
params: ids,
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (res.errorCode === 0) {
|
|
||||||
messageList.value = res.data;
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="h-full w-full px-12 py-8 max-sm:p-3">
|
|
||||||
<ElContainer class="bg-background h-full">
|
|
||||||
<ElHeader
|
|
||||||
height="80px"
|
|
||||||
class="rounded-xl bg-[#F8F8F9] !pr-9 max-sm:!h-16 max-sm:!pr-3"
|
|
||||||
>
|
|
||||||
<div class="flex h-full w-full items-center justify-between">
|
|
||||||
<!-- Left -->
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-lg font-medium max-sm:text-base">{{
|
|
||||||
conversationInfo?.title
|
|
||||||
}}</span>
|
|
||||||
<div
|
|
||||||
v-if="conversationInfo?.bot.title"
|
|
||||||
class="text-foreground/70 rounded bg-[#ECECEE] p-1 text-xs"
|
|
||||||
>
|
|
||||||
{{ conversationInfo.bot.title }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="text-foreground/50 text-sm max-sm:text-xs">{{
|
|
||||||
conversationInfo?.created
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Right -->
|
|
||||||
<img :src="logoUrl" class="w-40 max-sm:w-28" />
|
|
||||||
</div>
|
|
||||||
</ElHeader>
|
|
||||||
<ElMain class="relative max-sm:mt-2 max-sm:!p-0" v-loading="loading">
|
|
||||||
<div
|
|
||||||
class="absolute bottom-5 left-1/2 top-5 w-full max-w-[1000px] -translate-x-1/2 max-sm:bottom-0 max-sm:top-0"
|
|
||||||
>
|
|
||||||
<ChatBubbleList
|
|
||||||
class="relative mx-auto h-full max-w-[1000px]"
|
|
||||||
:bot="conversationInfo?.bot"
|
|
||||||
:messages="messageList"
|
|
||||||
:editable="false"
|
|
||||||
:open-editor="() => {}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElMain>
|
|
||||||
</ElContainer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
:deep(.el-bubble-list) {
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
|
||||||
--bubble-content-max-width: calc(100% - 52px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media not all and (min-width: 640px) {
|
|
||||||
:deep(.el-bubble) {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-avatar) {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-bubble-content-wrapper .el-bubble-content) {
|
|
||||||
--bubble-content-max-width: calc(100% - 38px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import type { ChatThinkingBlockProps } from './types';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChatThinkingBlock',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ChatThinkingBlockProps>(), {
|
||||||
|
content: '',
|
||||||
|
disabled: false,
|
||||||
|
emptyBehavior: 'hide',
|
||||||
|
expanded: false,
|
||||||
|
label: '',
|
||||||
|
readonly: false,
|
||||||
|
status: 'end',
|
||||||
|
summary: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:expanded': [boolean];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const normalizedContent = computed(() =>
|
||||||
|
String(props.content || '')
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.replace(/^\s*\n+/, '')
|
||||||
|
.trimEnd(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldRender = computed(
|
||||||
|
() =>
|
||||||
|
normalizedContent.value.length > 0 || props.emptyBehavior === 'placeholder',
|
||||||
|
);
|
||||||
|
|
||||||
|
const expandedModel = computed({
|
||||||
|
get: () => props.expanded,
|
||||||
|
set: (value: boolean) => emit('update:expanded', value),
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedLabel = computed(() => {
|
||||||
|
if (props.label) {
|
||||||
|
return props.label;
|
||||||
|
}
|
||||||
|
if (props.status === 'thinking') {
|
||||||
|
return '思考中';
|
||||||
|
}
|
||||||
|
if (props.status === 'error') {
|
||||||
|
return '思考异常';
|
||||||
|
}
|
||||||
|
return '已思考';
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedSummary = computed(() => {
|
||||||
|
if (props.summary) {
|
||||||
|
return props.summary;
|
||||||
|
}
|
||||||
|
const source = normalizedContent.value
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.find(Boolean);
|
||||||
|
if (!source) {
|
||||||
|
return props.emptyBehavior === 'placeholder' ? '暂无思考内容' : '';
|
||||||
|
}
|
||||||
|
return source.length > 56 ? `${source.slice(0, 56)}...` : source;
|
||||||
|
});
|
||||||
|
|
||||||
|
const canToggle = computed(
|
||||||
|
() => !props.disabled && normalizedContent.value.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleExpanded() {
|
||||||
|
if (!canToggle.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expandedModel.value = !expandedModel.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="shouldRender"
|
||||||
|
class="chat-thinking-block"
|
||||||
|
:class="[
|
||||||
|
`is-${status}`,
|
||||||
|
{
|
||||||
|
'is-disabled': disabled,
|
||||||
|
'is-expanded': expandedModel,
|
||||||
|
'is-readonly': readonly,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chat-thinking-block__trigger"
|
||||||
|
:disabled="!canToggle"
|
||||||
|
@click="toggleExpanded"
|
||||||
|
>
|
||||||
|
<span class="chat-thinking-block__leading">
|
||||||
|
<span class="chat-thinking-block__indicator" aria-hidden="true"></span>
|
||||||
|
<span class="chat-thinking-block__label">{{ computedLabel }}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="!expandedModel && computedSummary"
|
||||||
|
class="chat-thinking-block__summary"
|
||||||
|
>
|
||||||
|
{{ computedSummary }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="chat-thinking-block__chevron"
|
||||||
|
:class="{ 'is-open': expandedModel }"
|
||||||
|
aria-hidden="true"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="chat-thinking-block__body-transition">
|
||||||
|
<div
|
||||||
|
v-if="expandedModel && normalizedContent"
|
||||||
|
class="chat-thinking-block__body"
|
||||||
|
>
|
||||||
|
<div class="chat-thinking-block__content">
|
||||||
|
{{ normalizedContent }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-thinking-block {
|
||||||
|
border: 1px solid hsl(var(--divider-faint) / 0.18);
|
||||||
|
border-radius: 16px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
hsl(var(--glass-tint) / 0.48) 0%,
|
||||||
|
hsl(var(--surface-panel) / 0.74) 100%
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 hsl(var(--glass-border) / 0.24),
|
||||||
|
0 10px 24px -24px hsl(var(--foreground) / 0.18);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
transition: background-color 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:not(:disabled):hover {
|
||||||
|
background: hsl(var(--surface-contrast-soft) / 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__trigger:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__leading {
|
||||||
|
display: inline-flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__indicator {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: hsl(var(--text-muted) / 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-thinking .chat-thinking-block__indicator {
|
||||||
|
background: hsl(var(--primary) / 0.82);
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
animation: chat-thinking-pulse 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-error .chat-thinking-block__indicator {
|
||||||
|
background: hsl(var(--destructive) / 0.86);
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--destructive) / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: hsl(var(--text-strong));
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__summary {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: hsl(var(--text-muted));
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__chevron {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-right: 1.5px solid hsl(var(--text-muted));
|
||||||
|
border-bottom: 1.5px solid hsl(var(--text-muted));
|
||||||
|
transform: rotate(45deg) translateY(-1px);
|
||||||
|
transition: transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__chevron.is-open {
|
||||||
|
transform: rotate(225deg) translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body {
|
||||||
|
padding: 0 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: hsl(var(--surface-panel) / 0.72);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.68;
|
||||||
|
color: hsl(var(--text-secondary));
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block.is-disabled {
|
||||||
|
opacity: 0.82;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body-transition-enter-active,
|
||||||
|
.chat-thinking-block__body-transition-leave-active {
|
||||||
|
transition:
|
||||||
|
opacity 0.18s ease,
|
||||||
|
transform 0.18s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-thinking-block__body-transition-enter-from,
|
||||||
|
.chat-thinking-block__body-transition-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes chat-thinking-pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 4px hsl(var(--primary) / 0.12);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 7px hsl(var(--primary) / 0.04);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export { default as ChatThinkingBlock } from './ChatThinkingBlock.vue';
|
||||||
|
export type {
|
||||||
|
ChatThinkingBlockProps,
|
||||||
|
ChatThinkingBlockStatus,
|
||||||
|
} from './types';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type ChatThinkingBlockStatus = 'end' | 'error' | 'thinking';
|
||||||
|
|
||||||
|
export interface ChatThinkingBlockProps {
|
||||||
|
content?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
emptyBehavior?: 'hide' | 'placeholder';
|
||||||
|
expanded?: boolean;
|
||||||
|
label?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
status?: ChatThinkingBlockStatus;
|
||||||
|
summary?: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './api-component';
|
export * from './api-component';
|
||||||
export * from './captcha';
|
export * from './captcha';
|
||||||
|
export * from './chat-thinking';
|
||||||
export * from './col-page';
|
export * from './col-page';
|
||||||
export * from './count-to';
|
export * from './count-to';
|
||||||
export * from './ellipsis-text';
|
export * from './ellipsis-text';
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
<svg width="18px" height="16px" viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<g
|
||||||
<title>聊天助理备份 7</title>
|
fill="none"
|
||||||
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
stroke="currentColor"
|
||||||
<g id="聊天记录" transform="translate(-29, -463)">
|
stroke-linecap="round"
|
||||||
<g id="编组-12备份-2" transform="translate(0, 3)">
|
stroke-linejoin="round"
|
||||||
<g id="编组-32备份-2" transform="translate(12, 444)">
|
stroke-width="1.25"
|
||||||
<g id="聊天助理备份-7" transform="translate(16, 14)">
|
>
|
||||||
<rect id="矩形" x="0" y="0" width="20" height="20"></rect>
|
<path d="M3 3.4h7.9A1.9 1.9 0 0 1 12.8 5.3v3.5a1.9 1.9 0 0 1-1.9 1.9H7.2l-2.4 2V10.7H4.9A1.9 1.9 0 0 1 3 8.8V3.4Z"/>
|
||||||
<path d="M15,2.8 C15.8836556,2.8 16.6836556,3.1581722 17.2627417,3.7372583 C17.8418278,4.3163444 18.2,5.1163444 18.2,6 L18.2,17.2 L5,17.2 C4.1163444,17.2 3.3163444,16.8418278 2.7372583,16.2627417 C2.1581722,15.6836556 1.8,14.8836556 1.8,14 L1.8,6 C1.8,5.1163444 2.1581722,4.3163444 2.7372583,3.7372583 C3.3163444,3.1581722 4.1163444,2.8 5,2.8 Z" id="形状结合" stroke="currentColor" stroke-width="1.6"></path>
|
<path d="M5.3 5.9h2.2"/>
|
||||||
<polyline id="路径-2" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" points="8.8275 6.93789684 8.8275 10.9003453 12.5 10.9003453"></polyline>
|
<circle cx="9.6" cy="7.2" r="1.95"/>
|
||||||
</g>
|
<path d="M9.6 6.2v1.2l.85.58"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 432 B |
@@ -10,6 +10,7 @@ const SvgDownloadIcon = createIconifyIcon('svg:download');
|
|||||||
const SvgCardIcon = createIconifyIcon('svg:card');
|
const SvgCardIcon = createIconifyIcon('svg:card');
|
||||||
const SvgBellIcon = createIconifyIcon('svg:bell');
|
const SvgBellIcon = createIconifyIcon('svg:bell');
|
||||||
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
const SvgCakeIcon = createIconifyIcon('svg:cake');
|
||||||
|
const SvgChatHistoryIcon = createIconifyIcon('svg:chat-history');
|
||||||
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
const SvgAntdvLogoIcon = createIconifyIcon('svg:antdv-logo');
|
||||||
const SvgGithubIcon = createIconifyIcon('svg:github');
|
const SvgGithubIcon = createIconifyIcon('svg:github');
|
||||||
const SvgGoogleIcon = createIconifyIcon('svg:google');
|
const SvgGoogleIcon = createIconifyIcon('svg:google');
|
||||||
@@ -27,6 +28,7 @@ export {
|
|||||||
SvgBellIcon,
|
SvgBellIcon,
|
||||||
SvgCakeIcon,
|
SvgCakeIcon,
|
||||||
SvgCardIcon,
|
SvgCardIcon,
|
||||||
|
SvgChatHistoryIcon,
|
||||||
SvgDingDingIcon,
|
SvgDingDingIcon,
|
||||||
SvgDownloadIcon,
|
SvgDownloadIcon,
|
||||||
SvgGithubIcon,
|
SvgGithubIcon,
|
||||||
|
|||||||
Reference in New Issue
Block a user