mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge remote-tracking branch 'origin/main' into py-client-about
This commit is contained in:
@@ -156,6 +156,16 @@ public interface Constant {
|
||||
*/
|
||||
String MEMORY_MEM_REPORT_ONLY = "Memory_mem_report_only";
|
||||
|
||||
/**
|
||||
* Mem0AI记忆
|
||||
*/
|
||||
String MEMORY_MEM0AI = "Memory_mem0ai";
|
||||
|
||||
/**
|
||||
* PowerMem记忆
|
||||
*/
|
||||
String MEMORY_POWERMEM = "Memory_powermem";
|
||||
|
||||
/**
|
||||
* 火山引擎双声道语音克隆
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentChatTitleDao extends BaseMapper<AgentChatTitleEntity> {
|
||||
|
||||
}
|
||||
@@ -23,4 +23,9 @@ public class AgentChatSessionDTO {
|
||||
* 聊天条数
|
||||
*/
|
||||
private Integer chatCount;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
|
||||
private String llmModelId;
|
||||
|
||||
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
|
||||
private String slmModelId;
|
||||
|
||||
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
|
||||
private String vllmModelId;
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName(value = "ai_agent_chat_title")
|
||||
public class AgentChatTitleEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
@TableField(value = "session_id")
|
||||
private String sessionId;
|
||||
|
||||
@TableField(value = "title")
|
||||
private String title;
|
||||
|
||||
@TableField(value = "created_at")
|
||||
private Date createdAt;
|
||||
|
||||
@TableField(value = "updated_at")
|
||||
private Date updatedAt;
|
||||
}
|
||||
@@ -37,6 +37,9 @@ public class AgentEntity {
|
||||
@Schema(description = "大语言模型标识")
|
||||
private String llmModelId;
|
||||
|
||||
@Schema(description = "小模型标识")
|
||||
private String slmModelId;
|
||||
|
||||
@Schema(description = "VLLM模型标识")
|
||||
private String vllmModelId;
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
|
||||
public interface AgentChatTitleService {
|
||||
|
||||
void saveOrUpdateTitle(String sessionId, String title);
|
||||
|
||||
String getTitleBySessionId(String sessionId);
|
||||
}
|
||||
+7
-1
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -26,6 +27,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
|
||||
/**
|
||||
@@ -36,9 +38,12 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
|
||||
implements AgentChatHistoryService {
|
||||
|
||||
private final AgentChatTitleService agentChatTitleService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
|
||||
String agentId = (String) params.get("agentId");
|
||||
@@ -61,6 +66,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
dto.setSessionId((String) map.get("session_id"));
|
||||
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
|
||||
dto.setChatCount(((Number) map.get("chat_count")).intValue());
|
||||
dto.setTitle(agentChatTitleService.getTitleBySessionId(dto.getSessionId()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
@@ -91,7 +97,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
if (ToolUtil.isNotEmpty(audioIds)) {
|
||||
// 每批删除1000条
|
||||
List<List<String>> batch = ListUtil.split(audioIds, 1000);
|
||||
batch.forEach(dataList->{
|
||||
batch.forEach(dataList -> {
|
||||
baseMapper.deleteAudioByIds(dataList);
|
||||
});
|
||||
}
|
||||
|
||||
+107
-26
@@ -22,6 +22,7 @@ import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -42,6 +43,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
private final AgentChatTitleService agentChatTitleService;
|
||||
private final DeviceService deviceService;
|
||||
private final LLMService llmService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
@@ -91,40 +93,42 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
@Override
|
||||
public boolean generateAndSaveChatSummary(String sessionId) {
|
||||
try {
|
||||
// 1. 获取设备信息(通过会话关联的设备)
|
||||
DeviceEntity device = getDeviceBySessionId(sessionId);
|
||||
if (device == null) {
|
||||
log.info("未找到与会话 {} 关联的设备", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查记忆模型类型,如果是仅上报聊天记录模式则跳过总结
|
||||
String memModelId = agentService.getAgentById(device.getAgentId()).getMemModelId();
|
||||
if (memModelId != null && memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
|
||||
String agentId = device.getAgentId();
|
||||
String memModelId = agentService.getAgentById(agentId).getMemModelId();
|
||||
|
||||
if (memModelId == null || memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
|
||||
log.info("会话 {} 使用仅上报聊天记录模式,跳过记忆总结", sessionId);
|
||||
generateAndSaveChatTitle(sessionId, agentId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 3. 生成总结
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (!summaryDTO.isSuccess()) {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
return false;
|
||||
}
|
||||
boolean shouldSummarizeMemory = !memModelId.equals(Constant.MEMORY_NO_MEM)
|
||||
&& !memModelId.equals(Constant.MEMORY_MEM0AI)
|
||||
&& !memModelId.equals(Constant.MEMORY_POWERMEM);
|
||||
|
||||
// 4. 更新智能体记忆
|
||||
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
|
||||
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
|
||||
|
||||
// 调用现有接口更新记忆
|
||||
agentService.updateAgentById(device.getAgentId(),
|
||||
new AgentUpdateDTO() {
|
||||
if (shouldSummarizeMemory) {
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (summaryDTO.isSuccess()) {
|
||||
agentService.updateAgentById(agentId, new AgentUpdateDTO() {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||
} else {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("会话 {} 使用 {} 模式,跳过记忆总结", sessionId, memModelId);
|
||||
}
|
||||
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, device.getAgentId());
|
||||
generateAndSaveChatTitle(sessionId, agentId);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -133,6 +137,87 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
}
|
||||
}
|
||||
|
||||
private void generateAndSaveChatTitle(String sessionId, String agentId) {
|
||||
try {
|
||||
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
|
||||
if (chatHistory == null || chatHistory.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
|
||||
if (meaningfulMessages.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder conversation = new StringBuilder();
|
||||
for (int i = 0; i < meaningfulMessages.size(); i++) {
|
||||
conversation.append("消息").append(i + 1).append(": ").append(meaningfulMessages.get(i)).append("\n");
|
||||
}
|
||||
|
||||
String slmModelId = getSlmModelId(agentId);
|
||||
String title = llmService.generateTitle(conversation.toString(), slmModelId);
|
||||
|
||||
if (StringUtils.isNotBlank(title)) {
|
||||
agentChatTitleService.saveOrUpdateTitle(sessionId, title);
|
||||
log.info("成功保存会话 {} 的标题: {}", sessionId, title);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("生成会话 {} 的标题时发生错误: {}", sessionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getSlmModelId(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String slmModelId = agentInfo.getSlmModelId();
|
||||
if (StringUtils.isNotBlank(slmModelId)) {
|
||||
log.info("会话 {} 使用SLM模型: {}", agentId, slmModelId);
|
||||
return slmModelId;
|
||||
}
|
||||
|
||||
ModelConfigEntity defaultLlmConfig = getDefaultLLMConfig();
|
||||
if (defaultLlmConfig != null) {
|
||||
log.info("会话 {} 使用默认LLM模型: {}", agentId, defaultLlmConfig.getId());
|
||||
return defaultLlmConfig.getId();
|
||||
}
|
||||
|
||||
String llmModelId = agentInfo.getLlmModelId();
|
||||
log.info("会话 {} 使用LLM模型(最终回退): {}", agentId, llmModelId);
|
||||
return llmModelId;
|
||||
} catch (Exception e) {
|
||||
log.error("获取智能体slm模型ID失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ModelConfigEntity getDefaultLLMConfig() {
|
||||
try {
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("获取默认LLM配置失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录
|
||||
*/
|
||||
@@ -313,15 +398,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
*/
|
||||
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
String modelId = getSlmModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
log.info("未找到SLM模型,使用默认LLM服务");
|
||||
return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务(支持历史记忆合并)
|
||||
String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
@@ -341,15 +424,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
*/
|
||||
private String callJavaLLMForSummary(String conversation, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
String modelId = getSlmModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
log.info("未找到SLM模型,使用默认LLM服务");
|
||||
return llmService.generateSummary(conversation);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务
|
||||
String summary = llmService.generateSummaryWithModel(conversation, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.modules.agent.dao.AgentChatTitleDao;
|
||||
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatTitleService;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatTitleServiceImpl implements AgentChatTitleService {
|
||||
|
||||
private final AgentChatTitleDao agentChatTitleDao;
|
||||
|
||||
@Override
|
||||
public void saveOrUpdateTitle(String sessionId, String title) {
|
||||
if (StringUtils.isBlank(sessionId) || StringUtils.isBlank(title)) {
|
||||
return;
|
||||
}
|
||||
|
||||
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("session_id", sessionId);
|
||||
|
||||
AgentChatTitleEntity existing = agentChatTitleDao.selectOne(wrapper);
|
||||
|
||||
if (existing != null) {
|
||||
existing.setTitle(title);
|
||||
existing.setUpdatedAt(new Date());
|
||||
agentChatTitleDao.updateById(existing);
|
||||
} else {
|
||||
AgentChatTitleEntity newEntity = AgentChatTitleEntity.builder()
|
||||
.id(java.util.UUID.randomUUID().toString().replace("-", ""))
|
||||
.sessionId(sessionId)
|
||||
.title(title)
|
||||
.createdAt(new Date())
|
||||
.updatedAt(new Date())
|
||||
.build();
|
||||
agentChatTitleDao.insert(newEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitleBySessionId(String sessionId) {
|
||||
if (StringUtils.isBlank(sessionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("session_id", sessionId);
|
||||
|
||||
AgentChatTitleEntity entity = agentChatTitleDao.selectOne(wrapper);
|
||||
return entity != null ? entity.getTitle() : null;
|
||||
}
|
||||
}
|
||||
+29
@@ -300,6 +300,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
if (dto.getLlmModelId() != null) {
|
||||
existingEntity.setLlmModelId(dto.getLlmModelId());
|
||||
}
|
||||
if (dto.getSlmModelId() != null) {
|
||||
existingEntity.setSlmModelId(dto.getSlmModelId());
|
||||
}
|
||||
if (dto.getVllmModelId() != null) {
|
||||
existingEntity.setVllmModelId(dto.getVllmModelId());
|
||||
}
|
||||
@@ -506,6 +509,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
|
||||
if (entity.getSlmModelId() == null) {
|
||||
String defaultSlmModelId = getDefaultLLMModelId();
|
||||
if (defaultSlmModelId != null) {
|
||||
entity.setSlmModelId(defaultSlmModelId);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户ID和创建者信息
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
entity.setUserId(user.getId());
|
||||
@@ -544,4 +554,23 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
private String getDefaultLLMModelId() {
|
||||
try {
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config.getId();
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0).getId();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -67,4 +67,13 @@ public interface LLMService {
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable(String modelId);
|
||||
|
||||
/**
|
||||
* 生成会话标题
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param modelId 模型ID
|
||||
* @return 标题(约15字)
|
||||
*/
|
||||
String generateTitle(String conversation, String modelId);
|
||||
}
|
||||
+89
@@ -37,6 +37,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
|
||||
private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}";
|
||||
|
||||
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation) {
|
||||
return generateSummary(conversation, null, null);
|
||||
@@ -302,4 +304,91 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTitle(String conversation, String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成标题");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
String prompt = DEFAULT_TITLE_PROMPT.replace("{conversation}", conversation);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", 0.3);
|
||||
requestBody.put("max_tokens", 50);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
String title = messageObj.getStr("content");
|
||||
if (StringUtils.isNotBlank(title)) {
|
||||
title = title.trim().replaceAll("[,。!?、:;''\"\"【】()]", "");
|
||||
if (title.length() > 15) {
|
||||
title = title.substring(0, 15);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成标题时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 智能体表添加小模型ID字段
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'slm_model_id');
|
||||
SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `slm_model_id` VARCHAR(255) NULL COMMENT ''小模型ID'' AFTER `llm_model_id`', 'SELECT ''Column slm_model_id already exists'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 创建聊天标题表
|
||||
DROP TABLE IF EXISTS `ai_agent_chat_title`;
|
||||
CREATE TABLE `ai_agent_chat_title` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '主键ID',
|
||||
`session_id` VARCHAR(255) NOT NULL COMMENT '会话ID',
|
||||
`title` VARCHAR(255) DEFAULT NULL COMMENT '聊天标题',
|
||||
`created_at` DATETIME DEFAULT NULL COMMENT '创建时间',
|
||||
`updated_at` DATETIME DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_session_id` (`session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体聊天标题表';
|
||||
@@ -599,3 +599,10 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202604011035.sql
|
||||
- changeSet:
|
||||
id: 202604011545
|
||||
author: rainv123
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202604011545.sql
|
||||
@@ -14,6 +14,8 @@
|
||||
<result column="asrModelId" property="asrModelId"/>
|
||||
<result column="vadModelId" property="vadModelId"/>
|
||||
<result column="llmModelId" property="llmModelId"/>
|
||||
<result column="slmModelId" property="slmModelId"/>
|
||||
<result column="vllmModelId" property="vllmModelId"/>
|
||||
<result column="ttsModelId" property="ttsModelId"/>
|
||||
<result column="ttsVoiceId" property="ttsVoiceId"/>
|
||||
<result column="ttsLanguage" property="ttsLanguage"/>
|
||||
@@ -46,6 +48,7 @@
|
||||
a.asr_model_id AS asrModelId,
|
||||
a.vad_model_id AS vadModelId,
|
||||
a.llm_model_id AS llmModelId,
|
||||
a.slm_model_id AS slmModelId,
|
||||
a.vllm_model_id AS vllmModelId,
|
||||
a.tts_model_id AS ttsModelId,
|
||||
a.tts_voice_id AS ttsVoiceId,
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface AgentDetail {
|
||||
asrModelId: string
|
||||
vadModelId: string
|
||||
llmModelId: string
|
||||
slmModelId: string
|
||||
vllmModelId: string
|
||||
ttsModelId: string
|
||||
ttsVoiceId: string
|
||||
|
||||
@@ -34,6 +34,9 @@ export function getChatHistory(agentId: string, sessionId: string) {
|
||||
ignoreAuth: false,
|
||||
toast: false,
|
||||
},
|
||||
cacheFor: {
|
||||
expire: -1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface ChatSession {
|
||||
sessionId: string
|
||||
createdAt: string
|
||||
chatCount: number
|
||||
title: string
|
||||
}
|
||||
|
||||
// 聊天会话列表响应
|
||||
@@ -14,7 +15,7 @@ export interface ChatSessionsResponse {
|
||||
// 聊天消息
|
||||
export interface ChatMessage {
|
||||
createdAt: string
|
||||
chatType: 1 | 2 // 1是用户,2是AI
|
||||
chatType: 1 | 2 | 3 // 1是用户,2是AI,3是参数说明
|
||||
content: string
|
||||
audioId: string | null
|
||||
macAddress: string
|
||||
|
||||
@@ -108,7 +108,8 @@ export default {
|
||||
'agent.modelConfig': 'Modellkonfiguration',
|
||||
'agent.vad': 'Sprachaktivitätserkennung',
|
||||
'agent.asr': 'Spracherkennung',
|
||||
'agent.llm': 'Großes Sprachmodell',
|
||||
'agent.llm': 'Hauptsprachenmodell',
|
||||
'agent.slm': 'Kleine Parametermodelle',
|
||||
'agent.vllm': 'Vision-Sprachmodell',
|
||||
'agent.intent': 'Absichtserkennung',
|
||||
'agent.memory': 'Speicher',
|
||||
|
||||
@@ -108,7 +108,8 @@ export default {
|
||||
'agent.modelConfig': 'Model Configuration',
|
||||
'agent.vad': 'Voice Activity Detection',
|
||||
'agent.asr': 'Speech Recognition',
|
||||
'agent.llm': 'Large Language Model',
|
||||
'agent.llm': 'Main language model',
|
||||
'agent.slm': 'Small parameter model',
|
||||
'agent.vllm': 'Vision Language Model',
|
||||
'agent.intent': 'Intent Recognition',
|
||||
'agent.memory': 'Memory',
|
||||
|
||||
@@ -108,7 +108,8 @@ export default {
|
||||
'agent.modelConfig': 'Configuração do Modelo',
|
||||
'agent.vad': 'Detecção de Atividade de Voz',
|
||||
'agent.asr': 'Reconhecimento de Fala',
|
||||
'agent.llm': 'Modelo de Linguagem Grande',
|
||||
'agent.llm': 'Modelo de Linguagem Principal',
|
||||
'agent.slm': 'Modelo de pequenos parâmetros',
|
||||
'agent.vllm': 'Modelo de Linguagem Visual',
|
||||
'agent.intent': 'Reconhecimento de Intenção',
|
||||
'agent.memory': 'Memória',
|
||||
|
||||
@@ -108,7 +108,8 @@ export default {
|
||||
'agent.modelConfig': 'Cấu hình mô hình',
|
||||
'agent.vad': 'Phát hiện hoạt động giọng nói',
|
||||
'agent.asr': 'Nhận dạng giọng nói',
|
||||
'agent.llm': 'Mô hình ngôn ngữ lớn',
|
||||
'agent.llm': 'Mô hình ngôn ngữ chính',
|
||||
'agent.slm': 'Mô hình tham số nhỏ',
|
||||
'agent.vllm': 'Mô hình ngôn ngữ thị giác',
|
||||
'agent.intent': 'Nhận dạng ý định',
|
||||
'agent.memory': 'Bộ nhớ',
|
||||
|
||||
@@ -108,7 +108,8 @@ export default {
|
||||
'agent.modelConfig': '模型配置',
|
||||
'agent.vad': '语音活动检测',
|
||||
'agent.asr': '语音识别',
|
||||
'agent.llm': '大语言模型',
|
||||
'agent.llm': '主语言模型',
|
||||
'agent.slm': '小参数模型',
|
||||
'agent.vllm': '视觉大模型',
|
||||
'agent.intent': '意图识别',
|
||||
'agent.memory': '记忆',
|
||||
|
||||
@@ -129,7 +129,8 @@ export default {
|
||||
'agent.modelConfig': '模型配置',
|
||||
'agent.vad': '語音活動檢測',
|
||||
'agent.asr': '語音識別',
|
||||
'agent.llm': '大語言模型',
|
||||
'agent.llm': '主語言模型',
|
||||
'agent.slm': '小參數模型',
|
||||
'agent.vllm': '視覺大模型',
|
||||
'agent.intent': '意圖識別',
|
||||
'agent.memory': '記憶',
|
||||
|
||||
@@ -29,6 +29,7 @@ const formData = ref<Partial<AgentDetail>>({
|
||||
vadModelId: '',
|
||||
asrModelId: '',
|
||||
llmModelId: '',
|
||||
slmModelId: '',
|
||||
vllmModelId: '',
|
||||
intentModelId: '',
|
||||
memModelId: '',
|
||||
@@ -46,6 +47,7 @@ const displayNames = ref({
|
||||
vad: t('agent.pleaseSelect'),
|
||||
asr: t('agent.pleaseSelect'),
|
||||
llm: t('agent.pleaseSelect'),
|
||||
slm: t('agent.pleaseSelect'),
|
||||
vllm: t('agent.pleaseSelect'),
|
||||
intent: t('agent.pleaseSelect'),
|
||||
memory: t('agent.pleaseSelect'),
|
||||
@@ -94,6 +96,7 @@ const pickerShow = ref<{
|
||||
vad: false,
|
||||
asr: false,
|
||||
llm: false,
|
||||
slm: false,
|
||||
vllm: false,
|
||||
intent: false,
|
||||
memory: false,
|
||||
@@ -272,6 +275,7 @@ function updateDisplayNames() {
|
||||
displayNames.value.vad = getModelDisplayName('VAD', formData.value.vadModelId)
|
||||
displayNames.value.asr = getModelDisplayName('ASR', formData.value.asrModelId)
|
||||
displayNames.value.llm = getModelDisplayName('LLM', formData.value.llmModelId)
|
||||
displayNames.value.slm = getModelDisplayName('LLM', formData.value.slmModelId)
|
||||
displayNames.value.vllm = getModelDisplayName('VLLM', formData.value.vllmModelId)
|
||||
displayNames.value.intent = getModelDisplayName('Intent', formData.value.intentModelId)
|
||||
displayNames.value.memory = getModelDisplayName('Memory', formData.value.memModelId)
|
||||
@@ -279,7 +283,6 @@ function updateDisplayNames() {
|
||||
|
||||
// 角色音色特殊处理
|
||||
displayNames.value.report = reportOptions.find(item => item.value === formData.value.chatHistoryConf)?.name
|
||||
displayNames.value.language = formData.value.ttsLanguage
|
||||
|
||||
isVisibleReport.value = formData.value.memModelId !== 'Memory_nomem'
|
||||
|
||||
@@ -437,6 +440,7 @@ function selectRoleTemplate(templateId: string) {
|
||||
vadModelId: template.vadModelId || formData.value.vadModelId,
|
||||
asrModelId: template.asrModelId || formData.value.asrModelId,
|
||||
llmModelId: template.llmModelId || formData.value.llmModelId,
|
||||
slmModelId: template.llmModelId || formData.value.slmModelId,
|
||||
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
|
||||
intentModelId: template.intentModelId || formData.value.intentModelId,
|
||||
memModelId: template.memModelId || formData.value.memModelId,
|
||||
@@ -474,6 +478,9 @@ async function onPickerConfirm(type: string, value: any, name: string) {
|
||||
case 'llm':
|
||||
formData.value.llmModelId = value
|
||||
break
|
||||
case 'slm':
|
||||
formData.value.slmModelId = value
|
||||
break
|
||||
case 'vllm':
|
||||
formData.value.vllmModelId = value
|
||||
break
|
||||
@@ -490,7 +497,8 @@ async function onPickerConfirm(type: string, value: any, name: string) {
|
||||
if (value === 'Memory_nomem' || value === 'Memory_mem_report_only') {
|
||||
tempSummaryMemory.value = formData.value.summaryMemory
|
||||
formData.value.summaryMemory = ''
|
||||
} else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
|
||||
}
|
||||
else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
|
||||
formData.value.summaryMemory = tempSummaryMemory.value
|
||||
tempSummaryMemory.value = ''
|
||||
}
|
||||
@@ -839,6 +847,16 @@ onMounted(async () => {
|
||||
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
|
||||
</view>
|
||||
|
||||
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('slm')">
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('agent.slm') }}
|
||||
</text>
|
||||
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
|
||||
{{ displayNames.slm }}
|
||||
</text>
|
||||
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
|
||||
</view>
|
||||
|
||||
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
|
||||
<text class="text-[28rpx] text-[#232338] font-medium">
|
||||
{{ t('agent.vllm') }}
|
||||
@@ -995,6 +1013,13 @@ onMounted(async () => {
|
||||
@select="({ item }) => onPickerConfirm('llm', item.value, item.name)"
|
||||
/>
|
||||
|
||||
<wd-action-sheet
|
||||
v-model="pickerShow.slm"
|
||||
:actions="modelOptions.LLM && modelOptions.LLM.map(item => ({ name: item.modelName, value: item.id }))"
|
||||
@close="onPickerCancel('slm')"
|
||||
@select="({ item }) => onPickerConfirm('slm', item.value, item.name)"
|
||||
/>
|
||||
|
||||
<wd-action-sheet
|
||||
v-model="pickerShow.vllm"
|
||||
:actions="modelOptions.VLLM && modelOptions.VLLM.map(item => ({ name: item.modelName, value: item.id }))"
|
||||
|
||||
@@ -61,6 +61,7 @@ const loading = ref(false)
|
||||
// 音频播放相关
|
||||
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
|
||||
const playingAudioId = ref<string | null>(null)
|
||||
const expandedToolResults = ref({})
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
@@ -124,8 +125,50 @@ function getSpeakerName(message: ChatMessage): string {
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(timeStr: string) {
|
||||
const date = new Date(timeStr)
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
if (!timeStr)
|
||||
return t('chatHistory.unknownTime')
|
||||
|
||||
// 处理时间字符串,确保格式正确
|
||||
const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式
|
||||
const now = new Date()
|
||||
|
||||
// 检查日期是否有效
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return timeStr // 如果解析失败,直接返回原字符串
|
||||
}
|
||||
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
// 小于1分钟
|
||||
if (diff < 60000)
|
||||
return t('chatHistory.justNow')
|
||||
|
||||
// 小于1小时
|
||||
if (diff < 3600000)
|
||||
return t('chatHistory.minutesAgo', { minutes: Math.floor(diff / 60000) })
|
||||
|
||||
// 小于1天(24小时)
|
||||
if (diff < 86400000)
|
||||
return t('chatHistory.hoursAgo', { hours: Math.floor(diff / 3600000) })
|
||||
|
||||
// 小于7天
|
||||
if (diff < 604800000) {
|
||||
const days = Math.floor(diff / 86400000)
|
||||
return t('chatHistory.daysAgo', { days })
|
||||
}
|
||||
|
||||
// 超过7天,显示具体日期
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const currentYear = now.getFullYear()
|
||||
|
||||
// 如果是当前年份,不显示年份
|
||||
if (year === currentYear) {
|
||||
return `${month}-${day}`
|
||||
}
|
||||
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 播放音频
|
||||
@@ -192,11 +235,56 @@ const playAudio = debounce(async (audioId: string) => {
|
||||
}
|
||||
}, 400)
|
||||
|
||||
function extractContentFromString(content: string) {
|
||||
if (!content || content.trim() === '') {
|
||||
return content
|
||||
}
|
||||
|
||||
// 尝试解析为 JSON
|
||||
try {
|
||||
const jsonObj = JSON.parse(content)
|
||||
|
||||
// 如果是数组格式(包含 text 和 tool)
|
||||
if (Array.isArray(jsonObj)) {
|
||||
return jsonObj
|
||||
}
|
||||
|
||||
// 如果是对象且有 content 字段
|
||||
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
|
||||
return jsonObj.content
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// 如果不是有效的 JSON,直接返回原内容
|
||||
}
|
||||
|
||||
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
|
||||
return content
|
||||
}
|
||||
|
||||
function toggleToolResult(messageIndex, itemIndex) {
|
||||
const key = `${messageIndex}-${itemIndex}`
|
||||
expandedToolResults.value[key] = !expandedToolResults.value[key]
|
||||
}
|
||||
|
||||
function isToolResultCollapsed(messageIndex, itemIndex) {
|
||||
const key = `${messageIndex}-${itemIndex}`
|
||||
// 默认折叠(true表示折叠)
|
||||
return !expandedToolResults.value[key]
|
||||
}
|
||||
|
||||
function getFirstLineText(text: string) {
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
const firstLine = text.split('\n')[0]
|
||||
return firstLine.length < text.length ? `${firstLine}...` : text
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options?.sessionId && options?.agentId) {
|
||||
sessionId.value = options.sessionId
|
||||
agentId.value = options.agentId
|
||||
loadChatHistory()
|
||||
}
|
||||
else {
|
||||
console.error('缺少必要参数')
|
||||
@@ -204,6 +292,10 @@ onLoad((options) => {
|
||||
}
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadChatHistory()
|
||||
})
|
||||
|
||||
// 页面销毁时清理音频资源
|
||||
onUnload(() => {
|
||||
if (audioContext.value) {
|
||||
@@ -256,6 +348,7 @@ onUnload(() => {
|
||||
:class="{
|
||||
'items-end': message.chatType === 1,
|
||||
'items-start': message.chatType === 2,
|
||||
'tool-message': message.chatType === 3,
|
||||
}"
|
||||
>
|
||||
<!-- 消息气泡 -->
|
||||
@@ -263,11 +356,39 @@ onUnload(() => {
|
||||
class="shadow-message break-words rounded-[20rpx] p-[24rpx] leading-[1.4]"
|
||||
:class="{
|
||||
'bg-[#336cff] text-white': message.chatType === 1,
|
||||
'bg-white text-[#232338] border border-[#eeeeee]': message.chatType === 2,
|
||||
'bg-white text-[#232338] border border-[#eeeeee]': [2, 3].includes(message.chatType),
|
||||
}"
|
||||
>
|
||||
<template v-if="Array.isArray(extractContentFromString(message.content))">
|
||||
<div class="content-wrapper">
|
||||
<div v-for="(item, idx) in extractContentFromString(message.content)" :key="idx">
|
||||
<div v-if="item.type === 'text'" class="text-content">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<div v-else-if="item.type === 'tool'" class="tool-call-text">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<div v-else-if="item.type === 'tool_result'" class="tool-call-text">
|
||||
<div v-if="item.text && item.text.length > 80" class="tool-result-wrapper">
|
||||
<div v-if="isToolResultCollapsed(index, idx)" class="tool-result-collapsed">
|
||||
{{ getFirstLineText(item.text) }}
|
||||
</div>
|
||||
<div v-else class="tool-result-expanded">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
<span class="tool-toggle-btn" @click="toggleToolResult(index, idx)">
|
||||
<wd-icon :name="isToolResultCollapsed(index, idx) ? 'arrow-down' : 'arrow-up'" size="12" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 内容区域 - 使用flex布局让图标和文本对齐 -->
|
||||
<view class="flex items-center gap-[12rpx]">
|
||||
<view v-else class="flex items-center gap-[12rpx]">
|
||||
<!-- 音频播放图标 -->
|
||||
<view
|
||||
v-if="message.audioId"
|
||||
@@ -334,4 +455,46 @@ onUnload(() => {
|
||||
.animate-pulse-audio {
|
||||
animation: pulse-audio 1.5s infinite;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.tool-call-text {
|
||||
color: #1890ff;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 500;
|
||||
font-size: 24rpx;
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.user-message .tool-call-text {
|
||||
color: #e6f7ff;
|
||||
}
|
||||
|
||||
.tool-message .message-content {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.tool-result-wrapper {
|
||||
position: relative;
|
||||
padding-right: 40rpx;
|
||||
}
|
||||
|
||||
.tool-result-collapsed {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tool-toggle-btn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
color: #1890ff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,20 +3,21 @@ import type { ChatSession } from '@/api/chat-history/types'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getChatSessions } from '@/api/chat-history/chat-history'
|
||||
import { t } from '@/i18n'
|
||||
import { deepClone } from '@/utils'
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatHistory',
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
agentId: 'default',
|
||||
})
|
||||
|
||||
// 接收props
|
||||
interface Props {
|
||||
agentId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
agentId: 'default'
|
||||
})
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
let safeAreaInsets: any
|
||||
let systemInfo: any
|
||||
@@ -43,7 +44,7 @@ const sessionList = ref<ChatSession[]>([])
|
||||
const loading = ref(false)
|
||||
const loadingMore = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const currentPage = ref(0)
|
||||
const pageSize = 10
|
||||
|
||||
// 使用传入的智能体ID
|
||||
@@ -52,10 +53,8 @@ const currentAgentId = computed(() => {
|
||||
})
|
||||
|
||||
// 加载聊天会话列表
|
||||
async function loadChatSessions(page = 1, isRefresh = false) {
|
||||
async function loadChatSessions(page = 1, isUpdate = false) {
|
||||
try {
|
||||
console.log(t('chatHistory.getChatSessions'), { page, isRefresh })
|
||||
|
||||
// 检查是否有当前选中的智能体
|
||||
if (!currentAgentId.value) {
|
||||
console.warn(t('chatHistory.noSelectedAgent'))
|
||||
@@ -76,7 +75,10 @@ async function loadChatSessions(page = 1, isRefresh = false) {
|
||||
})
|
||||
|
||||
if (page === 1) {
|
||||
sessionList.value = response.list || []
|
||||
const oldSessionList = deepClone(sessionList.value)
|
||||
oldSessionList.splice(0, 10)
|
||||
oldSessionList.unshift(...(response.list || []))
|
||||
sessionList.value = isUpdate ? oldSessionList : response.list || []
|
||||
}
|
||||
else {
|
||||
sessionList.value.push(...(response.list || []))
|
||||
@@ -170,10 +172,15 @@ function goToChatDetail(session: ChatSession) {
|
||||
|
||||
onMounted(async () => {
|
||||
// 智能体已简化为默认
|
||||
|
||||
loadChatSessions(1)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
if (currentPage.value !== 0) {
|
||||
loadChatSessions(1, true)
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
refresh,
|
||||
@@ -187,8 +194,8 @@ defineExpose({
|
||||
<view v-if="loading && sessionList.length === 0" class="loading-container">
|
||||
<wd-loading color="#336cff" />
|
||||
<text class="loading-text">
|
||||
{{ t('chatHistory.loading') }}
|
||||
</text>
|
||||
{{ t('chatHistory.loading') }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
@@ -205,7 +212,7 @@ defineExpose({
|
||||
<view class="session-info">
|
||||
<view class="session-header">
|
||||
<text class="session-title">
|
||||
{{ t('chatHistory.conversationRecord') }} {{ session.sessionId.substring(0, 8) }}...
|
||||
{{ session.title || `${t('chatHistory.conversationRecord')} ${session.sessionId.substring(0, 8)}...` }}
|
||||
</text>
|
||||
<text class="session-time">
|
||||
{{ formatTime(session.createdAt) }}
|
||||
@@ -242,11 +249,11 @@ defineExpose({
|
||||
<view v-else-if="!loading" class="empty-state">
|
||||
<wd-icon name="chat" custom-class="empty-icon" />
|
||||
<text class="empty-text">
|
||||
{{ t('chatHistory.noChatRecords') }}
|
||||
</text>
|
||||
<text class="empty-desc">
|
||||
{{ t('chatHistory.chatRecordsDescription') }}
|
||||
</text>
|
||||
{{ t('chatHistory.noChatRecords') }}
|
||||
</text>
|
||||
<text class="empty-desc">
|
||||
{{ t('chatHistory.chatRecordsDescription') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -333,7 +340,7 @@ defineExpose({
|
||||
padding: 32rpx;
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
width: 94%;
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
@@ -345,8 +352,11 @@ defineExpose({
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #232338;
|
||||
max-width: 70%;
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
|
||||
@@ -300,3 +300,36 @@ export function debounce<T extends AnyFunction>(
|
||||
|
||||
return debounced
|
||||
}
|
||||
|
||||
type DeepCloneTarget = string | number | boolean | null | undefined | object
|
||||
|
||||
/**
|
||||
* 深拷贝方法
|
||||
* @param target 要拷贝的目标
|
||||
* @returns 拷贝后的新对象
|
||||
*/
|
||||
export function deepClone<T extends DeepCloneTarget>(target: T): T {
|
||||
if (target === null || typeof target !== 'object') {
|
||||
return target
|
||||
}
|
||||
|
||||
if (target instanceof Date) {
|
||||
return new Date(target.getTime()) as any
|
||||
}
|
||||
|
||||
if (Array.isArray(target)) {
|
||||
return target.map(item => deepClone(item)) as any
|
||||
}
|
||||
|
||||
if (target instanceof Object) {
|
||||
const clonedObj = {} as T
|
||||
for (const key in target) {
|
||||
if (Object.prototype.hasOwnProperty.call(target, key)) {
|
||||
(clonedObj as any)[key] = deepClone((target as any)[key])
|
||||
}
|
||||
}
|
||||
return clonedObj
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
|
||||
<img :src="getUserAvatar(session.sessionId)" class="avatar" />
|
||||
<div class="session-info">
|
||||
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
|
||||
<div class="session-time">{{ session.title || formatTime(session.createdAt) }}</div>
|
||||
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@ export default {
|
||||
if (this.messages[0]) {
|
||||
result.push({
|
||||
type: 'time',
|
||||
content: this.formatTime(this.messages[0].createdAt),
|
||||
content: this.formatTime(this.messages[this.messages.length - 1].createdAt),
|
||||
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': 'Interaktionssprache',
|
||||
'roleConfig.vad': 'VAD',
|
||||
'roleConfig.asr': 'ASR',
|
||||
'roleConfig.llm': 'LLM',
|
||||
'roleConfig.llm': 'Hauptsprachmodell (LLM)',
|
||||
'roleConfig.slm': 'Kleines Sprachmodell (SLM)',
|
||||
'roleConfig.vllm': 'VLLM',
|
||||
'roleConfig.tts': 'TTS',
|
||||
'roleConfig.memoryHis': 'Speicher',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': 'Audio kann nicht abgespielt werden',
|
||||
'roleConfig.audioPlayError': 'Fehler bei der Audio-Wiedergabe',
|
||||
|
||||
// Tooltip-Beschreibungen für Formularfelder
|
||||
'roleConfig.tooltip.agentName': 'Legen Sie den Namen Ihres KI-Agenten fest, um ihn zu identifizieren und wiederzuerkennen',
|
||||
'roleConfig.tooltip.roleTemplate': 'Wählen Sie aus voreingestellten Rollenvorlagen, um die Grundeinstellungen Ihres Agenten schnell zu konfigurieren',
|
||||
'roleConfig.tooltip.contextProvider': 'Wenn XiaoZhi aktiviert wird, werden externe Systemdaten abgerufen und dynamisch in die Systemaufforderung des LLM eingefügt',
|
||||
'roleConfig.tooltip.roleIntroduction': 'Definieren Sie die Rollenpositionierung, Persönlichkeitsmerkmale, Verhaltensnormen und beruflichen Wissenshintergründe des KI-Agenten',
|
||||
'roleConfig.tooltip.memoryHis': 'Zusammenfassung des Chatverlaufs',
|
||||
'roleConfig.tooltip.languageCode': 'Legen Sie Sprachcodes fest, z.B. de-DE, en-US usw., die für die Erkennung bestimmter Funktionen verwendet werden',
|
||||
'roleConfig.tooltip.interactionLanguage': 'Legen Sie die Interaktionssprache fest und geben Sie die primäre Sprache an, die der KI-Agent für die Kommunikation verwendet',
|
||||
'roleConfig.tooltip.vad': 'Sprachaktivitätserkennung: Erkennt, wann der Benutzer zu sprechen beginnt oder aufhört, wird verwendet, um Gesprächsbeginn und -ende zu bestimmen und ermöglicht die Unterbrechungsfunktion',
|
||||
'roleConfig.tooltip.asr': 'Automatische Spracherkennung: Wandelt Benutzersprache in Text um, der erste Schritt im Mensch-Computer-Dialog, unterstützt mehrsprachige Erkennung',
|
||||
'roleConfig.tooltip.llm': 'Hauptsprachmodell (Large Language Model): Das "Gehirn" des KI-Agenten, verantwortlich für das Verstehen von Benutzerabsichten, das Generieren von Antworten und das Ausführen verschiedener Aufgaben',
|
||||
'roleConfig.tooltip.slm': 'Kleines Parameter-Modell (Small Language Model): Wird für die KI-Agenten-Aktivierung verwendet, erstellt Zusammenfassungstitel für Erinnerungen',
|
||||
'roleConfig.tooltip.vllm': 'Visuelles großes Sprachmodell: Verarbeitet Bild- und Videoverständnis, ermöglicht dem KI-Agenten das Analysieren und Beschreiben von kameragefangenen Inhalten',
|
||||
'roleConfig.tooltip.intent': 'Absichtserkennung: Analysiert Benutzersprache oder -text, um die wahre Absicht des Benutzers zu bestimmen, wie z.B. Anfragen, Chat, Gerätesteuerung usw.',
|
||||
'roleConfig.tooltip.memory': 'Gedächtnismodell: Verwaltet Speicherung und Zusammenfassung des Gesprächsverlaufs, bestimmt, ob sich der KI-Agent an frühere Gespräche erinnern kann für Langzeitgedächtnis',
|
||||
'roleConfig.tooltip.tts': 'Text-zu-Sprache: Konvertiert Text in natürliche Sprache, bestimmt Stimme, Geschwindigkeit und Tonfall des KI-Agenten',
|
||||
'roleConfig.tooltip.language': 'Wählen Sie die Sprache der Stimme; das System filtert verfügbare Stimmen, die diese Sprache unterstützen',
|
||||
'roleConfig.tooltip.voiceType': 'Wählen Sie die Stimme für den KI-Agenten; verschiedene Stimmen haben unterschiedliche Eigenschaften und Stile. Einige Stimmen unterstützen die Vorschaufunktion, klicken Sie auf die Wiedergabeschaltfläche für eine Vorschau',
|
||||
|
||||
// Function management dialog text
|
||||
'functionDialog.title': 'Funktionsverwaltung',
|
||||
'functionDialog.unselectedFunctions': 'Nicht ausgewählte Funktionen',
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': 'Interaction Language',
|
||||
'roleConfig.vad': 'Voice Detect',
|
||||
'roleConfig.asr': 'Speech Recognition',
|
||||
'roleConfig.llm': 'Language Model',
|
||||
'roleConfig.llm': 'Main Language Model',
|
||||
'roleConfig.slm': 'Small Language Model',
|
||||
'roleConfig.vllm': 'Vision Model',
|
||||
'roleConfig.tts': 'Text-to-Speech',
|
||||
'roleConfig.memoryHis': 'Memory',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': 'Cannot play audio',
|
||||
'roleConfig.audioPlayError': 'Error occurred during audio playback',
|
||||
|
||||
// Form field Tooltip descriptions
|
||||
'roleConfig.tooltip.agentName': 'Set the name of your AI agent for identification and recognition',
|
||||
'roleConfig.tooltip.roleTemplate': 'Choose from preset role templates to quickly configure your agent\'s basic settings',
|
||||
'roleConfig.tooltip.contextProvider': 'When XiaoZhi is awakened, fetch external system data and dynamically inject it into the LLM system prompt',
|
||||
'roleConfig.tooltip.roleIntroduction': 'Define the AI agent\'s role positioning, personality traits, behavioral norms, and professional knowledge background',
|
||||
'roleConfig.tooltip.memoryHis': 'Summarize chat record content',
|
||||
'roleConfig.tooltip.languageCode': 'Set language code such as zh-CN, en-US, etc., used for specific feature recognition',
|
||||
'roleConfig.tooltip.interactionLanguage': 'Set the interaction language, specifying the primary language the AI agent uses for communication',
|
||||
'roleConfig.tooltip.vad': 'Voice Activity Detection: Detects when the user starts or stops speaking, used to determine conversation start and end, enabling interrupt functionality',
|
||||
'roleConfig.tooltip.asr': 'Automatic Speech Recognition: Converts user speech to text, the first step in human-computer dialogue, supporting multilingual recognition',
|
||||
'roleConfig.tooltip.llm': 'Main Language Model (Large Language Model): The "brain" of the AI agent, responsible for understanding user intent, generating responses, and executing various tasks',
|
||||
'roleConfig.tooltip.slm': 'Small Parameter Model (Small Language Model): Used for AI agent wake-up, generating memory summary titles',
|
||||
'roleConfig.tooltip.vllm': 'Visual Large Language Model: Processes image and video understanding, enabling the AI agent to analyze and describe camera-captured content',
|
||||
'roleConfig.tooltip.intent': 'Intent Detection: Analyzes user speech or text to determine the user\'s true intent, such as queries, chat, device control, etc.',
|
||||
'roleConfig.tooltip.memory': 'Memory Model: Manages conversation history storage and summarization, determining whether the AI can remember previous conversations for long-term memory',
|
||||
'roleConfig.tooltip.tts': 'Text-to-Speech: Converts text to natural speech, determining the AI\'s voice, speed, and tone',
|
||||
'roleConfig.tooltip.language': 'Select the language of the voice; the system will filter available voices that support that language',
|
||||
'roleConfig.tooltip.voiceType': 'Choose the voice for the AI agent; different voices have different characteristics and styles. Some voices support preview functionality, click the play button to preview',
|
||||
|
||||
// Function management dialog text
|
||||
'functionDialog.title': 'Function Management',
|
||||
'functionDialog.unselectedFunctions': 'Unselected Functions',
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': 'Idioma de Interação',
|
||||
'roleConfig.vad': 'Detecção de Voz',
|
||||
'roleConfig.asr': 'Reconhecimento de Fala',
|
||||
'roleConfig.llm': 'Modelo de Linguagem',
|
||||
'roleConfig.llm': 'Modelo de Linguagem Principal',
|
||||
'roleConfig.slm': 'Modelo de Linguagem Pequeno',
|
||||
'roleConfig.vllm': 'Modelo de Visão',
|
||||
'roleConfig.intent': 'Reconhecimento de Intenção',
|
||||
'roleConfig.memoryHis': 'Memória',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': 'Não é possível reproduzir áudio',
|
||||
'roleConfig.audioPlayError': 'Ocorreu um erro durante a reprodução de áudio',
|
||||
|
||||
// Descrições de Tooltip para campos de formulário
|
||||
'roleConfig.tooltip.agentName': 'Defina o nome do seu agente de IA para identificação e reconhecimento',
|
||||
'roleConfig.tooltip.roleTemplate': 'Escolha entre modelos de função predefinidos para configurar rapidamente as configurações básicas do seu agente',
|
||||
'roleConfig.tooltip.contextProvider': 'Quando o XiaoZhi é ativado, busca dados de sistemas externos e os injeta dinamicamente no prompt do sistema do LLM',
|
||||
'roleConfig.tooltip.roleIntroduction': 'Defina o posicionamento do papel, traços de personalidade, normas comportamentais e histórico de conhecimento profissional do agente de IA',
|
||||
'roleConfig.tooltip.memoryHis': 'Resumir conteúdo do registro de chat',
|
||||
'roleConfig.tooltip.languageCode': 'Defina o código de idioma como pt-BR, en-US, etc., usado para reconhecimento de recursos específicos',
|
||||
'roleConfig.tooltip.interactionLanguage': 'Defina o idioma de interação, especificando o idioma principal que o agente de IA usa para comunicação',
|
||||
'roleConfig.tooltip.vad': 'Detecção de Atividade de Voz: Detecta quando o usuário começa ou para de falar, usado para determinar o início e fim da conversa, permitindo funcionalidade de interrupção',
|
||||
'roleConfig.tooltip.asr': 'Reconhecimento Automático de Fala: Converte fala do usuário em texto, o primeiro passo no diálogo humano-computador, suporta reconhecimento multilíngue',
|
||||
'roleConfig.tooltip.llm': 'Modelo de Linguagem Principal (Large Language Model): O "cérebro" do agente de IA, responsável por entender a intenção do usuário, gerar respostas e executar várias tarefas',
|
||||
'roleConfig.tooltip.slm': 'Modelo de Parâmetros Pequeno (Small Language Model): Usado para ativação do agente de IA, gerando títulos de resumo de memória',
|
||||
'roleConfig.tooltip.vllm': 'Modelo de Linguagem Visual Grande: Processa compreensão de imagem e vídeo, permitindo que o agente de IA analise e descreva o conteúdo capturado pela câmera',
|
||||
'roleConfig.tooltip.intent': 'Detecção de Intenção: Analisa fala ou texto do usuário para determinar a verdadeira intenção do usuário, como consultas, bate-papo, controle de dispositivos, etc.',
|
||||
'roleConfig.tooltip.memory': 'Modelo de Memória: Gerencia armazenamento e resumo do histórico de conversas, determina se a IA pode lembrar conversas anteriores para memória de longo prazo',
|
||||
'roleConfig.tooltip.tts': 'Texto para Fala: Converte texto em fala natural, determinando a voz, velocidade e tom da IA',
|
||||
'roleConfig.tooltip.language': 'Selecione o idioma da voz; o sistema filtrará vozes disponíveis que suportam esse idioma',
|
||||
'roleConfig.tooltip.voiceType': 'Escolha a voz para o agente de IA; diferentes vozes têm diferentes características e estilos. Algumas vozes suportam funcionalidade de visualização, clique no botão de reprodução para visualizar',
|
||||
|
||||
// Diálogo de gerenciamento de funções
|
||||
'functionDialog.title': 'Gerenciamento de Funções',
|
||||
'functionDialog.unselectedFunctions': 'Funções Não Selecionadas',
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': 'Ngôn ngữ tương tác',
|
||||
'roleConfig.vad': 'Phát hiện giọng nói',
|
||||
'roleConfig.asr': 'Nhận dạng giọng nói',
|
||||
'roleConfig.llm': 'Mô hình ngôn ngữ',
|
||||
'roleConfig.llm': 'Mô hình ngôn ngữ chính',
|
||||
'roleConfig.slm': 'Mô hình ngôn ngữ nhỏ',
|
||||
'roleConfig.vllm': 'Mô hình thị giác',
|
||||
'roleConfig.tts': 'Văn bản thành giọng nói',
|
||||
'roleConfig.memoryHis': 'Bộ nhớ',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': 'Không thể phát âm thanh',
|
||||
'roleConfig.audioPlayError': 'Lỗi trong quá trình phát âm thanh',
|
||||
|
||||
// Mô tả Tooltip cho trường biểu mẫu
|
||||
'roleConfig.tooltip.agentName': 'Đặt tên cho tác nhân AI của bạn để nhận dạng và nhận biết',
|
||||
'roleConfig.tooltip.roleTemplate': 'Chọn từ các mẫu vai trò được đặt trước để nhanh chóng định cấu hình cài đặt cơ bản cho tác nhân của bạn',
|
||||
'roleConfig.tooltip.contextProvider': 'Khi XiaoZhi được kích hoạt, lấy dữ liệu từ hệ thống bên ngoài và tiêm động vào lời nhắc hệ thống của LLM',
|
||||
'roleConfig.tooltip.roleIntroduction': 'Xác định vị trí vai trò, đặc điểm tính cách, quy tắc hành vi và nền tảng kiến thức chuyên môn của tác nhân AI',
|
||||
'roleConfig.tooltip.memoryHis': 'Tóm tắt nội dung bản ghi trò chuyện',
|
||||
'roleConfig.tooltip.languageCode': 'Đặt mã ngôn ngữ như vi-VN, en-US, v.v., được sử dụng để nhận dạng các tính năng cụ thể',
|
||||
'roleConfig.tooltip.interactionLanguage': 'Đặt ngôn ngữ tương tác, chỉ định ngôn ngữ chính mà tác nhân AI sử dụng để giao tiếp',
|
||||
'roleConfig.tooltip.vad': 'Phát hiện Hoạt động Giọng nói: Phát hiện khi người dùng bắt đầu hoặc ngừng nói, được sử dụng để xác định thời điểm bắt đầu và kết thúc cuộc trò chuyện, cho phép chức năng ngắt',
|
||||
'roleConfig.tooltip.asr': 'Nhận dạng Giọng nói Tự động: Chuyển đổi giọng nói của người dùng thành văn bản, bước đầu tiên trong hội thoại người-máy, hỗ trợ nhận dạng đa ngôn ngữ',
|
||||
'roleConfig.tooltip.llm': 'Mô hình ngôn ngữ chính (Large Language Model): "Bộ não" của tác nhân AI, chịu trách nhiệm hiểu ý định của người dùng, tạo phản hồi và thực hiện các nhiệm vụ khác nhau',
|
||||
'roleConfig.tooltip.slm': 'Mô hình tham số nhỏ (Small Language Model): Được sử dụng để kích hoạt tác nhân AI, tạo tiêu đề tóm tắt ký ức',
|
||||
'roleConfig.tooltip.vllm': 'Mô hình Ngôn ngữ Lớn Thị giác: Xử lý hiểu hình ảnh và video, cho phép tác nhân AI phân tích và mô tả nội dung được camera ghi lại',
|
||||
'roleConfig.tooltip.intent': 'Phát hiện Ý định: Phân tích giọng nói hoặc văn bản của người dùng để xác định ý định thực sự của người dùng như truy vấn, trò chuyện, điều khiển thiết bị, v.v.',
|
||||
'roleConfig.tooltip.memory': 'Mô hình Bộ nhớ: Quản lý lưu trữ và tóm tắt lịch sử cuộc trò chuyện, quyết định liệu AI có thể nhớ các cuộc trò chuyện trước đó để có trí nhớ dài hạn hay không',
|
||||
'roleConfig.tooltip.tts': 'Chuyển văn bản thành giọng nói: Chuyển đổi văn bản thành giọng nói tự nhiên, xác định giọng nói, tốc độ và ngữ điệu của AI',
|
||||
'roleConfig.tooltip.language': 'Chọn ngôn ngữ của giọng nói; hệ thống sẽ lọc các giọng nói khả dụng hỗ trợ ngôn ngữ đó',
|
||||
'roleConfig.tooltip.voiceType': 'Chọn giọng nói cho tác nhân AI; các giọng nói khác nhau có đặc điểm và phong cách khác nhau. Một số giọng nói hỗ trợ chức năng xem trước, nhấp vào nút phát để xem trước',
|
||||
|
||||
// Function management dialog text
|
||||
'functionDialog.title': 'Quản lý chức năng',
|
||||
'functionDialog.unselectedFunctions': 'Chức năng chưa chọn',
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': '交互语种',
|
||||
'roleConfig.vad': '语音活动检测(VAD)',
|
||||
'roleConfig.asr': '语音识别(ASR)',
|
||||
'roleConfig.llm': '大语言模型(LLM)',
|
||||
'roleConfig.llm': '主语言模型(LLM)',
|
||||
"roleConfig.slm": "小参数模型(SLM)",
|
||||
'roleConfig.vllm': '视觉大模型(VLLM)',
|
||||
'roleConfig.intent': '意图识别(Intent)',
|
||||
'roleConfig.memoryHis': '记忆',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': '无法播放音频',
|
||||
'roleConfig.audioPlayError': '播放音频过程出错',
|
||||
|
||||
// 表单字段 Tooltip 提示说明
|
||||
'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手',
|
||||
'roleConfig.tooltip.roleTemplate': '从预设的角色模板中选择,快速配置智能体的基础设定',
|
||||
'roleConfig.tooltip.contextProvider': '在小智被唤醒时,获取外部系统的数据,并将其动态注入到大模型的系统提示词中',
|
||||
'roleConfig.tooltip.roleIntroduction': '定义AI助手的角色定位、人格特征、行为规范和专业知识背景',
|
||||
'roleConfig.tooltip.memoryHis': '总结聊天记录内容',
|
||||
'roleConfig.tooltip.languageCode': '设置语言代码,如zh-CN、en-US等,用于特定功能识别',
|
||||
'roleConfig.tooltip.interactionLanguage': '设置交互语言,指定AI助手使用的主要语言进行交流',
|
||||
'roleConfig.tooltip.vad': '语音活动检测(Voice Activity Detection):检测用户何时开始或结束说话,用于判断对话的开始和结束,实现打断功能',
|
||||
'roleConfig.tooltip.asr': '自动语音识别(Automatic Speech Recognition):将用户的语音转换为文字,是人机对话的第一步,支持多语言识别',
|
||||
'roleConfig.tooltip.llm': '主语言模型(Large Language Model):AI助手的"大脑",负责理解用户意图、生成回答和执行各种任务',
|
||||
"roleConfig.tooltip.slm": '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题',
|
||||
'roleConfig.tooltip.vllm': '视觉大语言模型(Visual LLM):处理图像和视频理解,使AI助手能够分析和描述摄像头捕获的画面内容',
|
||||
'roleConfig.tooltip.intent': '意图识别(Intent Detection):分析用户语音或文本,判断用户的真实意图,如查询、聊天、控制设备等',
|
||||
'roleConfig.tooltip.memory': '记忆模型(Memory Model):管理对话历史的存储和摘要,决定AI能否记住之前的对话内容,实现长期记忆功能',
|
||||
'roleConfig.tooltip.tts': '语音合成(Text-to-Speech):将文字转换为自然语音,决定AI说话的声音、语速和语调',
|
||||
'roleConfig.tooltip.language': '选择音色所属的语言,系统将筛选出支持该语言的可用音色',
|
||||
'roleConfig.tooltip.voiceType': '选择AI助手说话的声音,不同音色具有不同的声音特点和风格,部分音色支持试听功能,点击播放按钮可预览效果',
|
||||
|
||||
// 功能管理对话框文本
|
||||
'functionDialog.title': '功能管理',
|
||||
'functionDialog.unselectedFunctions': '未选功能',
|
||||
|
||||
@@ -757,7 +757,8 @@ export default {
|
||||
'roleConfig.interactionLanguage': '交互語種',
|
||||
'roleConfig.vad': '語音活動檢測(VAD)',
|
||||
'roleConfig.asr': '語音識別(ASR)',
|
||||
'roleConfig.llm': '大語言模型(LLM)',
|
||||
'roleConfig.llm': '主語言模型(LLM)',
|
||||
'roleConfig.slm': '小參數模型(SLM)',
|
||||
'roleConfig.vllm': '視覺大模型(VLLM)',
|
||||
'roleConfig.tts': '語音合成(TTS)',
|
||||
'roleConfig.memoryHis': '記憶',
|
||||
@@ -802,6 +803,25 @@ export default {
|
||||
'roleConfig.cannotPlayAudio': '無法播放音訊',
|
||||
'roleConfig.audioPlayError': '播放音訊過程出錯',
|
||||
|
||||
// 表單欄位 Tooltip 提示說明
|
||||
'roleConfig.tooltip.agentName': '設定智慧體的名稱,用於標識和識別您的AI助手',
|
||||
'roleConfig.tooltip.roleTemplate': '從預設的角色模板中選擇,快速配置智慧體的基礎設定',
|
||||
'roleConfig.tooltip.contextProvider': '在小智被喚醒時,獲取外部系統的資料,並將其動態注入到大模型的系統提示詞中',
|
||||
'roleConfig.tooltip.roleIntroduction': '定義AI助手的角色定位、人格特徵、行為規範和專業知識背景',
|
||||
'roleConfig.tooltip.memoryHis': '總結聊天記錄內容',
|
||||
'roleConfig.tooltip.languageCode': '設定語言代碼,如zh-TW、en-US等,用於特定功能識別',
|
||||
'roleConfig.tooltip.interactionLanguage': '設定互動語言,指定AI助手使用的主要語言進行交流',
|
||||
'roleConfig.tooltip.vad': '語音活動檢測(Voice Activity Detection):檢測用戶何時開始或結束說話,用於判斷對話的開始和結束,實現打斷功能',
|
||||
'roleConfig.tooltip.asr': '自動語音識別(Automatic Speech Recognition):將用戶的語音轉換為文字,是人機對話的第一步,支持多語言識別',
|
||||
'roleConfig.tooltip.llm': '主語言模型(Large Language Model):AI助手的"大腦",負責理解用戶意圖、生成回答和執行各種任務',
|
||||
'roleConfig.tooltip.slm': '小參數模型(Small Language Model):用於智慧體喚醒,生成記憶總結標題',
|
||||
'roleConfig.tooltip.vllm': '視覺大型語言模型(Visual LLM):處理圖像和視頻理解,使AI助手能夠分析和描述攝像頭擷取的畫面內容',
|
||||
'roleConfig.tooltip.intent': '意圖識別(Intent Detection):分析用戶語音或文字,判斷用戶的真實意圖,如查詢、聊天、控制設備等',
|
||||
'roleConfig.tooltip.memory': '記憶模型(Memory Model):管理對話歷史的存儲和摘要,決定AI能否記住之前的對話內容,實現長期記憶功能',
|
||||
'roleConfig.tooltip.tts': '語音合成(Text-to-Speech):將文字轉換為自然語音,決定AI說話的聲音、語速和語調',
|
||||
'roleConfig.tooltip.language': '選擇音色所屬的語言,系統將篩選出支援該語言的可用音色',
|
||||
'roleConfig.tooltip.voiceType': '選擇AI助手說話的聲音,不同音色具有不同的聲音特點和風格,部分音色支援預覽功能,點擊播放按鈕可預覽效果',
|
||||
|
||||
// 功能管理對話框文本
|
||||
'functionDialog.title': '功能管理',
|
||||
'functionDialog.unselectedFunctions': '未選功能',
|
||||
|
||||
@@ -60,14 +60,24 @@
|
||||
<div class="form-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item :label="$t('roleConfig.agentName') + ':'">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.agentName')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.agentName') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.agentName"
|
||||
class="form-input"
|
||||
maxlength="64"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('roleConfig.roleTemplate') + ':'">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.roleTemplate')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.roleTemplate') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="template-container">
|
||||
<div
|
||||
v-for="(template, index) in templates"
|
||||
@@ -80,7 +90,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('roleConfig.contextProvider') + ':'" class="context-provider-item">
|
||||
<el-form-item class="context-provider-item">
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.contextProvider')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.contextProvider') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="color: #606266; font-size: 13px;">
|
||||
{{ $t('roleConfig.contextProviderSuccess', { count: currentContextProviders.length }) }}<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/context-provider-integration.md" target="_blank" class="doc-link">{{ $t('roleConfig.contextProviderDocLink') }}</a>
|
||||
@@ -94,7 +109,12 @@
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.roleIntroduction')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.roleIntroduction') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="8"
|
||||
@@ -107,7 +127,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('roleConfig.memoryHis') + ':'">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.memoryHis')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.memoryHis') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
type="textarea"
|
||||
rows="4"
|
||||
@@ -120,9 +145,13 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('roleConfig.languageCode') + ':'"
|
||||
style="display: none"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.languageCode')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.languageCode') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.langCode"
|
||||
:placeholder="$t('roleConfig.pleaseEnterLangCode')"
|
||||
@@ -132,9 +161,13 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('roleConfig.interactionLanguage') + ':'"
|
||||
style="display: none"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.interactionLanguage')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.interactionLanguage') }}:</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="form.language"
|
||||
:placeholder="$t('roleConfig.pleaseEnterLangName')"
|
||||
@@ -148,9 +181,13 @@
|
||||
<div class="model-row">
|
||||
<el-form-item
|
||||
v-if="featureStatus.vad"
|
||||
:label="$t('roleConfig.vad')"
|
||||
class="model-item"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.vad')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.vad') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.vadModelId"
|
||||
@@ -170,9 +207,13 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="featureStatus.asr"
|
||||
:label="$t('roleConfig.asr')"
|
||||
class="model-item"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.asr')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.asr') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.asrModelId"
|
||||
@@ -191,12 +232,63 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="model-row">
|
||||
<el-form-item class="model-item">
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.llm')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.llm') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.llmModelId"
|
||||
filterable
|
||||
:placeholder="$t('roleConfig.pleaseSelect')"
|
||||
class="form-select"
|
||||
@change="handleModelChange('LLM', $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, optionIndex) in modelOptions['LLM']"
|
||||
:key="`option-asr-${optionIndex}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="model-item">
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.slm')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.slm') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model.slmModelId"
|
||||
filterable
|
||||
:placeholder="$t('roleConfig.pleaseSelect')"
|
||||
class="form-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, optionIndex) in modelOptions['LLM']"
|
||||
:key="`option-asr-${optionIndex}`"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item
|
||||
v-for="(model, index) in models.slice(2)"
|
||||
v-for="(model, index) in models.slice(4)"
|
||||
:key="`model-${index}`"
|
||||
:label="$t('roleConfig.' + model.type.toLowerCase())"
|
||||
class="model-item"
|
||||
>
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.' + model.type.toLowerCase())" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.' + model.type.toLowerCase()) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.model[model.key]"
|
||||
@@ -217,9 +309,8 @@
|
||||
<el-tooltip
|
||||
v-for="func in currentFunctions"
|
||||
:key="func.name"
|
||||
effect="dark"
|
||||
effect="light"
|
||||
placement="top"
|
||||
popper-class="custom-tooltip"
|
||||
>
|
||||
<div slot="content">
|
||||
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||
@@ -259,7 +350,12 @@
|
||||
</el-form-item>
|
||||
<div class="model-row">
|
||||
<!-- 语言筛选器 -->
|
||||
<el-form-item :label="$t('roleConfig.language')" class="model-item language-select-item">
|
||||
<el-form-item class="model-item language-select-item">
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.language')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.language') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="selectedLanguage"
|
||||
@@ -278,7 +374,12 @@
|
||||
</el-form-item>
|
||||
|
||||
<!-- 音色选择器 -->
|
||||
<el-form-item :label="$t('roleConfig.voiceType')" class="model-item">
|
||||
<el-form-item class="model-item">
|
||||
<template #label>
|
||||
<el-tooltip :content="$t('roleConfig.tooltip.voiceType')" placement="top" effect="light" popper-class="custom-tooltip">
|
||||
<span>{{ $t('roleConfig.voiceType') }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select
|
||||
v-model="form.ttsVoiceId"
|
||||
@@ -404,6 +505,7 @@ export default {
|
||||
vadModelId: "",
|
||||
asrModelId: "",
|
||||
llmModelId: "",
|
||||
slmModelId: "",
|
||||
vllmModelId: "",
|
||||
memModelId: "",
|
||||
intentModelId: "",
|
||||
@@ -413,6 +515,7 @@ export default {
|
||||
{ label: this.$t("roleConfig.vad"), key: "vadModelId", type: "VAD" },
|
||||
{ label: this.$t("roleConfig.asr"), key: "asrModelId", type: "ASR" },
|
||||
{ label: this.$t("roleConfig.llm"), key: "llmModelId", type: "LLM" },
|
||||
{ label: this.$t("roleConfig.slm"), key: "slmModelId", type: "SLM" },
|
||||
{ label: this.$t("roleConfig.vllm"), key: "vllmModelId", type: "VLLM" },
|
||||
{ label: this.$t("roleConfig.intent"), key: "intentModelId", type: "Intent" },
|
||||
{ label: this.$t("roleConfig.memory"), key: "memModelId", type: "Memory" },
|
||||
@@ -464,6 +567,7 @@ export default {
|
||||
asrModelId: this.form.model.asrModelId,
|
||||
vadModelId: this.form.model.vadModelId,
|
||||
llmModelId: this.form.model.llmModelId,
|
||||
slmModelId: this.form.model.slmModelId,
|
||||
vllmModelId: this.form.model.vllmModelId,
|
||||
ttsModelId: this.form.model.ttsModelId,
|
||||
ttsVoiceId: this.form.ttsVoiceId,
|
||||
@@ -532,6 +636,7 @@ export default {
|
||||
vadModelId: "",
|
||||
asrModelId: "",
|
||||
llmModelId: "",
|
||||
slmModelId: "",
|
||||
vllmModelId: "",
|
||||
memModelId: "",
|
||||
intentModelId: "",
|
||||
@@ -588,6 +693,7 @@ export default {
|
||||
vadModelId: templateData.vadModelId || this.form.model.vadModelId,
|
||||
asrModelId: templateData.asrModelId || this.form.model.asrModelId,
|
||||
llmModelId: templateData.llmModelId || this.form.model.llmModelId,
|
||||
slmModelId: templateData.llmModelId || this.form.model.slmModelId,
|
||||
vllmModelId: templateData.vllmModelId || this.form.model.vllmModelId,
|
||||
memModelId: templateData.memModelId || this.form.model.memModelId,
|
||||
intentModelId: templateData.intentModelId || this.form.model.intentModelId,
|
||||
@@ -606,6 +712,7 @@ export default {
|
||||
vadModelId: data.data.vadModelId,
|
||||
asrModelId: data.data.asrModelId,
|
||||
llmModelId: data.data.llmModelId,
|
||||
slmModelId: data.data.slmModelId,
|
||||
vllmModelId: data.data.vllmModelId,
|
||||
memModelId: data.data.memModelId,
|
||||
intentModelId: data.data.intentModelId,
|
||||
@@ -1753,4 +1860,12 @@ export default {
|
||||
width: 90px !important;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.custom-tooltip {
|
||||
max-width: 400px !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user