From 488f24774487440c3593c1f3bce749534157b550 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Fri, 18 Apr 2025 23:39:56 +0800 Subject: [PATCH] =?UTF-8?q?update:=E5=A2=9E=E5=8A=A0=E5=8D=95=E5=8F=B0?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=AF=8F=E5=A4=A9=E6=9C=80=E5=A4=9A=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E5=AD=97=E6=95=B0=EF=BC=8C=E9=98=B2=E6=AD=A2=E8=A2=AB?= =?UTF-8?q?ddos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加清空redis所有库的接口 --AdminController.java 添加了清除所有的接口 --RedisUtils.java 添加了清除redis所有key的方法,redisTemplate提供的清空方法已经被标记为弃用了,所有选择用执行lua脚本方式 * fix:修复使用本地配置时忘记附带提示词 * fix:修复意图识别插件名称格式bug * update:版本升级后强制刷新redis * update:增加单台设备每天最多聊天字数,防止被ddos --------- Co-authored-by: 剑雨 <2375294554@qq.com> --- .../xiaozhi/common/constant/Constant.java | 5 ++ .../java/xiaozhi/common/redis/RedisKeys.java | 7 +++ .../java/xiaozhi/common/redis/RedisUtils.java | 25 +++++++++- .../modules/config/init/SystemInitConfig.java | 15 ++++++ .../service/impl/ConfigServiceImpl.java | 5 +- .../security/controller/LoginController.java | 3 +- .../resources/db/changelog/202504181536.sql | 7 +++ .../db/changelog/db.changelog-master.yaml | 4 +- main/xiaozhi-server/core/connection.py | 7 ++- .../core/handle/receiveAudioHandle.py | 23 ++++++++- .../core/utils/output_counter.py | 50 +++++++++++++++++++ 11 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202504181536.sql create mode 100644 main/xiaozhi-server/core/utils/output_counter.py diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 1ff70fd4..522ceb27 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -163,4 +163,9 @@ public interface Constant { return value; } } + + /** + * 版本号 + */ + public static final String VERSION = "0.3.6"; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 7bc0c48d..8c266ce4 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -75,4 +75,11 @@ public class RedisKeys { public static String getTimbreDetailsKey(String id) { return "timbre:details:" + id; } + + /** + * 获取版本号Key + */ + public static String getVersionKey() { + return "system:version"; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java index 240e1339..9115e806 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisUtils.java @@ -1,11 +1,14 @@ package xiaozhi.common.redis; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import jakarta.annotation.Resource; @@ -27,7 +30,7 @@ public class RedisUtils { /** * 过期时长为1小时,单位:秒 */ - public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L; + public final static long HOUR_ONE_EXPIRE = (long) 60 * 60; /** * 过期时长为6小时,单位:秒 */ @@ -124,4 +127,24 @@ public class RedisUtils { public Object rightPop(String key) { return redisTemplate.opsForList().rightPop(key); } + + + /** + * 清空所有 Redis 数据库中的所有键 + */ + public void emptyAll() { + // Lua 脚本 FLUSHALL是redis清空所有库的命令 + String luaScript ="redis.call('FLUSHALL')"; + + // 创建 DefaultRedisScript 对象 + DefaultRedisScript redisScript = new DefaultRedisScript<>(); + redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容 + redisScript.setResultType(Void.class); // 设置返回值类型 + + // 执行 Lua 脚本 + List keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表 + redisTemplate.execute(redisScript, keys); + + } + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java index a8e3794e..e47cd0c7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/init/SystemInitConfig.java @@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; 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.sys.service.SysParamsService; @@ -18,8 +21,20 @@ public class SystemInitConfig { @Autowired private ConfigService configService; + @Autowired + private RedisUtils redisUtils; + @PostConstruct 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(); configService.getConfig(false); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index dd2a2881..f7c06fdb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -91,6 +91,7 @@ public class ConfigServiceImpl implements ConfigService { } throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device"); } + // 获取智能体信息 AgentEntity agent = agentService.getAgentById(device.getAgentId()); if (agent == null) { @@ -104,7 +105,9 @@ public class ConfigServiceImpl implements ConfigService { } // 构建返回数据 Map 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"); if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java index 68e84139..7cbaa8af 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/controller/LoginController.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.page.TokenDTO; @@ -121,7 +122,7 @@ public class LoginController { @Operation(summary = "公共配置") public Result> pubConfig() { Map config = new HashMap<>(); - config.put("version", "0.3.6"); + config.put("version", Constant.VERSION); config.put("allowUserRegister", sysUserService.getAllowUserRegister()); return new Result>().ok(config); } diff --git a/main/manager-api/src/main/resources/db/changelog/202504181536.sql b/main/manager-api/src/main/resources/db/changelog/202504181536.sql new file mode 100644 index 00000000..327aa605 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504181536.sql @@ -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表示不限制'); \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index a2a322a8..09136dc4 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -59,9 +59,9 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202504151206.sql - changeSet: - id: 202504181534 + id: 202504181536 author: John changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504181534.sql \ No newline at end of file + path: classpath:db/changelog/202504181536.sql diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index c77de638..6b2de546 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -29,6 +29,7 @@ from core.auth import AuthMiddleware, AuthenticationError from core.mcp.manager import MCPManager from config.config_loader import get_private_config_from_api from config.manage_api_client import DeviceNotFoundException, DeviceBindException +from core.utils.output_counter import add_device_output TAG = __name__ @@ -57,6 +58,7 @@ class ConnectionHandler: self.session_id = None self.prompt = None self.welcome_msg = None + self.max_output_size = 0 # 客户端状态相关 self.client_abort = False @@ -315,7 +317,6 @@ class ConnectionHandler: self.config["selected_module"]["LLM"] = private_config["selected_module"][ "LLM" ] - if private_config.get("Memory", None) is not None: init_memory = True self.config["Memory"] = private_config["Memory"] @@ -328,6 +329,8 @@ class ConnectionHandler: self.config["selected_module"]["Intent"] = private_config[ "selected_module" ]["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: modules = initialize_modules( self.logger, @@ -845,6 +848,8 @@ class ConnectionHandler: self.logger.bind(tag=TAG).error(f"tts转换失败,{text}") return None, text, text_index 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 def clearSpeakStatus(self): diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index 09f342dc..485544f7 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -1,9 +1,9 @@ from config.logger import setup_logging import time -import asyncio from core.utils.util import remove_punctuation_and_length from core.handle.sendAudioHandle import send_stt_message from core.handle.intentHandler import handle_user_intent +from core.utils.output_counter import check_device_output_limit TAG = __name__ logger = setup_logging() @@ -51,6 +51,15 @@ async def startToChat(conn, text): if conn.need_bind: await check_bind_device(conn) 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) @@ -89,6 +98,18 @@ async def no_voice_close_connect(conn): 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): if conn.bind_code: # 确保bind_code是6位数字 diff --git a/main/xiaozhi-server/core/utils/output_counter.py b/main/xiaozhi-server/core/utils/output_counter.py new file mode 100644 index 00000000..1e7ae8df --- /dev/null +++ b/main/xiaozhi-server/core/utils/output_counter.py @@ -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