Compare commits

..
7 Commits
Author SHA1 Message Date
hrzandGitHub 8b9174b67e update:绑定设备时,板子类型优先获取品牌商版名称 (#903)
* update:Server端兼容老设备缺失"client-id"请求头

* update:优化iot日志

* update:绑定设备时,板子类型优先获取品牌商版名称

* update:更新版本号
2025-04-20 01:40:21 +08:00
hrzandGitHub 6c04a6cf69 update:Server端兼容老设备缺失"client-id"请求头 (#902)
* update:Server端兼容老设备缺失"client-id"请求头

* update:优化iot日志

* update:绑定设备时,板子类型优先获取品牌商版名称
2025-04-20 01:28:01 +08:00
hrzandGitHub 14a63c4b97 update:补齐ddos攻击提示音 (#896) 2025-04-19 13:24:03 +08:00
hrzandGitHub 098d13a34b update:更新0.3.7 (#892) 2025-04-19 00:11:12 +08:00
488f247744 update:增加单台设备每天最多聊天字数,防止被ddos
* 添加清空redis所有库的接口
--AdminController.java 添加了清除所有的接口
--RedisUtils.java 添加了清除redis所有key的方法,redisTemplate提供的清空方法已经被标记为弃用了,所有选择用执行lua脚本方式

* fix:修复使用本地配置时忘记附带提示词

* fix:修复意图识别插件名称格式bug

* update:版本升级后强制刷新redis

* update:增加单台设备每天最多聊天字数,防止被ddos

---------

Co-authored-by: 剑雨 <2375294554@qq.com>
2025-04-18 23:39:56 +08:00
hrzandGitHub c74dbf03cd fix:修复意图识别插件名称格式bug (#885)
* fix:修复使用本地配置时忘记附带提示词

* fix:修复意图识别插件名称格式bug
2025-04-18 16:39:18 +08:00
hrzandGitHub 1f836c3235 fix:修复使用本地配置时忘记附带提示词 (#880) 2025-04-18 11:38:31 +08:00
18 changed files with 225 additions and 47 deletions
+1
View File
@@ -158,6 +158,7 @@ my_wakeup_words.mp3
!main/xiaozhi-server/config/assets/bind_code.wav
!main/xiaozhi-server/config/assets/bind_not_found.wav
!main/xiaozhi-server/config/assets/bind_code/*.wav
!main/xiaozhi-server/config/assets/max_output_size.wav
main/manager-api/.vscode
# Ignore webpack cache directory
@@ -163,4 +163,9 @@ public interface Constant {
return value;
}
}
/**
* 版本号
*/
public static final String VERSION = "0.3.8";
}
@@ -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";
}
}
@@ -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<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 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);
}
@@ -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<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");
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
@@ -267,6 +270,12 @@ public class ConfigServiceImpl implements ConfigService {
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
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不为空,则添加附加模型
@@ -260,8 +260,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
dataMap.put("id", deviceId);
dataMap.put("mac_address", deviceId);
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
? deviceReport.getBoard().getType()
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
dataMap.put("app_version", (deviceReport.getApplication() != null)
? deviceReport.getApplication().getVersion()
: null);
@@ -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<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>();
config.put("version", "0.3.6");
config.put("version", Constant.VERSION);
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
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:
encoding: utf8
path: classpath:db/changelog/202504151206.sql
- changeSet:
id: 202504181536
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504181536.sql
+1 -1
View File
@@ -3,7 +3,7 @@ import sys
from loguru import logger
from config.config_loader import load_config
SERVER_VERSION = "0.3.6"
SERVER_VERSION = "0.3.8"
def get_module_abbreviation(module_name, module_dict):
+14 -6
View File
@@ -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
@@ -214,8 +216,11 @@ class ConnectionHandler:
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()
"""加载意图识别"""
@@ -238,8 +243,8 @@ class ConnectionHandler:
begin_time = time.time()
private_config = get_private_config_from_api(
self.config,
self.headers.get("device-id", None),
self.headers.get("client-id", None),
self.headers.get("device-id"),
self.headers.get("client-id", self.headers.get("device-id")),
)
private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
self.logger.bind(tag=TAG).info(
@@ -312,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"]
@@ -325,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,
@@ -616,7 +622,7 @@ class ConnectionHandler:
)
if not bHasError:
response_message.clear()
self.logger.bind(tag=TAG).info(
self.logger.bind(tag=TAG).debug(
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}"
)
function_call_data = {
@@ -842,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):
@@ -79,7 +79,7 @@ class FunctionHandler:
func = funcItem.func
arguments = function_call_data["arguments"]
arguments = json.loads(arguments) if arguments else {}
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
logger.bind(tag=TAG).debug(f"调用函数: {function_name}, 参数: {arguments}")
if (
funcItem.type == ToolType.SYSTEM_CTL
or funcItem.type == ToolType.IOT_CTL
+54 -33
View File
@@ -57,7 +57,7 @@ def create_iot_function(device_name, method_name, method_info):
response_failure = "操作失败"
# 打印响应参数
logger.bind(tag=TAG).info(
logger.bind(tag=TAG).debug(
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
)
@@ -146,7 +146,7 @@ class IotDescriptor:
# 根据描述创建属性
if properties is not None:
for key, value in properties.items():
property_item = globals()[key] = {}
property_item = {}
property_item["name"] = key
property_item["description"] = value["description"]
if value["type"] == "number":
@@ -160,18 +160,17 @@ class IotDescriptor:
# 根据描述创建方法
if methods is not None:
for key, value in methods.items():
method = globals()[key] = {}
method = {}
method["description"] = value["description"]
method["name"] = key
for k, v in value["parameters"].items():
method[k] = {}
method[k]["description"] = v["description"]
if v["type"] == "number":
method[k]["value"] = 0
elif v["type"] == "boolean":
method[k]["value"] = False
else:
method[k]["value"] = ""
# 检查方法是否有参数
if "parameters" in value:
method["parameters"] = {}
for k, v in value["parameters"].items():
method["parameters"][k] = {
"description": v["description"],
"type": v["type"],
}
self.methods.append(method)
@@ -221,13 +220,19 @@ def register_device_type(descriptor):
func_name = f"{device_name.lower()}_{method_name.lower()}"
# 创建参数字典,添加原有参数
parameters = {
param_name: {
"type": param_info["type"],
"description": param_info["description"],
parameters = {}
required_params = []
# 如果方法有参数,则添加参数信息
if "parameters" in method_info:
parameters = {
param_name: {
"type": param_info["type"],
"description": param_info["description"],
}
for param_name, param_info in method_info["parameters"].items()
}
for param_name, param_info in method_info["parameters"].items()
}
required_params = list(method_info["parameters"].keys())
# 添加响应参数
parameters.update(
@@ -244,7 +249,6 @@ def register_device_type(descriptor):
)
# 构建必须参数列表(原有参数 + 响应参数)
required_params = list(method_info["parameters"].keys())
required_params.extend(["response_success", "response_failure"])
func_desc = {
@@ -282,6 +286,25 @@ async def handleIotDescriptors(conn, descriptors):
functions_changed = False
for descriptor in descriptors:
# 如果descriptor没有properties和methods,则直接跳过
if "properties" not in descriptor and "methods" not in descriptor:
continue
# 处理缺失properties的情况
if "properties" not in descriptor:
descriptor["properties"] = {}
# 从methods中提取所有参数作为properties
if "methods" in descriptor:
for method_name, method_info in descriptor["methods"].items():
if "parameters" in method_info:
for param_name, param_info in method_info["parameters"].items():
# 将参数信息转换为属性信息
descriptor["properties"][param_name] = {
"description": param_info["description"],
"type": param_info["type"],
}
# 创建IOT设备描述符
iot_descriptor = IotDescriptor(
descriptor["name"],
@@ -375,19 +398,17 @@ async def send_iot_conn(conn, name, method_name, parameters):
for method in value.methods:
# 找到了方法
if method["name"] == method_name:
await conn.websocket.send(
json.dumps(
{
"type": "iot",
"commands": [
{
"name": name,
"method": method_name,
"parameters": parameters,
}
],
}
)
)
# 构建命令对象
command = {
"name": name,
"method": method_name,
}
# 只有当参数不为空时才添加parameters字段
if parameters:
command["parameters"] = parameters
send_message = json.dumps({"type": "iot", "commands": [command]})
await conn.websocket.send(send_message)
logger.bind(tag=TAG).info(f"发送物联网指令: {send_message}")
return
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
@@ -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位数字
@@ -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