mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 |
@@ -158,6 +158,7 @@ my_wakeup_words.mp3
|
|||||||
!main/xiaozhi-server/config/assets/bind_code.wav
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
|
|||||||
@@ -163,4 +163,9 @@ public interface Constant {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
public static final String VERSION = "0.3.7";
|
||||||
}
|
}
|
||||||
@@ -75,4 +75,11 @@ public class RedisKeys {
|
|||||||
public static String getTimbreDetailsKey(String id) {
|
public static String getTimbreDetailsKey(String id) {
|
||||||
return "timbre:details:" + id;
|
return "timbre:details:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本号Key
|
||||||
|
*/
|
||||||
|
public static String getVersionKey() {
|
||||||
|
return "system:version";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,7 +30,7 @@ public class RedisUtils {
|
|||||||
/**
|
/**
|
||||||
* 过期时长为1小时,单位:秒
|
* 过期时长为1小时,单位:秒
|
||||||
*/
|
*/
|
||||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||||
/**
|
/**
|
||||||
* 过期时长为6小时,单位:秒
|
* 过期时长为6小时,单位:秒
|
||||||
*/
|
*/
|
||||||
@@ -124,4 +127,24 @@ public class RedisUtils {
|
|||||||
public Object rightPop(String key) {
|
public Object rightPop(String key) {
|
||||||
return redisTemplate.opsForList().rightPop(key);
|
return redisTemplate.opsForList().rightPop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Redis 数据库中的所有键
|
||||||
|
*/
|
||||||
|
public void emptyAll() {
|
||||||
|
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||||
|
String luaScript ="redis.call('FLUSHALL')";
|
||||||
|
|
||||||
|
// 创建 DefaultRedisScript 对象
|
||||||
|
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||||
|
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||||
|
|
||||||
|
// 执行 Lua 脚本
|
||||||
|
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||||
|
redisTemplate.execute(redisScript, keys);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -18,8 +21,20 @@ public class SystemInitConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ConfigService configService;
|
private ConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
// 检查版本号
|
||||||
|
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||||
|
if (!Constant.VERSION.equals(redisVersion)) {
|
||||||
|
// 如果版本不一致,清空Redis
|
||||||
|
redisUtils.emptyAll();
|
||||||
|
// 存储新版本号
|
||||||
|
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
sysParamsService.initServerSecret();
|
sysParamsService.initServerSecret();
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -91,6 +91,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取智能体信息
|
// 获取智能体信息
|
||||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
@@ -104,7 +105,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 获取单台设备每天最多输出字数
|
||||||
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
@@ -267,6 +270,12 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
intentLLMModelId = null;
|
intentLLMModelId = null;
|
||||||
}
|
}
|
||||||
|
} else if ("function_call".equals(map.get("type"))) {
|
||||||
|
String functionStr = (String) map.get("functions");
|
||||||
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
|
String[] functions = functionStr.split("\\;");
|
||||||
|
map.put("functions", functions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
@@ -121,7 +122,7 @@ public class LoginController {
|
|||||||
@Operation(summary = "公共配置")
|
@Operation(summary = "公共配置")
|
||||||
public Result<Map<String, Object>> pubConfig() {
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
Map<String, Object> config = new HashMap<>();
|
Map<String, Object> config = new HashMap<>();
|
||||||
config.put("version", "0.3.6");
|
config.put("version", Constant.VERSION);
|
||||||
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
return new Result<Map<String, Object>>().ok(config);
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 调整意图识别配置
|
||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加单台设备每天最多聊天句数
|
||||||
|
delete from `sys_params` where id = 105;
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (105, 'device_max_output_size', '0', 'number', 1, '单台设备每天最多输出字数,0表示不限制');
|
||||||
@@ -58,3 +58,10 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504151206.sql
|
path: classpath:db/changelog/202504151206.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504181536
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504181536.sql
|
||||||
|
|||||||
Binary file not shown.
@@ -3,7 +3,7 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.3.6"
|
SERVER_VERSION = "0.3.7"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from core.auth import AuthMiddleware, AuthenticationError
|
|||||||
from core.mcp.manager import MCPManager
|
from core.mcp.manager import MCPManager
|
||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
||||||
|
from core.utils.output_counter import add_device_output
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ class ConnectionHandler:
|
|||||||
self.session_id = None
|
self.session_id = None
|
||||||
self.prompt = None
|
self.prompt = None
|
||||||
self.welcome_msg = None
|
self.welcome_msg = None
|
||||||
|
self.max_output_size = 0
|
||||||
|
|
||||||
# 客户端状态相关
|
# 客户端状态相关
|
||||||
self.client_abort = False
|
self.client_abort = False
|
||||||
@@ -214,8 +216,11 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
def _initialize_components(self, private_config):
|
def _initialize_components(self, private_config):
|
||||||
"""初始化组件"""
|
"""初始化组件"""
|
||||||
self._initialize_models(private_config)
|
if private_config is not None:
|
||||||
|
self._initialize_models(private_config)
|
||||||
|
else:
|
||||||
|
self.prompt = self.config["prompt"]
|
||||||
|
self.change_system_prompt(self.prompt)
|
||||||
"""加载记忆"""
|
"""加载记忆"""
|
||||||
self._initialize_memory()
|
self._initialize_memory()
|
||||||
"""加载意图识别"""
|
"""加载意图识别"""
|
||||||
@@ -312,7 +317,6 @@ class ConnectionHandler:
|
|||||||
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
||||||
"LLM"
|
"LLM"
|
||||||
]
|
]
|
||||||
|
|
||||||
if private_config.get("Memory", None) is not None:
|
if private_config.get("Memory", None) is not None:
|
||||||
init_memory = True
|
init_memory = True
|
||||||
self.config["Memory"] = private_config["Memory"]
|
self.config["Memory"] = private_config["Memory"]
|
||||||
@@ -325,6 +329,8 @@ class ConnectionHandler:
|
|||||||
self.config["selected_module"]["Intent"] = private_config[
|
self.config["selected_module"]["Intent"] = private_config[
|
||||||
"selected_module"
|
"selected_module"
|
||||||
]["Intent"]
|
]["Intent"]
|
||||||
|
if private_config.get("device_max_output_size", None) is not None:
|
||||||
|
self.max_output_size = int(private_config["device_max_output_size"])
|
||||||
try:
|
try:
|
||||||
modules = initialize_modules(
|
modules = initialize_modules(
|
||||||
self.logger,
|
self.logger,
|
||||||
@@ -842,6 +848,8 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
|
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
|
||||||
return None, text, text_index
|
return None, text, text_index
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
|
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
|
||||||
|
if self.max_output_size > 0:
|
||||||
|
add_device_output(self.headers.get("device-id"), len(text))
|
||||||
return tts_file, text, text_index
|
return tts_file, text, text_index
|
||||||
|
|
||||||
def clearSpeakStatus(self):
|
def clearSpeakStatus(self):
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import time
|
import time
|
||||||
import asyncio
|
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.handle.intentHandler import handle_user_intent
|
from core.handle.intentHandler import handle_user_intent
|
||||||
|
from core.utils.output_counter import check_device_output_limit
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -51,6 +51,15 @@ async def startToChat(conn, text):
|
|||||||
if conn.need_bind:
|
if conn.need_bind:
|
||||||
await check_bind_device(conn)
|
await check_bind_device(conn)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 如果当日的输出字数大于限定的字数
|
||||||
|
if conn.max_output_size > 0:
|
||||||
|
if check_device_output_limit(
|
||||||
|
conn.headers.get("device-id"), conn.max_output_size
|
||||||
|
):
|
||||||
|
await max_out_size(conn)
|
||||||
|
return
|
||||||
|
|
||||||
# 首先进行意图分析
|
# 首先进行意图分析
|
||||||
intent_handled = await handle_user_intent(conn, text)
|
intent_handled = await handle_user_intent(conn, text)
|
||||||
|
|
||||||
@@ -89,6 +98,18 @@ async def no_voice_close_connect(conn):
|
|||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|
||||||
|
|
||||||
|
async def max_out_size(conn):
|
||||||
|
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 0
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
file_path = "config/assets/max_output_size.wav"
|
||||||
|
opus_packets, _ = conn.tts.audio_to_opus_data(file_path)
|
||||||
|
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||||
|
conn.close_after_chat = True
|
||||||
|
|
||||||
|
|
||||||
async def check_bind_device(conn):
|
async def check_bind_device(conn):
|
||||||
if conn.bind_code:
|
if conn.bind_code:
|
||||||
# 确保bind_code是6位数字
|
# 确保bind_code是6位数字
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
# 全局字典,用于存储每个设备的每日输出字数
|
||||||
|
_device_daily_output: Dict[Tuple[str, datetime.date], int] = {}
|
||||||
|
# 记录最后一次检查的日期
|
||||||
|
_last_check_date: datetime.date = None
|
||||||
|
|
||||||
|
|
||||||
|
def reset_device_output():
|
||||||
|
"""
|
||||||
|
重置所有设备的每日输出字数
|
||||||
|
每天0点调用此函数
|
||||||
|
"""
|
||||||
|
_device_daily_output.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_output(device_id: str) -> int:
|
||||||
|
"""
|
||||||
|
获取设备当日的输出字数
|
||||||
|
"""
|
||||||
|
current_date = datetime.datetime.now().date()
|
||||||
|
return _device_daily_output.get((device_id, current_date), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def add_device_output(device_id: str, char_count: int):
|
||||||
|
"""
|
||||||
|
增加设备的输出字数
|
||||||
|
"""
|
||||||
|
current_date = datetime.datetime.now().date()
|
||||||
|
global _last_check_date
|
||||||
|
|
||||||
|
# 如果是第一次调用或者日期发生变化,清空计数器
|
||||||
|
if _last_check_date is None or _last_check_date != current_date:
|
||||||
|
_device_daily_output.clear()
|
||||||
|
_last_check_date = current_date
|
||||||
|
|
||||||
|
current_count = _device_daily_output.get((device_id, current_date), 0)
|
||||||
|
_device_daily_output[(device_id, current_date)] = current_count + char_count
|
||||||
|
|
||||||
|
|
||||||
|
def check_device_output_limit(device_id: str, max_output_size: int) -> bool:
|
||||||
|
"""
|
||||||
|
检查设备是否超过输出限制
|
||||||
|
:return: True 如果超过限制,False 如果未超过
|
||||||
|
"""
|
||||||
|
if not device_id:
|
||||||
|
return False
|
||||||
|
current_output = get_device_output(device_id)
|
||||||
|
return current_output >= max_output_size
|
||||||
Reference in New Issue
Block a user