From 766a7062505a4c1179e8f90865544f16a08fb95d Mon Sep 17 00:00:00 2001 From: LFNL-scholar Date: Tue, 8 Apr 2025 20:37:23 +0800 Subject: [PATCH 01/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91=E7=9A=84ASR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 78 +++--- .../core/providers/asr/aliyun.py | 265 ++++++++++++++++++ 2 files changed, 311 insertions(+), 32 deletions(-) create mode 100644 main/xiaozhi-server/core/providers/asr/aliyun.py diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index f1ac85c0..ad85c712 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -86,35 +86,6 @@ wakeup_words: - "小滨小滨" - "小冰小冰" -# 插件的基础配置 -plugins: - # 获取天气插件的配置,这里填写你的api_key - # 这个密钥是项目共用的key,用多了可能会被限制 - # 想稳定一点就自行申请替换,每天有1000次免费调用 - # 申请地址:https://console.qweather.com/#/apps/create-key/over - get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } - # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻 - # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ - get_news: - default_rss_url: "https://www.chinanews.com.cn/rss/society.xml" - category_urls: - society: "https://www.chinanews.com.cn/rss/society.xml" - world: "https://www.chinanews.com.cn/rss/world.xml" - finance: "https://www.chinanews.com.cn/rss/finance.xml" - home_assistant: - devices: - - 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1 - - 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1 - base_url: http://homeassistant.local:8123 - api_key: 你的home assistant api访问令牌 - play_music: - music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 - music_ext: # 音乐文件类型,p3格式效率最高 - - ".mp3" - - ".wav" - - ".p3" - refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - # ##################################################################################### # ################################以下是角色模型配置###################################### @@ -138,9 +109,9 @@ selected_module: # 语音活动检测模块,默认使用SileroVAD模型 VAD: SileroVAD # 语音识别模块,默认使用FunASR本地模型 - ASR: FunASR + ASR: AliyunASR # 将根据配置名称对应的type调用实际的LLM适配器 - LLM: ChatGLMLLM + LLM: AliLLM # TTS将根据配置名称对应的type调用实际的TTS适配器 TTS: EdgeTTS # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short @@ -182,6 +153,36 @@ Intent: #- hass_set_state #- hass_play_music +# 插件的基础配置 +plugins: + # 获取天气插件的配置,这里填写你的api_key + # 这个密钥是项目共用的key,用多了可能会被限制 + # 想稳定一点就自行申请替换,每天有1000次免费调用 + # 申请地址:https://console.qweather.com/#/apps/create-key/over + get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } + # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻 + # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ + get_news: + default_rss_url: "https://www.chinanews.com.cn/rss/society.xml" + category_urls: + society: "https://www.chinanews.com.cn/rss/society.xml" + world: "https://www.chinanews.com.cn/rss/world.xml" + finance: "https://www.chinanews.com.cn/rss/finance.xml" + home_assistant: + devices: + - 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1 + - 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1 + base_url: http://homeassistant.local:8123 + api_key: 你的home assistant api访问令牌 + play_music: + music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 + music_ext: # 音乐文件类型,p3格式效率最高 + - ".mp3" + - ".wav" + - ".p3" + refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 + + Memory: mem0ai: type: mem0ai @@ -220,6 +221,19 @@ ASR: secret_id: 你的腾讯语音合成服务secret_id secret_key: 你的腾讯语音合成服务secret_key output_dir: tmp/ + AliyunASR: + # 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息 + # 平台地址:https://nls-portal.console.aliyun.com/ + # appkey地址:https://nls-portal.console.aliyun.com/applist + # token地址:https://nls-portal.console.aliyun.com/overview + # 定义ASR API类型 + type: aliyun + appkey: 你的阿里云智能语音交互服务项目Appkey + token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_id,access_key_secret + access_key_id: 的阿里云账号access_key_id + access_key_secret: 你的阿里云账号access_key_secret + output_dir: tmp/ + VAD: SileroVAD: threshold: 0.5 @@ -235,7 +249,7 @@ LLM: # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model_name: qwen-turbo - api_key: 你的deepseek web key + api_key: sk-a8b1fe19c885431c8a3769e18c02541e temperature: 0.7 # 温度值 max_tokens: 500 # 最大生成token数 top_p: 1 diff --git a/main/xiaozhi-server/core/providers/asr/aliyun.py b/main/xiaozhi-server/core/providers/asr/aliyun.py new file mode 100644 index 00000000..4205bec2 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/aliyun.py @@ -0,0 +1,265 @@ +import http.client +import json +import asyncio +from typing import Optional, Tuple, List +import opuslib_next +import wave +import io +import os +import uuid +import hmac +import hashlib +import base64 +import requests +from urllib import parse +import time +from datetime import datetime +from config.logger import setup_logging +from core.providers.asr.base import ASRProviderBase + +TAG = __name__ +logger = setup_logging() + +class AccessToken: + @staticmethod + def _encode_text(text): + encoded_text = parse.quote_plus(text) + return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') + + @staticmethod + def _encode_dict(dic): + keys = dic.keys() + dic_sorted = [(key, dic[key]) for key in sorted(keys)] + encoded_text = parse.urlencode(dic_sorted) + return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~') + + @staticmethod + def create_token(access_key_id, access_key_secret): + parameters = {'AccessKeyId': access_key_id, + 'Action': 'CreateToken', + 'Format': 'JSON', + 'RegionId': 'cn-shanghai', + 'SignatureMethod': 'HMAC-SHA1', + 'SignatureNonce': str(uuid.uuid1()), + 'SignatureVersion': '1.0', + 'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + 'Version': '2019-02-28'} + # 构造规范化的请求字符串 + query_string = AccessToken._encode_dict(parameters) + # print('规范化的请求字符串: %s' % query_string) + # 构造待签名字符串 + string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string) + # print('待签名的字符串: %s' % string_to_sign) + # 计算签名 + secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'), + bytes(string_to_sign, encoding='utf-8'), + hashlib.sha1).digest() + signature = base64.b64encode(secreted_string) + # print('签名: %s' % signature) + # 进行URL编码 + signature = AccessToken._encode_text(signature) + # print('URL编码后的签名: %s' % signature) + # 调用服务 + full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string) + # print('url: %s' % full_url) + # 提交HTTP GET请求 + response = requests.get(full_url) + if response.ok: + root_obj = response.json() + key = 'Token' + if key in root_obj: + token = root_obj[key]['Id'] + expire_time = root_obj[key]['ExpireTime'] + return token, expire_time + # print(response.text) + return None, None + + + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool): + """阿里云ASR初始化""" + # 新增空值判断逻辑 + self.access_key_id = config.get("access_key_id") + self.access_key_secret = config.get("access_key_secret") + + self.app_key = config.get("appkey") + self.host = "nls-gateway-cn-shanghai.aliyuncs.com" + self.base_url = f"https://{self.host}/stream/v1/asr" + self.sample_rate = 16000 + self.format = "wav" + self.output_dir = config.get("output_dir", "./audio_output") + self.delete_audio_file = delete_audio_file + + if self.access_key_id and self.access_key_secret: + # 使用密钥对生成临时token + self._refresh_token() + else: + # 直接使用预生成的长期token + self.token = config.get("token") + self.expire_time = None + + # 确保输出目录存在 + os.makedirs(self.output_dir, exist_ok=True) + + + def _refresh_token(self): + """刷新Token并记录过期时间""" + if self.access_key_id and self.access_key_secret: + self.token, expire_time_str = AccessToken.create_token( + self.access_key_id, + self.access_key_secret + ) + if not expire_time_str: + raise ValueError("无法获取有效的Token过期时间") + + try: + #统一转换为字符串处理 + expire_str = str(expire_time_str).strip() + + if expire_str.isdigit(): + expire_time = datetime.fromtimestamp(int(expire_str)) + else: + expire_time = datetime.strptime( + expire_str, + "%Y-%m-%dT%H:%M:%SZ" + ) + self.expire_time = expire_time.timestamp() - 60 + except Exception as e: + raise ValueError(f"无效的过期时间格式: {expire_str}") from e + + else: + self.expire_time = None + + if not self.token: + raise ValueError("无法获取有效的访问Token") + + def _is_token_expired(self): + """检查Token是否过期""" + if not self.expire_time: + return False # 长期Token不过期 + # 新增调试日志 + # current_time = time.time() + # remaining = self.expire_time - current_time + # print(f"Token过期检查: 当前时间 {datetime.fromtimestamp(current_time)} | " + # f"过期时间 {datetime.fromtimestamp(self.expire_time)} | " + # f"剩余 {remaining:.2f}秒") + return time.time() > self.expire_time + def generate_filename(self, extension=".wav"): + return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}") + + + def _construct_request_url(self) -> str: + """构造请求URL,包含参数""" + request = f"{self.base_url}?appkey={self.app_key}" + request += f"&format={self.format}" + request += f"&sample_rate={self.sample_rate}" + request += "&enable_punctuation_prediction=true" + request += "&enable_inverse_text_normalization=true" + request += "&enable_voice_detection=false" + return request + + def decode_opus(self, opus_data: List[bytes], session_id: str) -> List[bytes]: + """将Opus数据解码为PCM""" + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + return pcm_data + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """将Opus音频数据解码并保存为WAV文件""" + file_name = f"asr_{session_id}.wav" + file_path = os.path.join(self.output_dir, file_name) + + pcm_data = self.decode_opus(opus_data, session_id) + + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) # 单声道 + wf.setsampwidth(2) # 16-bit + wf.setframerate(self.sample_rate) + wf.writeframes(b''.join(pcm_data)) + + logger.bind(tag=TAG).debug(f"音频文件已保存至: {file_path}") + return file_path + + async def _send_request(self, pcm_data: bytes) -> Optional[str]: + """发送请求到阿里云ASR服务""" + try: + # 设置HTTP头 + headers = { + 'X-NLS-Token': self.token, + 'Content-type': 'application/octet-stream', + 'Content-Length': str(len(pcm_data)) + } + + # 创建连接并发送请求 + conn = http.client.HTTPSConnection(self.host) + request_url = self._construct_request_url() + + loop = asyncio.get_event_loop() + await loop.run_in_executor(None, lambda: conn.request( + method='POST', + url=request_url, + body=pcm_data, + headers=headers + )) + + # 获取响应 + response = await loop.run_in_executor(None, conn.getresponse) + body = await loop.run_in_executor(None, response.read) + conn.close() + + # 解析响应 + try: + body_json = json.loads(body) + status = body_json.get('status') + + if status == 20000000: + result = body_json.get('result', '') + logger.bind(tag=TAG).debug(f"ASR结果: {result}") + return result + else: + logger.bind(tag=TAG).error(f"ASR失败,状态码: {status}") + return None + + except ValueError: + logger.bind(tag=TAG).error("响应不是JSON格式") + return None + + except Exception as e: + logger.bind(tag=TAG).error(f"ASR请求失败: {e}", exc_info=True) + return None + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + """将语音数据转换为文本""" + if self._is_token_expired(): + logger.warning("Token已过期,正在自动刷新...") + self._refresh_token() + + try: + # 解码Opus为PCM + pcm_data_list = self.decode_opus(opus_data, session_id) + combined_pcm_data = b''.join(pcm_data_list) + + # 发送请求并获取文本 + text = await self._send_request(combined_pcm_data) + + file_path = self.save_audio_to_file(opus_data, session_id) + if self.delete_audio_file: + os.remove(file_path) + logger.bind(tag=TAG).debug(f"音频文件已删除: {file_path}") + + if text: + return text, None + return "", None + + except Exception as e: + logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) + return "", None \ No newline at end of file From c9eb64051007cfacbf5c2122f7c816679e745be2 Mon Sep 17 00:00:00 2001 From: LFNL-scholar Date: Tue, 8 Apr 2025 20:48:11 +0800 Subject: [PATCH 02/53] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=98=BF=E9=87=8C?= =?UTF-8?q?=E4=BA=91ASR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index ad85c712..b47ad6c9 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -109,9 +109,9 @@ selected_module: # 语音活动检测模块,默认使用SileroVAD模型 VAD: SileroVAD # 语音识别模块,默认使用FunASR本地模型 - ASR: AliyunASR + ASR: FunASR # 将根据配置名称对应的type调用实际的LLM适配器 - LLM: AliLLM + LLM: ChatGLMLLM # TTS将根据配置名称对应的type调用实际的TTS适配器 TTS: EdgeTTS # 记忆模块,默认不开启记忆;如果想使用超长记忆,推荐使用mem0ai;如果注重隐私,请使用本地的mem_local_short @@ -249,7 +249,7 @@ LLM: # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model_name: qwen-turbo - api_key: sk-a8b1fe19c885431c8a3769e18c02541e + api_key: 你的阿里云大模型 key temperature: 0.7 # 温度值 max_tokens: 500 # 最大生成token数 top_p: 1 From 244c76793c16919eb11814f3d07a1699a3ac0d77 Mon Sep 17 00:00:00 2001 From: wnk Date: Sun, 27 Apr 2025 11:55:10 +0800 Subject: [PATCH 03/53] =?UTF-8?q?=E5=BD=93=E5=89=8DfunASR=E4=BB=85?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9C=AC=E5=9C=B0=E6=A8=A1=E5=9E=8B=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9EFunASR=E6=9C=8D=E5=8A=A1API=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=88=E6=9C=8D=E5=8A=A1=E5=8F=AF=E6=A0=B9=E6=8D=AE=E5=AE=98?= =?UTF-8?q?=E6=96=B9=E8=AF=B4=E6=98=8E=E8=87=AA=E8=A1=8C=E5=8F=A6=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=EF=BC=89=EF=BC=8C=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8EAPI=E8=B0=83=E7=94=A8=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202504082211.sql | 1 + .../resources/db/changelog/202504112044.sql | 1 + main/xiaozhi-server/config.yaml | 6 + .../core/providers/asr/fun_server.py | 144 ++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 main/xiaozhi-server/core/providers/asr/fun_server.py diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 91ce33ff..5aa75bef 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -10,6 +10,7 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f ('SYSTEM_ASR_FunASR', 'ASR', 'fun_local', 'FunASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), ('SYSTEM_ASR_SherpaASR', 'ASR', 'sherpa_onnx_local', 'SherpaASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_ASR_DoubaoASR', 'ASR', 'doubao', '火山引擎语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 3, 1, NOW(), 1, NOW()), +('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW()), -- LLM模型供应器 ('SYSTEM_LLM_openai', 'LLM', 'openai', 'OpenAI接口', '[{"key":"base_url","label":"基础URL","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"temperature","label":"温度","type":"number"},{"key":"max_tokens","label":"最大令牌数","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"top_k","label":"top_k值","type":"number"},{"key":"frequency_penalty","label":"频率惩罚","type":"number"}]', 1, 1, NOW(), 1, NOW()), diff --git a/main/manager-api/src/main/resources/db/changelog/202504112044.sql b/main/manager-api/src/main/resources/db/changelog/202504112044.sql index 9fd2257d..3609d235 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504112044.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504112044.sql @@ -11,6 +11,7 @@ INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语 INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 0, 1, '{\"type\": \"sherpa_onnx_local\", \"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL); INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 0, 1, '{\"type\": \"doubao\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL); INSERT INTO `ai_model_config` VALUES ('ASR_TencentASR', 'ASR', 'TencentASR', '腾讯语音识别', 0, 1, '{\"type\": \"tencent\", \"appid\": \"\", \"secret_id\": \"\", \"secret_key\": \"你的secret_key\", \"output_dir\": \"tmp/\"}', NULL, NULL, 4, NULL, NULL, NULL, NULL); +INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL); -- LLM模型配置 INSERT INTO `ai_model_config` VALUES ('LLM_ChatGLMLLM', 'LLM', 'ChatGLMLLM', '智谱AI', 1, 1, '{\"type\": \"openai\", \"model_name\": \"glm-4-flash\", \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\", \"api_key\": \"你的api_key\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL); diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 696c82d0..3ec6b013 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -200,6 +200,12 @@ ASR: type: fun_local model_dir: models/SenseVoiceSmall output_dir: tmp/ + FunASRServer: + # 支持FunASR服务,部署方法:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md + # mode 预置 offline + type: fun_server + host: 127.0.0.1 + port: 10096 SherpaASR: type: sherpa_onnx_local model_dir: models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17 diff --git a/main/xiaozhi-server/core/providers/asr/fun_server.py b/main/xiaozhi-server/core/providers/asr/fun_server.py new file mode 100644 index 00000000..d0701d93 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/fun_server.py @@ -0,0 +1,144 @@ +from typing import Optional, Tuple, List +import opuslib_next +from core.providers.asr.base import ASRProviderBase +import ssl +import json +import websockets +from config.logger import setup_logging +import asyncio +TAG = __name__ +logger = setup_logging() + +class ASRProvider(ASRProviderBase): + def __init__(self, config: dict, delete_audio_file: bool): + ''' + Initialize the ASRProvider with server configuration. + :param config: Dictionary containing 'host', 'port', and 'is_ssl'. + :param delete_audio_file: Boolean to indicate whether to delete audio files after processing. + ''' + self.host = config.get('host', 'localhost') + self.port = config.get('port', 10095) + self.is_ssl = config.get('is_ssl', True) + self.delete_audio_file = delete_audio_file + self.uri = f"wss://{self.host}:{self.port}" if self.is_ssl else f"ws://{self.host}:{self.port}" + self.ssl_context = ssl.SSLContext() if self.is_ssl else None + if self.ssl_context: + self.ssl_context.check_hostname = False + self.ssl_context.verify_mode = ssl.CERT_NONE + + def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + """解码Opus数据并保存为WAV文件""" + pass + + @staticmethod + def decode_opus(opus_data: List[bytes]) -> bytes: + """将Opus音频数据解码为PCM数据""" + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + return b"".join(pcm_data) + + + + async def _receive_responses(self, ws) -> None: + ''' + Asynchronous generator to receive messages from the WebSocket. + Yields each message as it is received. + ''' + text = "" + while True: + try: + response = await asyncio.wait_for(ws.recv(), timeout=5) + response_data = json.loads(response) + logger.bind(tag=TAG).debug(f"Received response: {response_data}") + if response_data.get("is_final", True): + text += response_data.get("text", "") + break + else: + text += response_data.get("text", "") + except asyncio.TimeoutError: + logger.bind(tag=TAG).error("Timeout while waiting for response from WebSocket.") + break + except websockets.exceptions.ConnectionClosed as e: + logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}") + break + return text + async def _send_data(self, ws, pcm_data: bytes, session_id: str) -> tuple: + ''' + Internal method to handle WebSocket communication. + Reuses the persistent WebSocket connection if available. + :param pcm_data: PCM audio data to send. + :param session_id: Unique session identifier. + :return: Tuple containing recognized text and optional timestamp. + ''' + + # Send initial configuration message + config_message = json.dumps({ + "mode": "offline", + "chunk_size": [5, 10, 5], + "chunk_interval": 10, + "wav_name": session_id, + "is_speaking": True, + "itn": False + }) + await ws.send(config_message) + logger.bind(tag=TAG).debug(f"Sent configuration message: {config_message}") + + # Send PCM data + await ws.send(pcm_data) + logger.bind(tag=TAG).debug(f"Sent PCM data of length: {len(pcm_data)} bytes") + + # Indicate end of speech + end_message = json.dumps({"is_speaking": False}) + await ws.send(end_message) + logger.bind(tag=TAG).debug(f"Sent end message: {end_message}") + + + async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + ''' + Convert speech data to text using FunASR. + :param opus_data: List of Opus-encoded audio data chunks. + :param session_id: Unique session identifier. + :return: Tuple containing recognized text and optional timestamp. + ''' + + pcm_data = self.decode_opus(opus_data) + + async with websockets.connect(self.uri, subprotocols=["binary"], ping_interval=None, ssl=self.ssl_context) as ws: + try: + # Use asyncio to handle WebSocket communication + send_task = asyncio.create_task(self._send_data(ws, pcm_data, session_id)) + receive_task = asyncio.create_task(self._receive_responses(ws)) + + # Gather tasks with error handling + done, pending = await asyncio.wait( + [send_task, receive_task], + return_when=asyncio.FIRST_EXCEPTION + ) + + # Cancel any pending tasks + for task in pending: + task.cancel() + + # Check for exceptions in completed tasks + for task in done: + if task.exception(): + raise task.exception() + + # Get the result from the receive task + result = receive_task.result() + return result, None # Return the recognized text and timestamp (if any) + + except websockets.exceptions.ConnectionClosed as e: + logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}") + return "", None + except Exception as e: + logger.bind(tag=TAG).error(f"Error during speech-to-text conversion: {e}", exc_info=True) + return "", None \ No newline at end of file From 62857862828ceb3891d5bda7d31753b1409c1cff Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Mon, 28 Apr 2025 11:31:10 +0800 Subject: [PATCH 04/53] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=B1=86=E5=8C=85tts=E5=AF=B9=E8=AF=9D=E9=80=9F=E7=8E=87?= =?UTF-8?q?=E7=AD=89=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 3 +++ main/xiaozhi-server/core/providers/tts/doubao.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 4b93d87e..6632a3a9 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -385,6 +385,9 @@ TTS: appid: 你的火山引擎语音合成服务appid access_token: 你的火山引擎语音合成服务access_token cluster: volcano_tts + speed_ratio: 1.0 + volume_ratio: 1.0 + pitch_ratio: 1.0 CosyVoiceSiliconflow: type: siliconflow # 硅基流动TTS diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index 968399cc..f0aedc8e 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,6 +27,10 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") + self.speed_ratio = config.get("speed_ratio") if config.get("speed_ratio") else 1.0 + self.volume_ratio = config.get("volume_ratio") if config.get("volume_ratio") else 1.0 + self.pitch_ratio = config.get("pitch_ratio") if config.get("pitch_ratio") else 1.0 + self.api_url = config.get("api_url") self.authorization = config.get("authorization") self.header = {"Authorization": f"{self.authorization}{self.access_token}"} @@ -49,9 +53,9 @@ class TTSProvider(TTSProviderBase): "audio": { "voice_type": self.voice, "encoding": "wav", - "speed_ratio": 1.0, - "volume_ratio": 1.0, - "pitch_ratio": 1.0, + "speed_ratio": self.speed_ratio, + "volume_ratio": self.volume_ratio, + "pitch_ratio": self.pitch_ratio, }, "request": { "reqid": str(uuid.uuid4()), From 16e77b02c21efb6ed74f90e4559f5bd6f9e67f7b Mon Sep 17 00:00:00 2001 From: MakerZorky <1053714527zhq@gmail.com> Date: Tue, 29 Apr 2025 01:28:06 +0800 Subject: [PATCH 05/53] =?UTF-8?q?update:=E6=89=80=E6=9C=89ASR=E5=9D=87?= =?UTF-8?q?=E6=94=B9=E4=B8=BAPCM=E7=9B=B4=E6=8E=A5=E8=AF=86=E5=88=AB?= =?UTF-8?q?=EF=BC=8C=E9=80=9A=E8=BF=87delete=5Faudio=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E8=A6=81=E4=B8=8D=E8=A6=81=E5=82=A8=E5=AD=98?= =?UTF-8?q?wav?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaozhi-server/core/providers/asr/base.py | 4 +-- .../core/providers/asr/doubao.py | 21 ++++++------ .../core/providers/asr/fun_local.py | 32 +++++++++++++++---- .../core/providers/asr/sherpa_onnx_local.py | 24 ++++++++------ .../core/providers/asr/tencent.py | 18 ++++------- 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py index d8db22a8..6d9c2c90 100644 --- a/main/xiaozhi-server/core/providers/asr/base.py +++ b/main/xiaozhi-server/core/providers/asr/base.py @@ -9,8 +9,8 @@ logger = setup_logging() class ASRProviderBase(ABC): @abstractmethod - def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """解码Opus数据并保存为WAV文件""" + def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str: + """PCM数据保存为WAV文件""" pass @abstractmethod diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py index b19ceb97..6fba472c 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao.py +++ b/main/xiaozhi-server/core/providers/asr/doubao.py @@ -89,6 +89,7 @@ class ASRProvider(ASRProviderBase): self.cluster = config.get("cluster") self.access_token = config.get("access_token") self.output_dir = config.get("output_dir") + self.delete_audio_file = delete_audio_file self.host = "openspeech.bytedance.com" self.ws_url = f"wss://{self.host}/api/v2/asr" @@ -98,21 +99,11 @@ class ASRProvider(ASRProviderBase): # 确保输出目录存在 os.makedirs(self.output_dir, exist_ok=True) - def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """将Opus音频数据解码并保存为WAV文件""" + def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str: + """PCM数据保存为WAV文件""" file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" file_path = os.path.join(self.output_dir, file_name) - decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 - pcm_data = [] - - for opus_packet in opus_data: - try: - pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms - pcm_data.append(pcm_frame) - except opuslib_next.OpusError as e: - logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) - with wave.open(file_path, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) # 2 bytes = 16-bit @@ -274,6 +265,12 @@ class ASRProvider(ASRProviderBase): pcm_data = self.decode_opus(opus_data, session_id) combined_pcm_data = b"".join(pcm_data) + # 判断是否保存为WAV文件 + if self.delete_audio_file: + pass + else: + self.save_audio_to_file(pcm_data, session_id) + # 直接使用PCM数据 # 计算分段大小 (单声道, 16bit, 16kHz采样率) size_per_sec = 1 * 2 * 16000 # nchannels * sampwidth * framerate diff --git a/main/xiaozhi-server/core/providers/asr/fun_local.py b/main/xiaozhi-server/core/providers/asr/fun_local.py index 82d09ab5..a678780b 100644 --- a/main/xiaozhi-server/core/providers/asr/fun_local.py +++ b/main/xiaozhi-server/core/providers/asr/fun_local.py @@ -51,7 +51,7 @@ class ASRProvider(ASRProviderBase): ) def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """将Opus音频数据解码并保存为WAV文件""" + """PCM数据保存为WAV文件""" file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" file_path = os.path.join(self.output_dir, file_name) @@ -72,20 +72,40 @@ class ASRProvider(ASRProviderBase): wf.writeframes(b"".join(pcm_data)) return file_path + + @staticmethod + def decode_opus(opus_data: List[bytes], session_id: str) -> List[bytes]: + + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + return pcm_data async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: """语音转文本主处理逻辑""" file_path = None try: - # 保存音频文件 - start_time = time.time() - file_path = self.save_audio_to_file(opus_data, session_id) - logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") + # 合并所有opus数据包 + pcm_data = self.decode_opus(opus_data, session_id) + combined_pcm_data = b"".join(pcm_data) + + # 判断是否保存为WAV文件 + if self.delete_audio_file: + pass + else: + self.save_audio_to_file(pcm_data, session_id) # 语音识别 start_time = time.time() result = self.model.generate( - input=file_path, + input=combined_pcm_data, cache={}, language="auto", use_itn=True, diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py index 3dfa3162..b086022b 100644 --- a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py +++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py @@ -84,10 +84,21 @@ class ASRProvider(ASRProviderBase): ) def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """将Opus音频数据解码并保存为WAV文件""" + """PCM数据保存为WAV文件""" file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" file_path = os.path.join(self.output_dir, file_name) + with wave.open(file_path, "wb") as wf: + wf.setnchannels(1) + wf.setsampwidth(2) # 2 bytes = 16-bit + wf.setframerate(16000) + wf.writeframes(b"".join(pcm_data)) + + return file_path + + @staticmethod + def decode_opus(opus_data: List[bytes], session_id: str) -> List[bytes]: + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 pcm_data = [] @@ -98,13 +109,7 @@ class ASRProvider(ASRProviderBase): except opuslib_next.OpusError as e: logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) - with wave.open(file_path, "wb") as wf: - wf.setnchannels(1) - wf.setsampwidth(2) # 2 bytes = 16-bit - wf.setframerate(16000) - wf.writeframes(b"".join(pcm_data)) - - return file_path + return pcm_data def read_wave(self, wave_filename: str) -> Tuple[np.ndarray, int]: """ @@ -136,7 +141,8 @@ class ASRProvider(ASRProviderBase): try: # 保存音频文件 start_time = time.time() - file_path = self.save_audio_to_file(opus_data, session_id) + pcm_data = self.decode_opus(opus_data, session_id) + file_path = self.save_audio_to_file(pcm_data, session_id) logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") # 语音识别 diff --git a/main/xiaozhi-server/core/providers/asr/tencent.py b/main/xiaozhi-server/core/providers/asr/tencent.py index 8221f9df..0ac297a6 100644 --- a/main/xiaozhi-server/core/providers/asr/tencent.py +++ b/main/xiaozhi-server/core/providers/asr/tencent.py @@ -31,21 +31,11 @@ class ASRProvider(ASRProviderBase): os.makedirs(self.output_dir, exist_ok=True) def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: - """将Opus音频数据解码并保存为WAV文件""" + """PCM数据保存为WAV文件""" file_name = f"tencent_asr_{session_id}_{uuid.uuid4()}.wav" file_path = os.path.join(self.output_dir, file_name) - decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 - pcm_data = [] - - for opus_packet in opus_data: - try: - pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms - pcm_data.append(pcm_frame) - except opuslib_next.OpusError as e: - logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) - with wave.open(file_path, "wb") as wf: wf.setnchannels(1) wf.setsampwidth(2) # 2 bytes = 16-bit @@ -85,6 +75,12 @@ class ASRProvider(ASRProviderBase): # 将Opus音频数据解码为PCM pcm_data = self.decode_opus(opus_data) + + # 判断是否保存为WAV文件 + if self.delete_audio_file: + pass + else: + self.save_audio_to_file(pcm_data, session_id) # 将音频数据转换为Base64编码 base64_audio = base64.b64encode(pcm_data).decode('utf-8') From 6ac894716853392e55e2ae2cb335293933c09629 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 29 Apr 2025 10:14:43 +0800 Subject: [PATCH 06/53] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0issue=E6=A8=A1?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2d6c173f..deaff830 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,26 +9,24 @@ assignees: '' ## 🐛 问题描述 -## 🔍 复现步骤 - +## 🖥️ 环境信息 +- 部署方式: 全模块部署 还是 单Server部署 +- 版本号: 例如 0.3.x + +## 🔍 告诉我们,应该怎么复现这个问题 + 1. 打开 '...' 2. 点击 '...' 3. 滚动到 '...' 4. 看到错误 -## 🤔 预期行为 +## 🤔 你原本希望是怎么样的 -## 😯 截图 +## 😯 提供一些截图 - -## 🖥️ 环境信息 -- 操作系统: [例如 Windows 10] -- 浏览器: [例如 Chrome 89] -- 项目版本: [例如 1.0.0] -- Python版本: [例如 3.9.13] -- Jdk版本:[例如 java 21.0.5 2024-10-15 LTS] -- Nodejs版本:[例如 v20.14.0] +1. 比如日志截图,越多越好 +2. 比如界面反应 ## 📋 其他信息 From 361defc3487ed481d976ae555fb0e4cbb57eec7f Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Tue, 29 Apr 2025 10:36:20 +0800 Subject: [PATCH 07/53] =?UTF-8?q?update:=E5=AE=8C=E6=88=90Python=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=9A=84=E2=80=9C=E5=8A=A8=E6=80=81=E6=9B=B4=E6=96=B0?= =?UTF-8?q?web=E7=AB=AF=E7=9A=84=E9=85=8D=E7=BD=AE=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 24 +++++++++++++++++++ main/xiaozhi-server/core/handle/textHandle.py | 3 +++ 2 files changed, 27 insertions(+) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 3cf6b018..22b90313 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -214,6 +214,30 @@ class ConnectionHandler: elif isinstance(message, bytes): await handleAudioMessage(self, message) + async def handle_config_update(self, message): + """处理配置更新请求""" + config_model = message.get("model") + new_content = message.get("content") + + # 打印更新前的配置 + old_value = self.config.get(config_model) + self.logger.bind(tag=TAG).info(f"配置更新: {config_model} 从 {old_value} 更新为 {new_content}") + + # 更新配置 + self.config[config_model] = new_content + + # 如果是模型相关配置,重新初始化模型 + if config_model in ["LLM", "TTS", "ASR", "VAD", "Intent", "Memory"]: + self._initialize_components(self.config) + self.logger.bind(tag=TAG).info(f"已使用新配置重新初始化 {config_model} 模块") + + # 返回更新确认 + await self.websocket.send(json.dumps({ + "type": "config_update_response", + "status": "success", + "message": f"{config_model} 配置已更新" + })) + def _initialize_components(self, private_config): """初始化组件""" if private_config is not None: diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index 43e980e8..f65d436a 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -61,5 +61,8 @@ async def handleTextMessage(conn, message): asyncio.create_task(handleIotDescriptors(conn, msg_json["descriptors"])) if "states" in msg_json: asyncio.create_task(handleIotStatus(conn, msg_json["states"])) + elif msg_json["type"] == "server": + if "model" in msg_json: + await conn.handle_config_update(msg_json) except json.JSONDecodeError: await conn.websocket.send(message) From 5e35cfc44abc36b4791ba6c4d72fc4daab85bdbb Mon Sep 17 00:00:00 2001 From: myifeng Date: Tue, 29 Apr 2025 13:55:34 +0800 Subject: [PATCH 08/53] =?UTF-8?q?=E5=AE=9A=E4=B9=89config.yaml=E4=B8=BA?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE=EF=BC=9B=20data/.config.ya?= =?UTF-8?q?ml=20=E4=B8=BA=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=9B=20=E4=BD=BF=E7=94=A8=E8=80=85=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF=E5=AE=8C=E6=88=90=E5=90=AF=E5=8A=A8=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E5=85=B3=E5=BF=83=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9=EF=BC=8C=E5=87=8F=E5=B0=91=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=86=97=E4=BD=99=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config/config_loader.py | 43 +++++++++++++++++--- main/xiaozhi-server/config/settings.py | 45 ++++++++------------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index 849a2a8a..b892b437 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -1,6 +1,7 @@ import os import argparse import yaml +from collections.abc import Mapping from config.manage_api_client import init_service, get_server_config, get_agent_models @@ -25,12 +26,19 @@ def load_config(): if _config_cache is not None: return _config_cache - parser = argparse.ArgumentParser(description="Server configuration") - config_file = get_config_file() + default_config_path = get_project_dir() + "config.yaml" + custom_config_path = get_project_dir() + "data/.config.yaml" - parser.add_argument("--config_path", type=str, default=config_file) - args = parser.parse_args() - config = read_config(args.config_path) + # 加载默认配置 + default_config = read_config(default_config_path) + + # 加载用户自定义配置(如果存在) + if os.path.exists(custom_config_path): + custom_config = read_config(custom_config_path) + # 合并配置 + config = merge_configs(default_config, custom_config) + else: + config = default_config if config.get("manager-api", {}).get("url"): config = get_config_from_api(config) @@ -115,3 +123,28 @@ def ensure_directories(config): os.makedirs(dir_path, exist_ok=True) except PermissionError: print(f"警告:无法创建目录 {dir_path},请检查写入权限") + + +def merge_configs(default_config, custom_config): + """ + 递归合并配置,custom_config优先级更高 + + Args: + default_config: 默认配置 + custom_config: 用户自定义配置 + + Returns: + 合并后的配置 + """ + if not isinstance(default_config, Mapping) or not isinstance(custom_config, Mapping): + return custom_config + + merged = dict(default_config) + + for key, value in custom_config.items(): + if key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping): + merged[key] = merge_configs(merged[key], value) + else: + merged[key] = value + + return merged \ No newline at end of file diff --git a/main/xiaozhi-server/config/settings.py b/main/xiaozhi-server/config/settings.py index 535a81ed..c12c70d8 100644 --- a/main/xiaozhi-server/config/settings.py +++ b/main/xiaozhi-server/config/settings.py @@ -1,7 +1,11 @@ import os from collections.abc import Mapping +from config import logger from config.config_loader import read_config, get_project_dir, load_config +TAG = __name__ +logger = logger.setup_logging() + default_config_file = "config.yaml" @@ -34,31 +38,16 @@ def find_missing_keys(new_config, old_config, parent_key=""): def check_config_file(): - old_config_file = get_project_dir() + "data/." + default_config_file - if not os.path.exists(old_config_file): - return - old_config = load_config() - new_config = read_config(get_project_dir() + default_config_file) - # 查找缺失的配置项 - missing_keys = find_missing_keys(new_config, old_config) - read_config_from_api = old_config.get("read_config_from_api", False) - if read_config_from_api: - old_config_origin = read_config(old_config_file) - if old_config_origin.get("selected_module") is not None: - missing_keys_str = "\n".join(f"- {key}" for key in missing_keys) - error_msg = "您的配置文件好像既包含智控台的配置又包含本地配置:\n" - error_msg += "\n建议您:\n" - error_msg += "1、将根目录的config_from_api.yaml文件复制到data下,重命名为.config.yaml\n" - error_msg += "2、按教程配置好接口地址和密钥\n" - raise ValueError(error_msg) - return - - if missing_keys: - missing_keys_str = "\n".join(f"- {key}" for key in missing_keys) - error_msg = "您的配置文件太旧了,缺少了:\n" - error_msg += missing_keys_str - error_msg += "\n建议您:\n" - error_msg += "1、备份data/.config.yaml文件\n" - error_msg += "2、将根目录的config.yaml文件复制到data下,重命名为.config.yaml\n" - error_msg += "3、将密钥逐个复制到新的配置文件中\n" - raise ValueError(error_msg) + """ + 简化的配置检查,仅提示用户配置文件的使用情况 + """ + custom_config_file = get_project_dir() + "data/." + default_config_file + if not os.path.exists(custom_config_file): + logger.bind(tag=TAG).info("提示: 使用默认配置文件。如需自定义配置,请创建 data/.config.yaml 文件") + else: + logger.bind(tag=TAG).info(f"提示: 使用自定义配置文件 data/.config.yaml,配置将覆盖默认值") + + # 检查是否从API读取配置 + config = load_config() + if config.get("read_config_from_api", False): + logger.bind(tag=TAG).info("提示: 从API获取配置") From 9a52b43e096109fd0301fcca1ca27e99a22df195 Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Tue, 29 Apr 2025 14:31:01 +0800 Subject: [PATCH 09/53] =?UTF-8?q?=E4=BF=AE=E6=94=B9SQL=E5=8F=8A=E5=85=A5?= =?UTF-8?q?=E5=8F=82=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/db/changelog/202504082211.sql | 2 +- main/xiaozhi-server/core/providers/tts/doubao.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 91ce33ff..08e5dc5f 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -25,7 +25,7 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f -- TTS模型供应器 ('SYSTEM_TTS_edge', 'TTS', 'edge', 'Edge TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), -('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key":"api_url","label":"API地址","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"authorization","label":"授权","type":"string"},{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"}]', 2, 1, NOW(), 1, NOW()), +('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_siliconflow', 'TTS', 'siliconflow', '硅基流动TTS', '[{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 3, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_cozecn', 'TTS', 'cozecn', 'COZECN TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 4, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_fishspeech', 'TTS', 'fishspeech', 'FishSpeech TTS', '[{"key":"output_dir","label":"输出目录","type":"string"},{"key":"response_format","label":"响应格式","type":"string"},{"key":"reference_id","label":"参考ID","type":"string"},{"key":"reference_audio","label":"参考音频","type":"dict","dict_name":"reference_audio"},{"key":"reference_text","label":"参考文本","type":"dict","dict_name":"reference_text"},{"key":"normalize","label":"是否标准化","type":"boolean"},{"key":"max_new_tokens","label":"最大新令牌数","type":"number"},{"key":"chunk_length","label":"块长度","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"repetition_penalty","label":"重复惩罚","type":"number"},{"key":"temperature","label":"温度","type":"number"},{"key":"streaming","label":"是否流式","type":"boolean"},{"key":"use_memory_cache","label":"是否使用内存缓存","type":"string"},{"key":"seed","label":"种子","type":"number"},{"key":"channels","label":"通道数","type":"number"},{"key":"rate","label":"采样率","type":"number"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"api_url","label":"API地址","type":"string"}]', 5, 1, NOW(), 1, NOW()), diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index f0aedc8e..1b6414c4 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,9 +27,9 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") - self.speed_ratio = config.get("speed_ratio") if config.get("speed_ratio") else 1.0 - self.volume_ratio = config.get("volume_ratio") if config.get("volume_ratio") else 1.0 - self.pitch_ratio = config.get("pitch_ratio") if config.get("pitch_ratio") else 1.0 + self.speed_ratio = float(config.get("speed_ratio", 0.1)) + self.volume_ratio = float(config.get("volume_ratio", 0.1)) + self.pitch_ratio = float(config.get("pitch_ratio", 0.1)) self.api_url = config.get("api_url") self.authorization = config.get("authorization") From f1b4b337273ab16a00f0e6929c05e6600f62250a Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Tue, 29 Apr 2025 15:25:03 +0800 Subject: [PATCH 10/53] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0python=E9=83=A8?= =?UTF-8?q?=E5=88=86=E2=80=9C=E5=8A=A8=E6=80=81=E6=9B=B4=E6=96=B0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 67 +++++++++++++++---- main/xiaozhi-server/core/handle/textHandle.py | 2 +- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 8a5f1ffd..9a59d388 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -216,26 +216,67 @@ class ConnectionHandler: async def handle_config_update(self, message): """处理配置更新请求""" - config_model = message.get("model") - new_content = message.get("content") + content = message.get("content", {}) + secret = content.pop("secret", None) + new_config = content - # 打印更新前的配置 - old_value = self.config.get(config_model) - self.logger.bind(tag=TAG).info(f"配置更新: {config_model} 从 {old_value} 更新为 {new_content}") + # 密钥验证 + if secret != "43c716ae-37c4-49ba-84ab-2f0fbfcd7115": + self.logger.bind(tag=TAG).warning("配置更新请求的密钥不正确") + await self.websocket.send(json.dumps({ + "type": "config_update_response", + "status": "error", + "message": "密钥不正确" + })) + return - # 更新配置 - self.config[config_model] = new_content + # 遍历所有支持的配置模块 + updated_modules = [] + for config_model in ["tts", "llm", "vad", "asr", "memory", "intent"]: + if config_model not in new_config: + continue - # 如果是模型相关配置,重新初始化模型 - if config_model in ["LLM", "TTS", "ASR", "VAD", "Intent", "Memory"]: - self._initialize_components(self.config) - self.logger.bind(tag=TAG).info(f"已使用新配置重新初始化 {config_model} 模块") + new_content = new_config[config_model] + old_content = self.config.get(config_model, {}) - # 返回更新确认 + # 记录配置变更 + self.logger.bind(tag=TAG).info( + f"配置更新: {config_model} 旧值: {json.dumps(old_content, ensure_ascii=False)} " + f"新值: {json.dumps(new_content, ensure_ascii=False)}" + ) + + # 深度合并配置 + if isinstance(old_content, dict) and isinstance(new_content, dict): + merged = {**old_content, **new_content} + self.config[config_model] = merged + else: + self.config[config_model] = new_content + + # 标记需要重新初始化的模块 + if config_model in ["llm", "tts", "asr", "vad", "intent", "memory"]: + updated_modules.append(config_model) + + # 批量初始化模块 + if updated_modules: + try: + self._initialize_components(self.config) + self.logger.bind(tag=TAG).info( + f"已重新初始化模块: {', '.join(updated_modules)}" + ) + except Exception as e: + self.logger.bind(tag=TAG).error(f"模块初始化失败: {str(e)}") + await self.websocket.send(json.dumps({ + "type": "config_update_response", + "status": "error", + "message": f"模块初始化失败: {str(e)}" + })) + return + + # 返回成功响应 await self.websocket.send(json.dumps({ "type": "config_update_response", "status": "success", - "message": f"{config_model} 配置已更新" + "message": f"已更新配置: {', '.join(updated_modules)}" })) def _initialize_components(self, private_config): diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index f65d436a..f0683d59 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -62,7 +62,7 @@ async def handleTextMessage(conn, message): if "states" in msg_json: asyncio.create_task(handleIotStatus(conn, msg_json["states"])) elif msg_json["type"] == "server": - if "model" in msg_json: + if msg_json["action"] == "update_config": await conn.handle_config_update(msg_json) except json.JSONDecodeError: await conn.websocket.send(message) From b0262cfe72e6f4416629314408b5816155137923 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Tue, 29 Apr 2025 17:14:53 +0800 Subject: [PATCH 11/53] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E5=90=9E?= =?UTF-8?q?=E9=9F=B3=E9=97=AE=E9=A2=98=EF=BC=8C=E8=A7=A3=E5=86=B3=E6=92=AD?= =?UTF-8?q?=E6=94=BE=E9=95=BF=E9=9F=B3=E9=A2=91=E6=97=B6=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E9=80=80=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 10 +++++-- .../core/handle/sendAudioHandle.py | 28 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 3cf6b018..c9e984f2 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -202,12 +202,16 @@ class ConnectionHandler: finally: await self.close(ws) + async def reset_timeout(self): + """重置超时计时器""" + if self.timeout_task: + self.timeout_task.cancel() + self.timeout_task = asyncio.create_task(self._check_timeout()) + async def _route_message(self, message): """消息路由""" # 重置超时计时器 - if self.timeout_task: - self.timeout_task.cancel() - self.timeout_task = asyncio.create_task(self._check_timeout()) + await self.reset_timeout() if isinstance(message, str): await handleTextMessage(self, message) diff --git a/main/xiaozhi-server/core/handle/sendAudioHandle.py b/main/xiaozhi-server/core/handle/sendAudioHandle.py index ec29a9fd..1d4da583 100644 --- a/main/xiaozhi-server/core/handle/sendAudioHandle.py +++ b/main/xiaozhi-server/core/handle/sendAudioHandle.py @@ -51,8 +51,8 @@ async def sendAudioMessage(conn, audios, text, text_index=0): logger.bind(tag=TAG).info(f"发送第一段语音: {text}") await send_tts_message(conn, "sentence_start", text) - # 播放音频 - await sendAudio(conn, audios) + is_first_audio = (text_index == conn.tts_first_text_index) + await sendAudio(conn, audios, pre_buffer=is_first_audio) await send_tts_message(conn, "sentence_end", text) @@ -64,22 +64,32 @@ async def sendAudioMessage(conn, audios, text, text_index=0): # 播放音频 -async def sendAudio(conn, audios): +async def sendAudio(conn, audios, pre_buffer=True): # 流控参数优化 frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码 start_time = time.perf_counter() play_position = 0 + last_reset_time = time.perf_counter() # 记录最后的重置时间 - # 预缓冲:发送前 3 帧 - pre_buffer = min(3, len(audios)) - for i in range(pre_buffer): - await conn.websocket.send(audios[i]) + # 仅当第一句话时执行预缓冲 + if pre_buffer: + pre_buffer_frames = min(3, len(audios)) + for i in range(pre_buffer_frames): + await conn.websocket.send(audios[i]) + remaining_audios = audios[pre_buffer_frames:] + else: + remaining_audios = audios - # 正常播放剩余帧 - for opus_packet in audios[pre_buffer:]: + # 播放剩余音频帧 + for opus_packet in remaining_audios: if conn.client_abort: return + # 每分钟重置一次计时器 + if time.perf_counter() - last_reset_time > 60: + await conn.reset_timeout() + last_reset_time = time.perf_counter() + # 计算预期发送时间 expected_time = start_time + (play_position / 1000) current_time = time.perf_counter() From 30708a59f1815c04e005ee8beeba6ee53f5971ba Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Tue, 29 Apr 2025 17:23:48 +0800 Subject: [PATCH 12/53] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0server=E9=80=9A?= =?UTF-8?q?=E7=94=A8secret=E8=BF=87=E6=BB=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/controller/ConfigController.java | 21 +--- .../modules/security/config/ShiroConfig.java | 25 +++-- .../security/secret/ServerSecretFilter.java | 105 ++++++++++++++++++ .../security/secret/ServerSecretToken.java | 25 +++++ .../modules/sys/dto/ConfigSecretDTO.java | 13 --- .../config/manage_api_client.py | 4 +- 6 files changed, 149 insertions(+), 44 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java delete mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index bdbd20ed..13106869 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -1,6 +1,6 @@ package xiaozhi.modules.config.controller; -import org.apache.commons.lang3.StringUtils; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,13 +9,10 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; -import xiaozhi.common.constant.Constant; -import xiaozhi.common.exception.RenException; import xiaozhi.common.utils.Result; import xiaozhi.common.validator.ValidatorUtils; import xiaozhi.modules.config.dto.AgentModelsDTO; import xiaozhi.modules.config.service.ConfigService; -import xiaozhi.modules.sys.dto.ConfigSecretDTO; import xiaozhi.modules.sys.service.SysParamsService; /** @@ -33,29 +30,17 @@ public class ConfigController { @PostMapping("server-base") @Operation(summary = "获取配置") - public Result getConfig(@RequestBody ConfigSecretDTO dto) { - // 效验数据 - ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); + public Result getConfig() { Object config = configService.getConfig(true); return new Result().ok(config); } @PostMapping("agent-models") @Operation(summary = "获取智能体模型") - public Result getAgentModels(@RequestBody AgentModelsDTO dto) { + public Result getAgentModels(@Valid @RequestBody AgentModelsDTO dto) { // 效验数据 ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule()); return new Result().ok(models); } - - private void checkSecret(String secret) { - String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true); - // 验证密钥 - if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) { - throw new RenException("密钥错误"); - } - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 6e68ce06..62e192b7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -1,9 +1,6 @@ package xiaozhi.modules.security.config; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - +import jakarta.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; @@ -14,10 +11,14 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import jakarta.servlet.Filter; import xiaozhi.modules.security.oauth2.Oauth2Filter; import xiaozhi.modules.security.oauth2.Oauth2Realm; +import xiaozhi.modules.security.secret.ServerSecretFilter; +import xiaozhi.modules.sys.service.SysParamsService; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** * Shiro的配置文件 @@ -46,7 +47,7 @@ public class ShiroConfig { } @Bean("shiroFilter") - public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { + public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) { ShiroFilterConfiguration config = new ShiroFilterConfiguration(); config.setFilterOncePerRequest(true); @@ -54,9 +55,11 @@ public class ShiroConfig { shiroFilter.setSecurityManager(securityManager); shiroFilter.setShiroFilterConfiguration(config); - // oauth过滤 Map filters = new HashMap<>(); + // oauth过滤 filters.put("oauth2", new Oauth2Filter()); + // 服务密钥过滤 + filters.put("server", new ServerSecretFilter(sysParamsService)); shiroFilter.setFilters(filters); // 添加Shiro的内置过滤器 @@ -79,8 +82,8 @@ public class ShiroConfig { filterMap.put("/user/login", "anon"); filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/register", "anon"); - filterMap.put("/config/server-base", "anon"); - filterMap.put("/config/agent-models", "anon"); + // 将config路径使用server服务过滤器 + filterMap.put("/config/**", "server"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); @@ -98,4 +101,4 @@ public class ShiroConfig { advisor.setSecurityManager(securityManager); return advisor; } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java new file mode 100644 index 00000000..b33ce23d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -0,0 +1,105 @@ +package xiaozhi.modules.security.secret; + +import java.io.IOException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.springframework.web.bind.annotation.RequestMethod; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.utils.HttpContextUtils; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.sys.service.SysParamsService; + +/** + * Config API 过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class ServerSecretFilter extends AuthenticatingFilter { + private final SysParamsService sysParamsService; + + @Override + protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) { + // 获取请求token + String token = getRequestToken((HttpServletRequest) request); + + if (StringUtils.isBlank(token)) { + log.warn("createToken:token is empty"); + return null; + } + + return new ServerSecretToken(token); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + // 对OPTIONS请求放行 + if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { + return true; + } + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { + // 获取token并校验 + String token = getRequestToken((HttpServletRequest) servletRequest); + if (StringUtils.isBlank(token)) { + // token为空,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "Authorization token不能为空"); + return false; + } + + // 验证token是否匹配 + String serverSecret = getServerSecret(); + if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { + // token无效,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的Authorization token"); + return false; + } + + return true; + } + + /** + * 发送未授权响应 + */ + private void sendUnauthorizedResponse(HttpServletResponse response, String message) { + response.setContentType("application/json;charset=utf-8"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); + + try { + String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); + response.getWriter().print(json); + } catch (IOException e) { + log.error("响应输出失败", e); + } + } + + /** + * 获取请求的token + */ + private String getRequestToken(HttpServletRequest httpRequest) { + String token = null; + // 从header中获取token + String authorization = httpRequest.getHeader("Authorization"); + if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) { + token = authorization.replace("Bearer ", ""); + } + return token; + } + + private String getServerSecret() { + return sysParamsService.getValue(Constant.SERVER_SECRET, true); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java new file mode 100644 index 00000000..9072eaf8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.security.secret; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Config API Token + */ +public class ServerSecretToken implements AuthenticationToken { + private static final long serialVersionUID = 1L; + private final String token; + + public ServerSecretToken(String token) { + this.token = token; + } + + @Override + public Object getPrincipal() { + return token; + } + + @Override + public Object getCredentials() { + return token; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java deleted file mode 100644 index 8e4978f9..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package xiaozhi.modules.sys.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -@Schema(description = "配置密钥DTO") -public class ConfigSecretDTO { - @Schema(description = "密钥") - @NotBlank(message = "密钥不能为空") - private String secret; -} \ No newline at end of file diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 1f583857..770cbf56 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -53,6 +53,7 @@ class ManageApiClient: headers={ "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})", "Accept": "application/json", + "Authorization": "Bearer " + cls._secret }, timeout=cls.config.get("timeout", 30), # 默认超时时间30秒 ) @@ -126,7 +127,7 @@ class ManageApiClient: def get_server_config() -> Optional[Dict]: """获取服务器基础配置""" return ManageApiClient._instance._execute_request( - "POST", "/config/server-base", json={"secret": ManageApiClient._secret} + "POST", "/config/server-base" ) @@ -138,7 +139,6 @@ def get_agent_models( "POST", "/config/agent-models", json={ - "secret": ManageApiClient._secret, "macAddress": mac_address, "clientId": client_id, "selectedModule": selected_module, From ef662e0ccfbde3cb06f7e078e6904a91bd9e87b1 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 29 Apr 2025 18:04:31 +0800 Subject: [PATCH 13/53] =?UTF-8?q?add:=E5=A2=9E=E5=8A=A0=E6=99=BA=E6=8E=A7?= =?UTF-8?q?=E5=8F=B0=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E5=90=84=E4=B8=AA?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=9A=84=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202504082211.sql | 1 - .../resources/db/changelog/202504112044.sql | 1 - .../resources/db/changelog/202504291043.sql | 504 ++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 9 +- main/xiaozhi-server/config.yaml | 22 +- .../core/providers/asr/sherpa_onnx_local.py | 27 +- 6 files changed, 542 insertions(+), 22 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202504291043.sql diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 5aa75bef..91ce33ff 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -10,7 +10,6 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f ('SYSTEM_ASR_FunASR', 'ASR', 'fun_local', 'FunASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), ('SYSTEM_ASR_SherpaASR', 'ASR', 'sherpa_onnx_local', 'SherpaASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_ASR_DoubaoASR', 'ASR', 'doubao', '火山引擎语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 3, 1, NOW(), 1, NOW()), -('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW()), -- LLM模型供应器 ('SYSTEM_LLM_openai', 'LLM', 'openai', 'OpenAI接口', '[{"key":"base_url","label":"基础URL","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"temperature","label":"温度","type":"number"},{"key":"max_tokens","label":"最大令牌数","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"top_k","label":"top_k值","type":"number"},{"key":"frequency_penalty","label":"频率惩罚","type":"number"}]', 1, 1, NOW(), 1, NOW()), diff --git a/main/manager-api/src/main/resources/db/changelog/202504112044.sql b/main/manager-api/src/main/resources/db/changelog/202504112044.sql index 3609d235..9fd2257d 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504112044.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504112044.sql @@ -11,7 +11,6 @@ INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语 INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 0, 1, '{\"type\": \"sherpa_onnx_local\", \"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL); INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 0, 1, '{\"type\": \"doubao\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL); INSERT INTO `ai_model_config` VALUES ('ASR_TencentASR', 'ASR', 'TencentASR', '腾讯语音识别', 0, 1, '{\"type\": \"tencent\", \"appid\": \"\", \"secret_id\": \"\", \"secret_key\": \"你的secret_key\", \"output_dir\": \"tmp/\"}', NULL, NULL, 4, NULL, NULL, NULL, NULL); -INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL); -- LLM模型配置 INSERT INTO `ai_model_config` VALUES ('LLM_ChatGLMLLM', 'LLM', 'ChatGLMLLM', '智谱AI', 1, 1, '{\"type\": \"openai\", \"model_name\": \"glm-4-flash\", \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\", \"api_key\": \"你的api_key\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL); diff --git a/main/manager-api/src/main/resources/db/changelog/202504291043.sql b/main/manager-api/src/main/resources/db/changelog/202504291043.sql new file mode 100644 index 00000000..555b8cf4 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504291043.sql @@ -0,0 +1,504 @@ +-- 增加FunASR服务语音识别模型供应器和配置 +DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_ASR_FunASRServer'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_FunASRServer', 'ASR', 'fun_server', 'FunASR服务语音识别', '[{"key":"host","label":"服务地址","type":"string"},{"key":"port","label":"端口号","type":"number"}]', 4, 1, NOW(), 1, NOW()); + +DELETE FROM `ai_model_config` WHERE `id` = 'ASR_FunASRServer'; +INSERT INTO `ai_model_config` VALUES ('ASR_FunASRServer', 'ASR', 'FunASRServer', 'FunASR服务语音识别', 0, 1, '{\"type\": \"fun_server\", \"host\": \"127.0.0.1\", \"port\": 10096}', NULL, NULL, 5, NULL, NULL, NULL, NULL); + +-- 修改ai_model_config表的remark字段类型为TEXT +ALTER TABLE `ai_model_config` MODIFY COLUMN `remark` TEXT COMMENT '备注'; + +-- 更新ASR模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md', +`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话 +第一句:mkdir -p ./funasr-runtime-resources/models +第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12 +上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime +不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 & +上一句话执行后会进入到容器,继续第五句:tail -f log.txt +第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了 +以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer'; + +-- 更新FunASR本地模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/modelscope/FunASR', +`remark` = 'FunASR本地模型配置说明: +1. 需要下载模型文件到xiaozhi-server/models/SenseVoiceSmall目录 +2. 支持中日韩粤语音识别 +3. 本地推理,无需网络连接 +4. 待识别文件保存在tmp/目录' WHERE `id` = 'ASR_FunASR'; + +-- 更新SherpaASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/k2-fsa/sherpa-onnx', +`remark` = 'SherpaASR配置说明: +1. 运行时自动下载模型文件到models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17目录 +2. 支持中文、英文、日语、韩语、粤语等多种语言 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录' WHERE `id` = 'ASR_SherpaASR'; + +-- 更新豆包ASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/speech/app', +`remark` = '豆包ASR配置说明: +1. 需要在火山引擎控制台创建应用并获取appid和access_token +2. 支持中文语音识别 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.volcengine.com/speech/app +2. 创建新应用 +3. 获取appid和access_token +4. 填入配置文件中' WHERE `id` = 'ASR_DoubaoASR'; + +-- 更新腾讯ASR配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.cloud.tencent.com/cam/capi', +`remark` = '腾讯ASR配置说明: +1. 需要在腾讯云控制台创建应用并获取appid、secret_id和secret_key +2. 支持中文语音识别 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥 +2. 访问 https://console.cloud.tencent.com/asr/resourcebundle 领取免费资源 +3. 获取appid、secret_id和secret_key +4. 填入配置文件中' WHERE `id` = 'ASR_TencentASR'; + +-- 更新TTS模型配置说明 +-- EdgeTTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/rany2/edge-tts', +`remark` = 'EdgeTTS配置说明: +1. 使用微软Edge TTS服务 +2. 支持多种语言和音色 +3. 免费使用,无需注册 +4. 需要网络连接 +5. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_EdgeTTS'; + +-- 豆包TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/speech/service/8', +`remark` = '豆包TTS配置说明: +1. 访问 https://console.volcengine.com/speech/service/8 +2. 需要在火山引擎控制台创建应用并获取appid和access_token +3. 山引擎语音一定要购买花钱,起步价30元,就有100并发了。如果用免费的只有2个并发,会经常报tts错误 +4. 购买服务后,购买免费的音色后,可能要等半小时左右,才能使用。 +5. 填入配置文件中' WHERE `id` = 'TTS_DoubaoTTS'; + +-- 硅基流动TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.siliconflow.cn/account/ak', +`remark` = '硅基流动TTS配置说明: +1. 访问 https://cloud.siliconflow.cn/account/ak +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'TTS_CosyVoiceSiliconflow'; + +-- Coze中文TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.coze.cn/open/oauth/pats', +`remark` = 'Coze中文TTS配置说明: +1. 访问 https://www.coze.cn/open/oauth/pats +2. 获取个人令牌 +3. 填入配置文件中' WHERE `id` = 'TTS_CozeCnTTS'; + +-- FishSpeech配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/fishaudio/fish-speech', +`remark` = 'FishSpeech配置说明: +1. 需要本地部署FishSpeech服务 +2. 支持自定义音色 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录 +5. 运行服务示例命令:python -m tools.api_server --listen 0.0.0.0:8080 --llama-checkpoint-path "checkpoints/fish-speech-1.5" --decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth" --decoder-config-name firefly_gan_vq --compile' WHERE `id` = 'TTS_FishSpeech'; + +-- GPT-SoVITS V2配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS', +`remark` = 'GPT-SoVITS V2配置说明: +1. 需要本地部署GPT-SoVITS服务 +2. 支持自定义音色克隆 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录 +部署步骤: +1. 运行服务示例命令:python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/demo.yaml' WHERE `id` = 'TTS_GPT_SOVITS_V2'; + +-- GPT-SoVITS V3配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/RVC-Boss/GPT-SoVITS', +`remark` = 'GPT-SoVITS V3配置说明: +1. 需要本地部署GPT-SoVITS V3服务 +2. 支持自定义音色克隆 +3. 本地推理,无需网络连接 +4. 输出文件保存在tmp/目录' WHERE `id` = 'TTS_GPT_SOVITS_V3'; + +-- MiniMax TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.minimaxi.com/', +`remark` = 'MiniMax TTS配置说明: +1. 需要在MiniMax平台创建账户并充值 +2. 支持多种音色,当前配置使用female-shaonv +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://platform.minimaxi.com/ 注册账号 +2. 访问 https://platform.minimaxi.com/user-center/payment/balance 充值 +3. 访问 https://platform.minimaxi.com/user-center/basic-information 获取group_id +4. 访问 https://platform.minimaxi.com/user-center/basic-information/interface-key 获取api_key +5. 填入配置文件中' WHERE `id` = 'TTS_MinimaxTTS'; + +-- 阿里云TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://nls-portal.console.aliyun.com/', +`remark` = '阿里云TTS配置说明: +1. 需要在阿里云平台开通智能语音交互服务 +2. 支持多种音色,当前配置使用xiaoyun +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 +2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey +3. 访问 https://nls-portal.console.aliyun.com/overview 获取token +4. 填入配置文件中 +注意:token是临时的24小时有效,长期使用需要配置access_key_id和access_key_secret' WHERE `id` = 'TTS_AliyunTTS'; + +-- 腾讯TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.cloud.tencent.com/cam/capi', +`remark` = '腾讯TTS配置说明: +1. 需要在腾讯云平台开通智能语音交互服务 +2. 支持多种音色,当前配置使用101001 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥 +2. 访问 https://console.cloud.tencent.com/tts/resourcebundle 领取免费资源 +3. 创建新应用 +4. 获取appid、secret_id和secret_key +5. 填入配置文件中' WHERE `id` = 'TTS_TencentTTS'; + +-- 302AI TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://dash.302.ai/', +`remark` = '302AI TTS配置说明: +1. 需要在302平台创建账户并获取API密钥 +2. 支持多种音色,当前配置使用湾湾小何音色 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://dash.302.ai/ 注册账号 +2. 访问 https://dash.302.ai/apis/list 获取API密钥 +3. 填入配置文件中 +价格:$35/百万字符' WHERE `id` = 'TTS_TTS302AI'; + +-- 机智云TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://agentrouter.gizwitsapi.com/panel/token', +`remark` = '机智云TTS配置说明: +1. 需要在机智云平台获取API密钥 +2. 支持多种音色,当前配置使用湾湾小何音色 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://agentrouter.gizwitsapi.com/panel/token 获取API密钥 +2. 填入配置文件中 +注意:前一万名注册的用户,将送5元体验金额' WHERE `id` = 'TTS_GizwitsTTS'; + +-- ACGN TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://acgn.ttson.cn/', +`remark` = 'ACGN TTS配置说明: +1. 需要在ttson平台购买token +2. 支持多种角色音色,当前配置使用角色ID:1695 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://acgn.ttson.cn/ 查看角色列表 +2. 访问 www.ttson.cn 购买token +3. 填入配置文件中 +开发相关疑问请提交至网站上的qq' WHERE `id` = 'TTS_ACGNTTS'; + +-- OpenAI TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.openai.com/api-keys', +`remark` = 'OpenAI TTS配置说明: +1. 需要在OpenAI平台获取API密钥 +2. 支持多种音色,当前配置使用onyx +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://platform.openai.com/api-keys 获取API密钥 +2. 填入配置文件中 +注意:国内需要使用代理访问' WHERE `id` = 'TTS_OpenAITTS'; + +-- 自定义TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '自定义TTS配置说明: +1. 支持自定义TTS接口服务 +2. 使用GET方式请求 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +配置说明: +1. 在params中配置请求参数 +2. 在headers中配置请求头 +3. 设置返回音频格式' WHERE `id` = 'TTS_CustomTTS'; + +-- 火山引擎边缘大模型网关TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/vei/aigateway/', +`remark` = '火山引擎边缘大模型网关TTS配置说明: +1. 访问 https://console.volcengine.com/vei/aigateway/ +2. 创建网关访问密钥,搜索并勾选 Doubao-语音合成 +3. 如果需要使用LLM,一并勾选 Doubao-pro-32k-functioncall +4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥 +5. 填入配置文件中 +音色列表参考:https://www.volcengine.com/docs/6561/1257544' WHERE `id` = 'TTS_VolcesAiGatewayTTS'; + +-- 更新LLM模型配置说明 +-- ChatGLM配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys', +`remark` = 'ChatGLM配置说明: +1. 访问 https://bigmodel.cn/usercenter/proj-mgmt/apikeys +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_ChatGLMLLM'; + +-- Ollama配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://ollama.com/', +`remark` = 'Ollama配置说明: +1. 安装Ollama服务 +2. 运行命令:ollama pull qwen2.5 +3. 确保服务运行在http://localhost:11434' WHERE `id` = 'LLM_OllamaLLM'; + +-- 通义千问配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key', +`remark` = '通义千问配置说明: +1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key +2. 获取API密钥 +3. 填入配置文件中,当前配置使用qwen-turbo模型 +4. 支持自定义参数:temperature=0.7, max_tokens=500, top_p=1, top_k=50' WHERE `id` = 'LLM_AliLLM'; + +-- 通义百炼配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key', +`remark` = '通义百炼配置说明: +1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key +2. 获取app_id和api_key +3. 填入配置文件中' WHERE `id` = 'LLM_AliAppLLM'; + +-- 豆包大模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement', +`remark` = '豆包大模型配置说明: +1. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement +2. 开通Doubao-1.5-pro服务 +3. 访问 https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey 获取API密钥 +4. 填入配置文件中 +5. 当前建议使用doubao-1-5-pro-32k-250115 +注意:有免费额度500000token' WHERE `id` = 'LLM_DoubaoLLM'; + +-- DeepSeek配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://platform.deepseek.com/', +`remark` = 'DeepSeek配置说明: +1. 访问 https://platform.deepseek.com/ +2. 注册并获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_DeepSeekLLM'; + +-- Dify配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.dify.ai/', +`remark` = 'Dify配置说明: +1. 访问 https://cloud.dify.ai/ +2. 注册并获取API密钥 +3. 填入配置文件中 +4. 支持多种对话模式:workflows/run, chat-messages, completion-messages +5. 平台设置的角色定义会失效,需要在Dify控制台设置 +注意:建议使用本地部署的Dify接口,国内部分区域访问公有云接口可能受限' WHERE `id` = 'LLM_DifyLLM'; + +-- Gemini配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://aistudio.google.com/apikey', +`remark` = 'Gemini配置说明: +1. 使用谷歌Gemini API服务 +2. 当前配置使用gemini-2.0-flash模型 +3. 需要网络连接 +4. 支持配置代理 +申请步骤: +1. 访问 https://aistudio.google.com/apikey +2. 创建API密钥 +3. 填入配置文件中 +注意:若在中国境内使用,请遵守《生成式人工智能服务管理暂行办法》' WHERE `id` = 'LLM_GeminiLLM'; + +-- Coze配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.coze.cn/open/oauth/pats', +`remark` = 'Coze配置说明: +1. 使用Coze平台服务 +2. 需要bot_id、user_id和个人令牌 +3. 需要网络连接 +申请步骤: +1. 访问 https://www.coze.cn/open/oauth/pats +2. 获取个人令牌 +3. 手动计算bot_id和user_id +4. 填入配置文件中' WHERE `id` = 'LLM_CozeLLM'; + +-- LM Studio配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://lmstudio.ai/', +`remark` = 'LM Studio配置说明: +1. 使用本地部署的LM Studio服务 +2. 当前配置使用deepseek-r1-distill-llama-8b@q4_k_m模型 +3. 本地推理,无需网络连接 +4. 需要预先下载模型 +部署步骤: +1. 安装LM Studio +2. 从社区下载模型 +3. 确保服务运行在http://localhost:1234/v1' WHERE `id` = 'LLM_LMStudioLLM'; + +-- FastGPT配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://cloud.tryfastgpt.ai/account/apikey', +`remark` = 'FastGPT配置说明: +1. 使用FastGPT平台服务 +2. 需要网络连接 +3. 配置文件中的prompt无效,需要在FastGPT控制台设置 +4. 支持自定义变量 +申请步骤: +1. 访问 https://cloud.tryfastgpt.ai/account/apikey +2. 获取API密钥 +3. 填入配置文件中' WHERE `id` = 'LLM_FastgptLLM'; + +-- Xinference配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/xorbitsai/inference', +`remark` = 'Xinference配置说明: +1. 使用本地部署的Xinference服务 +2. 当前配置使用qwen2.5:72b-AWQ模型 +3. 本地推理,无需网络连接 +4. 需要预先启动对应模型 +部署步骤: +1. 安装Xinference +2. 启动服务并加载模型 +3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceLLM'; + +-- Xinference小模型配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/xorbitsai/inference', +`remark` = 'Xinference小模型配置说明: +1. 使用本地部署的Xinference服务 +2. 当前配置使用qwen2.5:3b-AWQ模型 +3. 本地推理,无需网络连接 +4. 用于意图识别 +部署步骤: +1. 安装Xinference +2. 启动服务并加载模型 +3. 确保服务运行在http://localhost:9997' WHERE `id` = 'LLM_XinferenceSmallLLM'; + +-- 火山引擎边缘大模型网关LLM配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.volcengine.com/vei/aigateway/', +`remark` = '火山引擎边缘大模型网关LLM配置说明: +1. 使用火山引擎边缘大模型网关服务 +2. 需要网关访问密钥 +3. 需要网络连接 +4. 支持function_call功能 +申请步骤: +1. 访问 https://console.volcengine.com/vei/aigateway/ +2. 创建网关访问密钥,搜索并勾选 Doubao-pro-32k-functioncall +3. 如果需要使用语音合成,一并勾选 Doubao-语音合成 +4. 访问 https://console.volcengine.com/vei/aigateway/tokens-list 获取密钥 +5. 填入配置文件中' WHERE `id` = 'LLM_VolcesAiGatewayLLM'; + +-- 更新Memory模型配置说明 +-- 无记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '无记忆配置说明: +1. 不保存对话历史 +2. 每次对话都是独立的 +3. 无需额外配置 +4. 适合对隐私要求高的场景' WHERE `id` = 'Memory_nomem'; + +-- 本地短期记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '本地短期记忆配置说明: +1. 使用本地存储保存对话历史 +2. 通过selected_module的llm总结对话内容 +3. 数据保存在本地,不会上传到服务器 +4. 适合注重隐私的场景 +5. 无需额外配置' WHERE `id` = 'Memory_mem_local_short'; + +-- Mem0AI记忆配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://app.mem0.ai/dashboard/api-keys', +`remark` = 'Mem0AI记忆配置说明: +1. 使用Mem0AI服务保存对话历史 +2. 需要API密钥 +3. 需要网络连接 +4. 每月有1000次免费调用 +申请步骤: +1. 访问 https://app.mem0.ai/dashboard/api-keys +2. 获取API密钥 +3. 填入配置文件中' WHERE `id` = 'Memory_mem0ai'; + +-- 更新Intent模型配置说明 +-- 无意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '无意图识别配置说明: +1. 不进行意图识别 +2. 所有对话直接传递给LLM处理 +3. 无需额外配置 +4. 适合简单对话场景' WHERE `id` = 'Intent_nointent'; + +-- LLM意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = 'LLM意图识别配置说明: +1. 使用独立的LLM进行意图识别 +2. 默认使用selected_module.LLM的模型 +3. 可以配置使用独立的LLM(如免费的ChatGLMLLM) +4. 通用性强,但会增加处理时间 +5. 不支持控制音量大小等iot操作 +配置说明: +1. 在llm字段中指定使用的LLM模型 +2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm'; + +-- 函数调用意图识别配置说明 +UPDATE `ai_model_config` SET +`doc_link` = NULL, +`remark` = '函数调用意图识别配置说明: +1. 使用LLM的function_call功能进行意图识别 +2. 需要所选择的LLM支持function_call +3. 按需调用工具,处理速度快 +4. 支持所有iot指令 +5. 默认已加载以下功能: + - handle_exit_intent(退出识别) + - play_music(音乐播放) + - change_role(角色切换) + - get_weather(天气查询) + - get_news(新闻查询) +配置说明: +1. 在functions字段中配置需要加载的功能模块 +2. 系统默认已加载基础功能,无需重复配置 +3. 可以添加自定义功能模块' WHERE `id` = 'Intent_function_call'; + +-- 更新VAD模型配置说明 +-- SileroVAD配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://github.com/snakers4/silero-vad', +`remark` = 'SileroVAD配置说明: +1. 使用SileroVAD模型进行语音活动检测 +2. 本地推理,无需网络连接 +3. 需要下载模型文件到models/snakers4_silero-vad目录 +4. 可配置参数: + - threshold: 0.5(语音检测阈值) + - min_silence_duration_ms: 700(最小静音持续时间,单位毫秒) +5. 如果说话停顿比较长,可以适当增加min_silence_duration_ms的值' WHERE `id` = 'VAD_SileroVAD'; 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 1bbddb75..131aaf01 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 @@ -78,4 +78,11 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504251422.sql \ No newline at end of file + path: classpath:db/changelog/202504251422.sql + - changeSet: + id: 202504291043 + author: jiangkunyin + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202504291043.sql \ No newline at end of file diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index d18b23e9..21266333 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -210,8 +210,14 @@ ASR: model_dir: models/SenseVoiceSmall output_dir: tmp/ FunASRServer: - # 支持FunASR服务,部署方法:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md - # mode 预置 offline + # 独立部署FunASR,使用FunASR的API服务,只需要五句话 + # 第一句:mkdir -p ./funasr-runtime-resources/models + # 第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12 + # 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime + # 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 & + # 上一句话执行后会进入到容器,继续第五句:tail -f log.txt + # 第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了 + # 以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md type: fun_server host: 127.0.0.1 port: 10096 @@ -272,12 +278,12 @@ LLM: DoubaoLLM: # 定义LLM API类型 type: openai - # 先开通服务,打开以下网址,开通的服务搜索Doubao-pro-32k,开通它 + # 先开通服务,打开以下网址,开通的服务搜索Doubao-1.5-pro,开通它 # 开通改地址:https://console.volcengine.com/ark/region:ark+cn-beijing/openManagement?LLM=%7B%7D&OpenTokenDrawer=false # 免费额度500000token # 开通后,进入这里获取密钥:https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D base_url: https://ark.cn-beijing.volces.com/api/v3 - model_name: doubao-pro-32k-functioncall-241028 + model_name: doubao-1-5-pro-32k-250115 api_key: 你的doubao web key DeepSeekLLM: # 定义LLM API类型 @@ -453,12 +459,12 @@ TTS: GPT_SOVITS_V2: # 定义TTS API类型 #启动tts方法: - #python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml + #python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/demo.yaml type: gpt_sovits_v2 url: "http://127.0.0.1:9880/tts" output_dir: tmp/ text_lang: "auto" - ref_audio_path: "caixukun.wav" + ref_audio_path: "demo.wav" prompt_text: "" prompt_lang: "zh" top_k: 5 @@ -591,8 +597,8 @@ TTS: ACGNTTS: #在线网址:https://acgn.ttson.cn/ #token购买:www.ttson.cn - #开发相关疑问请提交至3497689533@qq.com - #角色id获取地址:ctrl+f快速检索角色——网站管理者不允许发布,可询问网站管理者:1069379506 + #开发相关疑问请提交至网站上的qq + #角色id获取地址:ctrl+f快速检索角色——网站管理者不允许发布,可询问网站管理者 #各参数意义见开发文档:https://www.yuque.com/alexuh/skmti9/wm6taqislegb02gd?singleDoc# type: ttson token: your_token diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py index b086022b..cfcbe83b 100644 --- a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py +++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py @@ -43,11 +43,11 @@ class ASRProvider(ASRProviderBase): # 确保输出目录存在 os.makedirs(self.output_dir, exist_ok=True) - + # 初始化模型文件路径 model_files = { "model.int8.onnx": os.path.join(self.model_dir, "model.int8.onnx"), - "tokens.txt": os.path.join(self.model_dir, "tokens.txt") + "tokens.txt": os.path.join(self.model_dir, "tokens.txt"), } # 下载并检查模型文件 @@ -58,15 +58,15 @@ class ASRProvider(ASRProviderBase): model_file_download( model_id="pengzhendong/sherpa-onnx-sense-voice-zh-en-ja-ko-yue", file_path=file_name, - local_dir=self.model_dir + local_dir=self.model_dir, ) - + if not os.path.isfile(file_path): raise FileNotFoundError(f"模型文件下载失败: {file_path}") - + self.model_path = model_files["model.int8.onnx"] self.tokens_path = model_files["tokens.txt"] - + except Exception as e: logger.bind(tag=TAG).error(f"模型文件处理失败: {str(e)}") raise @@ -83,7 +83,7 @@ class ASRProvider(ASRProviderBase): use_itn=True, ) - def save_audio_to_file(self, opus_data: List[bytes], session_id: str) -> str: + def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str: """PCM数据保存为WAV文件""" file_name = f"asr_{session_id}_{uuid.uuid4()}.wav" file_path = os.path.join(self.output_dir, file_name) @@ -135,7 +135,9 @@ class ASRProvider(ASRProviderBase): samples_float32 = samples_float32 / 32768 return samples_float32, f.getframerate() - async def speech_to_text(self, opus_data: List[bytes], session_id: str) -> Tuple[Optional[str], Optional[str]]: + async def speech_to_text( + self, opus_data: List[bytes], session_id: str + ) -> Tuple[Optional[str], Optional[str]]: """语音转文本主处理逻辑""" file_path = None try: @@ -143,7 +145,9 @@ class ASRProvider(ASRProviderBase): start_time = time.time() pcm_data = self.decode_opus(opus_data, session_id) file_path = self.save_audio_to_file(pcm_data, session_id) - logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}") + logger.bind(tag=TAG).debug( + f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}" + ) # 语音识别 start_time = time.time() @@ -152,14 +156,15 @@ class ASRProvider(ASRProviderBase): s.accept_waveform(sample_rate, samples) self.model.decode_stream(s) text = s.result.text - logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}") + logger.bind(tag=TAG).debug( + f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}" + ) return text, file_path except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) return "", None - finally: # 文件清理逻辑 if self.delete_audio_file and file_path and os.path.exists(file_path): From b50c7dbed434afb20e34a92a438e056e4f48c4e4 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 10:38:18 +0800 Subject: [PATCH 14/53] =?UTF-8?q?update:=E4=BF=AE=E6=AD=A3docker=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E7=9B=AE=E5=BD=95uploadfile=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/docker-compose_all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/docker-compose_all.yml b/main/xiaozhi-server/docker-compose_all.yml index 91910120..b62188d9 100644 --- a/main/xiaozhi-server/docker-compose_all.yml +++ b/main/xiaozhi-server/docker-compose_all.yml @@ -49,7 +49,7 @@ services: - SPRING_DATA_REDIS_PORT=6379 volumes: # 配置文件目录 - - ./uploadfile:/app/uploadfile + - ./uploadfile:/uploadfile xiaozhi-esp32-server-db: image: mysql:latest From 9e9af2a031e20a9a84e2ccf1915d02bd5b8a9cca Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 10:53:29 +0800 Subject: [PATCH 15/53] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a783b3e9..a8d54c15 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server) -

小智后端服务xiaozhi-esp32-server

@@ -12,11 +11,10 @@

English -· 简体中文 -· 更新日志部署文档反馈问题 · 常见问题反馈问题部署文档更新日志

@@ -132,17 +130,7 @@ 1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。 建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。 -2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件 -`config.yaml` 中开启防护: - -```yaml -server: - auth: - # 开启防护 - enabled: true -``` - -开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。 +2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。 --- @@ -168,11 +156,6 @@ server: OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ ``` ---- - -## 常见问题 ❓ - -如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。 --- ## 功能清单 ✨ From 3ceb3feccf9c80030f7f8a1a8e24ba8bd276686a Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Wed, 30 Apr 2025 11:55:19 +0800 Subject: [PATCH 16/53] =?UTF-8?q?update:python=E9=83=A8=E5=88=86"=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E6=9B=B4=E6=96=B0=E9=85=8D=E7=BD=AE"=E7=9A=84?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 11 +++++++++-- main/xiaozhi-server/core/websocket_server.py | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 9a59d388..8f406ed1 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -42,9 +42,10 @@ class TTSException(RuntimeError): class ConnectionHandler: def __init__( - self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent + self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent, server=None ): - self.config = copy.deepcopy(config) + self.config = config + self.server = server self.logger = setup_logging() self.auth = AuthMiddleware(config) @@ -256,6 +257,12 @@ class ConnectionHandler: if config_model in ["llm", "tts", "asr", "vad", "intent", "memory"]: updated_modules.append(config_model) + # 同步更新 WebSocketServer 的配置 + if self.server: + async with self.server.config_lock: # 使用锁确保线程安全 + for config_model in updated_modules: + self.server.config[config_model].update(new_config[config_model]) + # 批量初始化模块 if updated_modules: try: diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py index fae94046..4b933865 100644 --- a/main/xiaozhi-server/core/websocket_server.py +++ b/main/xiaozhi-server/core/websocket_server.py @@ -11,6 +11,7 @@ class WebSocketServer: def __init__(self, config: dict): self.config = config self.logger = setup_logging() + self.config_lock = asyncio.Lock() modules = initialize_modules( self.logger, self.config, True, True, True, True, True, True ) @@ -43,6 +44,7 @@ class WebSocketServer: self._tts, self._memory, self._intent, + self # 传入当前 WebSocketServer 实例 ) self.active_connections.add(handler) try: From e8b1156bfdf2d938834417d3868d5603533f87a5 Mon Sep 17 00:00:00 2001 From: MakerZorky <1053714527zhq@gmail.com> Date: Wed, 30 Apr 2025 13:34:00 +0800 Subject: [PATCH 17/53] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0DoubaoASR=E7=83=AD?= =?UTF-8?q?=E8=AF=8D=E3=80=81=E6=9B=BF=E6=8D=A2=E8=AF=8D=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 2 ++ .../core/providers/asr/doubao.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 21266333..7565179c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -232,6 +232,8 @@ ASR: appid: 你的火山引擎语音合成服务appid access_token: 你的火山引擎语音合成服务access_token cluster: volcengine_input_common + boosting_table_name: 你的热词文件名称 + correct_table_name: 你的替换词文件名称 output_dir: tmp/ TencentASR: # token申请地址:https://console.cloud.tencent.com/cam/capi diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py index 6fba472c..755a19b7 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao.py +++ b/main/xiaozhi-server/core/providers/asr/doubao.py @@ -88,6 +88,8 @@ class ASRProvider(ASRProviderBase): self.appid = config.get("appid") self.cluster = config.get("cluster") self.access_token = config.get("access_token") + self.boosting_table_name = config.get("boosting_table_name") + self.correct_table_name = config.get("correct_table_name") self.output_dir = config.get("output_dir") self.delete_audio_file = delete_audio_file @@ -136,7 +138,13 @@ class ASRProvider(ASRProviderBase): "user": { "uid": str(uuid.uuid4()), }, - "request": {"reqid": reqid, "show_utterances": False, "sequence": 1}, + "request": { + "reqid": reqid, + "show_utterances": False, + "sequence": 1, + "boosting_table_name": self.boosting_table_name, + "correct_table_name": self.correct_table_name, + }, "audio": { "format": "raw", "rate": 16000, @@ -232,14 +240,6 @@ class ASRProvider(ASRProviderBase): return pcm_data - @staticmethod - def read_wav_info(data: io.BytesIO = None) -> (int, int, int, int, int): - with io.BytesIO(data) as _f: - wave_fp = wave.open(_f, "rb") - nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4] - wave_bytes = wave_fp.readframes(nframes) - return nchannels, sampwidth, framerate, nframes, len(wave_bytes) - @staticmethod def slice_data(data: bytes, chunk_size: int) -> (list, bool): """ From d2f8f05acbb75439b0b7a49fe02bf7d9cbe7fd3d Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 15:05:42 +0800 Subject: [PATCH 18/53] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=8A=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Deployment.md | 45 +++++++--------- .../modules/config/dto/AgentModelsDTO.java | 3 -- .../security/secret/ServerSecretFilter.java | 12 ++--- .../resources/db/changelog/202504082211.sql | 2 +- .../resources/db/changelog/202504301339.sql | 3 ++ .../db/changelog/db.changelog-master.yaml | 9 +++- main/xiaozhi-server/app.py | 3 +- main/xiaozhi-server/config.yaml | 10 ++-- main/xiaozhi-server/config/config_loader.py | 48 +++++++---------- main/xiaozhi-server/config/logger.py | 2 + main/xiaozhi-server/config/settings.py | 54 ++++++------------- main/xiaozhi-server/core/handle/textHandle.py | 2 + .../core/providers/tts/doubao.py | 11 ++-- .../core/providers/tts/fishspeech.py | 31 ++++++++--- .../core/providers/tts/gpt_sovits_v2.py | 33 +++++++++--- .../core/providers/tts/gpt_sovits_v3.py | 19 +++++-- .../core/providers/tts/openai.py | 6 ++- .../core/providers/vad/silero.py | 11 +++- 18 files changed, 167 insertions(+), 137 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301339.sql diff --git a/docs/Deployment.md b/docs/Deployment.md index 03147198..01f0afeb 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -81,7 +81,7 @@ xiaozhi-server 下载完后,回到本教程继续往下。 -##### 1.2.3.2 下载 config.yaml +##### 1.2.3.2 创建 config.yaml 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 @@ -224,43 +224,36 @@ python app.py ## 配置项目 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 -如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml` +如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种: -修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 +第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改 + +第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 -配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 -本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 +以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例 ``` +server: + websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/ +prompt: | + 我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 + 我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。 + 我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。 + 请你像一个人一样说话,请勿返回配置xml及其他特殊字符。 + selected_module: - VAD: SileroVAD - ASR: FunASR - LLM: ChatGLMLLM - TTS: EdgeTTS - # 默认不开启记忆,如需开启请看配置文件里的描述 - Memory: nomem - # 默认不开启意图识别,如需开启请看配置文件里的描述 - Intent: nointent -``` + LLM: DoubaoLLM -比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。 -使用时,在`selected_module`修改成对应的如下LLM配置的名称: - -``` LLM: - DeepSeekLLM: - type: openai - ... ChatGLMLLM: - type: openai - ... - DifyLLM: - type: dify - ... + api_key: xxxxxxxxxxxxxxx.xxxxxx ``` +建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。 +比如你要换更换模型,修改`selected_module`下的配置就行。 + ## 模型文件 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java index 4a07a743..47c08a81 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java @@ -10,9 +10,6 @@ import lombok.Data; @Data @Schema(description = "获取智能体模型配置DTO") public class AgentModelsDTO { - @NotBlank(message = "密钥不能为空") - @Schema(description = "密钥") - private String secret; @NotBlank(message = "设备MAC地址不能为空") @Schema(description = "设备MAC地址") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java index b33ce23d..b2a18547 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -2,8 +2,6 @@ package xiaozhi.modules.security.secret; import java.io.IOException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.web.bind.annotation.RequestMethod; @@ -12,6 +10,8 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.utils.HttpContextUtils; @@ -55,7 +55,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { String token = getRequestToken((HttpServletRequest) servletRequest); if (StringUtils.isBlank(token)) { // token为空,返回401 - this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "Authorization token不能为空"); + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空"); return false; } @@ -63,7 +63,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { String serverSecret = getServerSecret(); if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { // token无效,返回401 - this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的Authorization token"); + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥"); return false; } @@ -77,7 +77,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { response.setContentType("application/json;charset=utf-8"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); - + try { String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); response.getWriter().print(json); @@ -102,4 +102,4 @@ public class ServerSecretFilter extends AuthenticatingFilter { private String getServerSecret() { return sysParamsService.getValue(Constant.SERVER_SECRET, true); } -} +} diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 08e5dc5f..91ce33ff 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -25,7 +25,7 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f -- TTS模型供应器 ('SYSTEM_TTS_edge', 'TTS', 'edge', 'Edge TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), -('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]', 2, 1, NOW(), 1, NOW()), +('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key":"api_url","label":"API地址","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"authorization","label":"授权","type":"string"},{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_siliconflow', 'TTS', 'siliconflow', '硅基流动TTS', '[{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 3, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_cozecn', 'TTS', 'cozecn', 'COZECN TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 4, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_fishspeech', 'TTS', 'fishspeech', 'FishSpeech TTS', '[{"key":"output_dir","label":"输出目录","type":"string"},{"key":"response_format","label":"响应格式","type":"string"},{"key":"reference_id","label":"参考ID","type":"string"},{"key":"reference_audio","label":"参考音频","type":"dict","dict_name":"reference_audio"},{"key":"reference_text","label":"参考文本","type":"dict","dict_name":"reference_text"},{"key":"normalize","label":"是否标准化","type":"boolean"},{"key":"max_new_tokens","label":"最大新令牌数","type":"number"},{"key":"chunk_length","label":"块长度","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"repetition_penalty","label":"重复惩罚","type":"number"},{"key":"temperature","label":"温度","type":"number"},{"key":"streaming","label":"是否流式","type":"boolean"},{"key":"use_memory_cache","label":"是否使用内存缓存","type":"string"},{"key":"seed","label":"种子","type":"number"},{"key":"channels","label":"通道数","type":"number"},{"key":"rate","label":"采样率","type":"number"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"api_url","label":"API地址","type":"string"}]', 5, 1, NOW(), 1, NOW()), diff --git a/main/manager-api/src/main/resources/db/changelog/202504301339.sql b/main/manager-api/src/main/resources/db/changelog/202504301339.sql new file mode 100644 index 00000000..e4393f6a --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301339.sql @@ -0,0 +1,3 @@ +update `ai_model_provider` set `fields` = +'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' +where `id` = 'SYSTEM_TTS_doubao'; 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 131aaf01..90735fe1 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 @@ -85,4 +85,11 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504291043.sql \ No newline at end of file + path: classpath:db/changelog/202504291043.sql + - changeSet: + id: 202504301339 + author: Goody + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202504301339.sql \ No newline at end of file diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index 63b40d71..7ed71ae5 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -1,7 +1,7 @@ import asyncio import sys import signal -from config.settings import load_config, check_config_file +from config.settings import load_config from core.websocket_server import WebSocketServer from core.ota_server import SimpleOtaServer from core.utils.util import check_ffmpeg_installed @@ -31,7 +31,6 @@ async def wait_for_exit(): async def main(): - check_config_file() check_ffmpeg_installed() config = load_config() diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index db8dd5d5..3ce485ec 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -1,7 +1,7 @@ -# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。 -# 在开发中,在项目根目录创建data目录,将【config.yaml】复制一份,改成【.config.yaml】,放进data目录中 -# 系统会优先读取【data/.config.yaml】文件的配置。 -# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。 +# 在开发中,请在项目根目录创建data目录,然后在data目录创建名称为【.config.yaml】的空文件 +# 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件 +# 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。 +# 这样做,可以最简化配置,保护您的密钥安全。 # ##################################################################################### # #############################以下是服务器基本运行配置#################################### @@ -158,7 +158,7 @@ selected_module: # 不想开通意图识别,就设置成:nointent # 意图识别可使用intent_llm。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作 # 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令 - # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 + # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-1-5-pro-32k-250115 Intent: function_call # 意图识别,是用于理解用户意图的模块,例如:播放音乐 diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index b892b437..35802a67 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -31,37 +31,19 @@ def load_config(): # 加载默认配置 default_config = read_config(default_config_path) + custom_config = read_config(custom_config_path) - # 加载用户自定义配置(如果存在) - if os.path.exists(custom_config_path): - custom_config = read_config(custom_config_path) + if custom_config.get("manager-api", {}).get("url"): + config = get_config_from_api(custom_config) + else: # 合并配置 config = merge_configs(default_config, custom_config) - else: - config = default_config - - if config.get("manager-api", {}).get("url"): - config = get_config_from_api(config) - # 初始化目录 ensure_directories(config) _config_cache = config return config -def get_config_file(): - """获取配置文件路径,优先使用私有配置文件(若存在)。 - - Returns: - str: 配置文件路径(相对路径或默认路径) - """ - default_config_file = "config.yaml" - config_file = default_config_file - if os.path.exists(get_project_dir() + "data/." + default_config_file): - config_file = "data/." + default_config_file - return config_file - - def get_config_from_api(config): """从Java API获取配置""" # 初始化API客户端 @@ -128,23 +110,29 @@ def ensure_directories(config): def merge_configs(default_config, custom_config): """ 递归合并配置,custom_config优先级更高 - + Args: default_config: 默认配置 custom_config: 用户自定义配置 - + Returns: 合并后的配置 """ - if not isinstance(default_config, Mapping) or not isinstance(custom_config, Mapping): + if not isinstance(default_config, Mapping) or not isinstance( + custom_config, Mapping + ): return custom_config - + merged = dict(default_config) - + for key, value in custom_config.items(): - if key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping): + if ( + key in merged + and isinstance(merged[key], Mapping) + and isinstance(value, Mapping) + ): merged[key] = merge_configs(merged[key], value) else: merged[key] = value - - return merged \ No newline at end of file + + return merged diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 9eed8540..f20dc0ad 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -2,6 +2,7 @@ import os import sys from loguru import logger from config.config_loader import load_config +from config.settings import check_config_file SERVER_VERSION = "0.3.13" @@ -32,6 +33,7 @@ def formatter(record): def setup_logging(): + check_config_file() """从配置文件中读取日志配置,并设置日志输出格式和级别""" config = load_config() log_config = config["log"] diff --git a/main/xiaozhi-server/config/settings.py b/main/xiaozhi-server/config/settings.py index c12c70d8..fb8868b4 100644 --- a/main/xiaozhi-server/config/settings.py +++ b/main/xiaozhi-server/config/settings.py @@ -1,53 +1,33 @@ import os -from collections.abc import Mapping -from config import logger from config.config_loader import read_config, get_project_dir, load_config -TAG = __name__ -logger = logger.setup_logging() default_config_file = "config.yaml" - - -def find_missing_keys(new_config, old_config, parent_key=""): - """ - 递归查找缺失的配置项 - 返回格式:[缺失配置路径] - """ - missing_keys = [] - - if not isinstance(new_config, Mapping): - return missing_keys - - for key, value in new_config.items(): - # 构建当前配置路径 - full_path = f"{parent_key}.{key}" if parent_key else key - - # 检查键是否存在 - if key not in old_config: - missing_keys.append(full_path) - continue - - # 递归检查嵌套字典 - if isinstance(value, Mapping): - sub_missing = find_missing_keys( - value, old_config[key], parent_key=full_path - ) - missing_keys.extend(sub_missing) - return missing_keys +config_file_valid = False def check_config_file(): + global config_file_valid + if config_file_valid: + return """ 简化的配置检查,仅提示用户配置文件的使用情况 """ custom_config_file = get_project_dir() + "data/." + default_config_file if not os.path.exists(custom_config_file): - logger.bind(tag=TAG).info("提示: 使用默认配置文件。如需自定义配置,请创建 data/.config.yaml 文件") - else: - logger.bind(tag=TAG).info(f"提示: 使用自定义配置文件 data/.config.yaml,配置将覆盖默认值") - + raise FileNotFoundError( + "找不到data/.config.yaml文件,请按教程确认该配置文件是否存在" + ) + # 检查是否从API读取配置 config = load_config() if config.get("read_config_from_api", False): - logger.bind(tag=TAG).info("提示: 从API获取配置") + print("从API读取配置") + old_config_origin = read_config(custom_config_file) + if old_config_origin.get("selected_module") is not None: + error_msg = "您的配置文件好像既包含智控台的配置又包含本地配置:\n" + error_msg += "\n建议您:\n" + error_msg += "1、将根目录的config_from_api.yaml文件复制到data下,重命名为.config.yaml\n" + error_msg += "2、按教程配置好接口地址和密钥\n" + raise ValueError(error_msg) + config_file_valid = True diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index f0683d59..a26f5bd9 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -53,6 +53,8 @@ async def handleTextMessage(conn, message): # 如果是唤醒词,且关闭了唤醒词回复,就不用回答 await send_stt_message(conn, text) await send_tts_message(conn, "stop", None) + elif is_wakeup_words: + await startToChat(conn, "嘿,你好呀") else: # 否则需要LLM对文字内容进行答复 await startToChat(conn, text) diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index 1b6414c4..367a99c9 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,9 +27,14 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") - self.speed_ratio = float(config.get("speed_ratio", 0.1)) - self.volume_ratio = float(config.get("volume_ratio", 0.1)) - self.pitch_ratio = float(config.get("pitch_ratio", 0.1)) + # 处理空字符串的情况 + speed_ratio = config.get("speed_ratio", "1.0") + volume_ratio = config.get("volume_ratio", "1.0") + pitch_ratio = config.get("pitch_ratio", "1.0") + + self.speed_ratio = float(speed_ratio) if speed_ratio else 1.0 + self.volume_ratio = float(volume_ratio) if volume_ratio else 1.0 + self.pitch_ratio = float(pitch_ratio) if pitch_ratio else 1.0 self.api_url = config.get("api_url") self.authorization = config.get("authorization") diff --git a/main/xiaozhi-server/core/providers/tts/fishspeech.py b/main/xiaozhi-server/core/providers/tts/fishspeech.py index 30fee3c9..316dbed7 100644 --- a/main/xiaozhi-server/core/providers/tts/fishspeech.py +++ b/main/xiaozhi-server/core/providers/tts/fishspeech.py @@ -89,18 +89,35 @@ class TTSProvider(TTSProviderBase): self.reference_audio = parse_string_to_list(config.get("reference_audio")) self.reference_text = parse_string_to_list(config.get("reference_text")) self.format = config.get("format", "wav") - self.channels = int(config.get("channels", 1)) - self.rate = int(config.get("rate", 44100)) + self.api_key = config.get("api_key", "YOUR_API_KEY") have_key = check_model_key("FishSpeech TTS", self.api_key) if not have_key: return self.normalize = config.get("normalize", True) - self.max_new_tokens = int(config.get("max_new_tokens", 1024)) - self.chunk_length = int(config.get("chunk_length", 200)) - self.top_p = float(config.get("top_p", 0.7)) - self.repetition_penalty = float(config.get("repetition_penalty", 1.2)) - self.temperature = float(config.get("temperature", 0.7)) + + # 处理空字符串的情况 + channels = config.get("channels", "1") + rate = config.get("rate", "44100") + max_new_tokens = config.get("max_new_tokens", "1024") + chunk_length = config.get("chunk_length", "200") + + self.channels = int(channels) if channels else 1 + self.rate = int(rate) if rate else 44100 + self.max_new_tokens = int(max_new_tokens) if max_new_tokens else 1024 + self.chunk_length = int(chunk_length) if chunk_length else 200 + + # 处理空字符串的情况 + top_p = config.get("top_p", "0.7") + temperature = config.get("temperature", "0.7") + repetition_penalty = config.get("repetition_penalty", "1.2") + + self.top_p = float(top_p) if top_p else 0.7 + self.temperature = float(temperature) if temperature else 0.7 + self.repetition_penalty = ( + float(repetition_penalty) if repetition_penalty else 1.2 + ) + self.streaming = str(config.get("streaming", False)).lower() in ( "true", "1", diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py index ec6c2053..b2aad88a 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py @@ -20,12 +20,29 @@ class TTSProvider(TTSProviderBase): self.ref_audio_path = config.get("ref_audio_path") self.prompt_text = config.get("prompt_text") self.prompt_lang = config.get("prompt_lang", "zh") - self.top_k = int(config.get("top_k", 5)) - self.top_p = float(config.get("top_p", 1)) - self.temperature = float(config.get("temperature", 1)) + + # 处理空字符串的情况 + top_k = config.get("top_k", "5") + top_p = config.get("top_p", "1") + temperature = config.get("temperature", "1") + batch_threshold = config.get("batch_threshold", "0.75") + batch_size = config.get("batch_size", "1") + speed_factor = config.get("speed_factor", "1.0") + seed = config.get("seed", "-1") + repetition_penalty = config.get("repetition_penalty", "1.35") + + self.top_k = int(top_k) if top_k else 5 + self.top_p = float(top_p) if top_p else 1 + self.temperature = float(temperature) if temperature else 1 + self.batch_threshold = float(batch_threshold) if batch_threshold else 0.75 + self.batch_size = int(batch_size) if batch_size else 1 + self.speed_factor = float(speed_factor) if speed_factor else 1.0 + self.seed = int(seed) if seed else -1 + self.repetition_penalty = ( + float(repetition_penalty) if repetition_penalty else 1.35 + ) + self.text_split_method = config.get("text_split_method", "cut0") - self.batch_size = int(config.get("batch_size", 1)) - self.batch_threshold = float(config.get("batch_threshold", 0.75)) self.split_bucket = str(config.get("split_bucket", True)).lower() in ( "true", @@ -37,19 +54,19 @@ class TTSProvider(TTSProviderBase): "1", "yes", ) - self.speed_factor = float(config.get("speed_factor", 1.0)) + self.streaming_mode = str(config.get("streaming_mode", False)).lower() in ( "true", "1", "yes", ) - self.seed = int(config.get("seed", -1)) + self.parallel_infer = str(config.get("parallel_infer", True)).lower() in ( "true", "1", "yes", ) - self.repetition_penalty = float(config.get("repetition_penalty", 1.35)) + self.aux_ref_audio_paths = parse_string_to_list( config.get("aux_ref_audio_paths") ) diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py index b2746acf..488280f1 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py @@ -18,13 +18,22 @@ class TTSProvider(TTSProviderBase): self.prompt_text = config.get("prompt_text") self.prompt_language = config.get("prompt_language") self.text_language = config.get("text_language", "audo") - self.top_k = int(config.get("top_k", 15)) - self.top_p = float(config.get("top_p", 1.0)) - self.temperature = float(config.get("temperature", 1.0)) + + # 处理空字符串的情况 + top_k = config.get("top_k", "15") + top_p = config.get("top_p", "1.0") + temperature = config.get("temperature", "1.0") + sample_steps = config.get("sample_steps", "32") + speed = config.get("speed", "1.0") + + self.top_k = int(top_k) if top_k else 15 + self.top_p = float(top_p) if top_p else 1.0 + self.temperature = float(temperature) if temperature else 1.0 + self.sample_steps = int(sample_steps) if sample_steps else 32 + self.speed = float(speed) if speed else 1.0 + self.cut_punc = config.get("cut_punc", "") - self.speed = float(config.get("speed", 1.0)) self.inp_refs = parse_string_to_list(config.get("inp_refs")) - self.sample_steps = int(config.get("sample_steps", 32)) self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes") def generate_filename(self, extension=".wav"): diff --git a/main/xiaozhi-server/core/providers/tts/openai.py b/main/xiaozhi-server/core/providers/tts/openai.py index a5d48057..a7c4e30d 100644 --- a/main/xiaozhi-server/core/providers/tts/openai.py +++ b/main/xiaozhi-server/core/providers/tts/openai.py @@ -21,7 +21,11 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice", "alloy") self.response_format = "wav" - self.speed = float(config.get("speed", 1.0)) + + # 处理空字符串的情况 + speed = config.get("speed", "1.0") + self.speed = float(speed) if speed else 1.0 + self.output_file = config.get("output_dir", "tmp/") check_model_key("TTS", self.api_key) diff --git a/main/xiaozhi-server/core/providers/vad/silero.py b/main/xiaozhi-server/core/providers/vad/silero.py index 3332d9ac..8ec1f6eb 100644 --- a/main/xiaozhi-server/core/providers/vad/silero.py +++ b/main/xiaozhi-server/core/providers/vad/silero.py @@ -21,8 +21,15 @@ class VADProvider(VADProviderBase): (get_speech_timestamps, _, _, _, _) = self.utils self.decoder = opuslib_next.Decoder(16000, 1) - self.vad_threshold = float(config.get("threshold", 0.5)) - self.silence_threshold_ms = int(config.get("min_silence_duration_ms", 1000)) + + # 处理空字符串的情况 + threshold = config.get("threshold", "0.5") + min_silence_duration_ms = config.get("min_silence_duration_ms", "1000") + + self.vad_threshold = float(threshold) if threshold else 0.5 + self.silence_threshold_ms = ( + int(min_silence_duration_ms) if min_silence_duration_ms else 1000 + ) def is_vad(self, conn, opus_packet): try: From 48f8c6c5b7b09449aa2d2ebb492d012f16877a6c Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Wed, 30 Apr 2025 17:29:27 +0800 Subject: [PATCH 19/53] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0asr,tts?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E6=8A=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AgentChatHistoryController.java | 35 ++++++ .../xiaozhi/modules/agent/dao/AgentDao.java | 15 ++- .../agent/dao/AiAgentChatHistoryDao.java | 16 +++ .../agent/dto/AgentChatHistoryReportDTO.java | 36 ++++++ .../agent/entity/AgentChatHistoryEntity.java | 92 +++++++++++++++ .../service/AgentChatHistoryService.java | 14 +++ .../modules/agent/service/AgentService.java | 10 +- .../biz/AgentChatHistoryBizService.java | 22 ++++ .../impl/AgentChatHistoryBizServiceImpl.java | 83 ++++++++++++++ .../impl/AgentChatHistoryServiceImpl.java | 19 ++++ .../agent/service/impl/AgentServiceImpl.java | 10 +- .../modules/security/config/ShiroConfig.java | 1 + .../resources/db/changelog/202504301634.sql | 18 +++ .../mapper/agent/AiAgentChatHistoryDao.xml | 24 ++++ .../config/manage_api_client.py | 30 +++++ main/xiaozhi-server/core/connection.py | 106 +++++++++++++++++- .../core/handle/asrReportHandle.py | 100 +++++++++++++++++ .../core/handle/receiveAudioHandle.py | 4 + main/xiaozhi-server/core/handle/textHandle.py | 5 + .../core/handle/ttsReportHandle.py | 70 ++++++++++++ main/xiaozhi-server/core/utils/util.py | 2 +- 21 files changed, 706 insertions(+), 6 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301634.sql create mode 100644 main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml create mode 100644 main/xiaozhi-server/core/handle/asrReportHandle.py create mode 100644 main/xiaozhi-server/core/handle/ttsReportHandle.py diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java new file mode 100644 index 00000000..7abf3d13 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java @@ -0,0 +1,35 @@ +package xiaozhi.modules.agent.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; +import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; + +@Tag(name = "智能体聊天历史管理") +@RequiredArgsConstructor +@RestController +@RequestMapping("/agent/chat-history") +public class AgentChatHistoryController { + private final AgentChatHistoryBizService agentChatHistoryBizService; + + /** + * 小智服务聊天上报请求 + *

+ * 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。 + * + * @param request 包含上传文件及相关信息的请求对象 + */ + @Operation(summary = "小智服务聊天上报请求") + @PostMapping("/report") + public Result uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) { + Boolean result = agentChatHistoryBizService.report(request); + return new Result().ok(result); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java index 8c80d6cc..3b4be452 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java @@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import xiaozhi.common.dao.BaseDao; import xiaozhi.modules.agent.entity.AgentEntity; @@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao { * @return 设备数量 */ Integer getDeviceCountByAgentId(@Param("agentId") String agentId); -} \ No newline at end of file + + /** + * 根据设备MAC地址查询对应设备的默认智能体信息 + * + * @param macAddress 设备MAC地址 + * @return 默认智能体信息 + */ + @Select(" SELECT a.* FROM ai_device d " + + " LEFT JOIN ai_agent a ON d.agent_id = a.id " + + " WHERE d.mac_address = #{macAddress} " + + " ORDER BY d.id DESC LIMIT 1") + AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java new file mode 100644 index 00000000..9c07be1d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java @@ -0,0 +1,16 @@ +package xiaozhi.modules.agent.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; + +/** + * {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Mapper +public interface AiAgentChatHistoryDao extends BaseMapper { +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java new file mode 100644 index 00000000..0f79dda6 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java @@ -0,0 +1,36 @@ +package xiaozhi.modules.agent.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 小智设备聊天上报请求 + * + * @author Haotian + * @version 1.0, 2025/5/8 + */ +@Data +@Schema(description = "小智设备聊天上报请求") +public class AgentChatHistoryReportDTO { + @Schema(description = "MAC地址", example = "00:11:22:33:44:55") + @NotBlank + private String macAddress; + @Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a") + @NotBlank + private String sessionId; + @Schema(description = "排序值(与session_id对应)", example = "1745566378") + @NotNull + private Long sort; + @Schema(description = "消息类型: 1-用户, 2-智能体", example = "1") + @NotNull + private Byte chatType; + @Schema(description = "聊天内容", example = "你好呀") + @NotBlank + private String content; + @Schema(description = "文件数据(Base64编码)", example = "") + private String fileBase64; + @Schema(description = "文件扩展名(如wav、mp3等)", example = "wav") + private String fileExtension; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java new file mode 100644 index 00000000..3e486f43 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java @@ -0,0 +1,92 @@ +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; + +/** + * 智能体聊天记录表 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "ai_agent_chat_history") +public class AgentChatHistoryEntity { + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * MAC地址 + */ + @TableField(value = "mac_address") + private String macAddress; + + /** + * 智能体id + */ + @TableField(value = "agent_id") + private String agentId; + + /** + * 会话ID + */ + @TableField(value = "session_id") + private String sessionId; + + /** + * 排序值(与session_id对应),使用时间戳,方便排序 + */ + @TableField(value = "sort") + private Long sort; + + /** + * 消息类型: 1-用户, 2-智能体 + */ + @TableField(value = "chat_type") + private Byte chatType; + + /** + * 聊天内容 + */ + @TableField(value = "content") + private String content; + + /** + * 音频base64数据 + */ + @TableField(value = "audio") + private String audio; + + /** + * 音频URL + */ + @TableField(value = "audio_url") + private String audioUrl; + + /** + * 创建时间 + */ + @TableField(value = "created_at") + private Date createdAt; + + /** + * 更新时间 + */ + @TableField(value = "updated_at") + private Date updatedAt; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java new file mode 100644 index 00000000..e852cae3 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java @@ -0,0 +1,14 @@ +package xiaozhi.modules.agent.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; + +/** + * 智能体聊天记录表处理service + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +public interface AgentChatHistoryService extends IService { +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java index 8d0b2959..48355a4b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java @@ -42,4 +42,12 @@ public interface AgentService extends BaseService { * @return 设备数量 */ Integer getDeviceCountByAgentId(String agentId); -} \ No newline at end of file + + /** + * 根据设备MAC地址查询对应设备的默认智能体信息 + * + * @param macAddress 设备MAC地址 + * @return 默认智能体信息,不存在时返回null + */ + AgentEntity getDefaultAgentByMacAddress(String macAddress); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java new file mode 100644 index 00000000..191b231b --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java @@ -0,0 +1,22 @@ +package xiaozhi.modules.agent.service.biz; + +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; + +/** + * 智能体聊天历史业务逻辑层 + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +public interface AgentChatHistoryBizService { + + /** + * 聊天上报方法 + * + * @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象 + * 例如:设备MAC地址、文件类型、内容等 + * @return 上传结果,true表示成功,false表示失败 + */ + Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java new file mode 100644 index 00000000..ae99455d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java @@ -0,0 +1,83 @@ +package xiaozhi.modules.agent.service.biz.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO; +import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService; + +import javax.annotation.Nullable; + +/** + * {@link AgentChatHistoryBizService} impl + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService { + private final AgentService agentService; + private final AgentChatHistoryService agentChatHistoryService; + + /** + * 处理聊天记录上报,包括文件上传和相关信息记录 + * + * @param report 包含聊天上报所需信息的输入对象 + * @return 上传结果,true表示成功,false表示失败 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean report(AgentChatHistoryReportDTO report) { + final String macAddress = report.getMacAddress(); + final Byte chatType = report.getChatType(); + log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType); + + // 1. 上传音频文件 + final String uploadUrl = this.upload(report); + + // 2. 组装上报数据 + // 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报 + AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress); + if (agentEntity == null) { + return false; + } + final String agentId = agentEntity.getId(); + log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId()); + + // 2.2 构建聊天记录实体 + final AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder() + .macAddress(macAddress) + .agentId(agentId) + .sessionId(report.getSessionId()) + .sort(report.getSort()) + .chatType(report.getChatType()) + .content(report.getContent()) + .audio(report.getFileBase64()) + .audioUrl(uploadUrl) + .build(); + + // 3. 保存数据 + agentChatHistoryService.save(entity); + return Boolean.TRUE; + } + + /** + * 上传文件 + * + * @param report 上报文件数据 + * @return 上传文件url + */ + @Nullable + private String upload(AgentChatHistoryReportDTO report) { + // TODO(haotian): 2025/4/30 根据需要自定义完成上传生成url即可 + return null; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java new file mode 100644 index 00000000..95293a3f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java @@ -0,0 +1,19 @@ +package xiaozhi.modules.agent.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; +import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao; +import xiaozhi.modules.agent.entity.AgentChatHistoryEntity; +import xiaozhi.modules.agent.service.AgentChatHistoryService; + +/** + * 智能体聊天记录表处理service {@link AgentChatHistoryService} impl + * + * @author Goody + * @version 1.0, 2025/4/30 + * @since 1.0.0 + */ +@Service +public class AgentChatHistoryServiceImpl extends ServiceImpl implements AgentChatHistoryService { + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index ff284ef0..4c529882 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -130,4 +130,12 @@ public class AgentServiceImpl extends BaseServiceImpl imp return deviceCount != null ? deviceCount : 0; } -} \ No newline at end of file + + @Override + public AgentEntity getDefaultAgentByMacAddress(String macAddress) { + if (StringUtils.isEmpty(macAddress)) { + return null; + } + return agentDao.getDefaultAgentByMacAddress(macAddress); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 62e192b7..b462b2c4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -84,6 +84,7 @@ public class ShiroConfig { filterMap.put("/user/register", "anon"); // 将config路径使用server服务过滤器 filterMap.put("/config/**", "server"); + filterMap.put("/agent/chat-history/report", "server"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); diff --git a/main/manager-api/src/main/resources/db/changelog/202504301634.sql b/main/manager-api/src/main/resources/db/changelog/202504301634.sql new file mode 100644 index 00000000..6a86a417 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301634.sql @@ -0,0 +1,18 @@ +-- 初始化智能体聊天记录 +DROP TABLE IF EXISTS ai_agent_chat_history; +CREATE TABLE ai_agent_chat_history +( + id BIGINT AUTO_INCREMENT COMMENT '主键ID' + PRIMARY KEY, + mac_address VARCHAR(50) COMMENT 'MAC地址', + agent_id BIGINT DEFAULT 0 COMMENT '智能体id', + session_id VARCHAR(50) COMMENT '会话ID', + sort BIGINT COMMENT '排序值(与session_id对应),使用时间戳,方便排序', + chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体', + content VARCHAR(1024) COMMENT '聊天内容', + audio text COMMENT '音频base64数据', + audio_url VARCHAR(256) COMMENT '音频URL', + created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间', + updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + INDEX idx_mac_session (mac_address, sort) +) COMMENT '智能体聊天记录表'; diff --git a/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml new file mode 100644 index 00000000..2d87d8cf --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + id, mac_address, agent_id, session_id, sort, chat_type, content, audio, audio_url, + created_at, updated_at + + diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 770cbf56..ec8fd30a 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -1,3 +1,4 @@ +import base64 import os import time from typing import Optional, Dict @@ -145,6 +146,35 @@ def get_agent_models( }, ) +async def report(mac_address: str, + session_id: str, + sort: int, + chat_type: int, + content: str, + audio, + file_extension: str = "wav", + need_report: bool = None, + report_type: int = None, + reported: bool = None) -> Optional[Dict]: + """带熔断的业务方法示例""" + if not content or not ManageApiClient._instance: + return None + return await ManageApiClient._instance._execute_request( + "POST", + f"/agent/chat-history/report", + json = { + "macAddress": mac_address, + "sessionId": session_id, + "sort": sort, + "chatType": chat_type, + "content": content, + "fileBase64": base64.b64encode(audio).decode('utf-8'), + "fileExtension": file_extension, + "needReport": need_report, + "reportType": report_type, + "reported": reported + } + ) def init_service(config): ManageApiClient(config) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index fbd3569e..1d1caf04 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -30,6 +30,7 @@ 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 +from core.handle.ttsReportHandle import enqueue_tts_report TAG = __name__ @@ -54,6 +55,7 @@ class ConnectionHandler: self.websocket = None self.headers = None + self.device_id = None self.client_ip = None self.client_ip_info = {} self.session_id = None @@ -72,6 +74,13 @@ class ConnectionHandler: self.audio_play_queue = queue.Queue() self.executor = ThreadPoolExecutor(max_workers=10) + # 上报线程标志 + self.session_open_time = time.time() + self.tts_report_queue = queue.Queue() + self.asr_report_queue = queue.Queue() + self.asr_report_thread = None + self.tts_report_thread = None + # 依赖的组件 self.vad = _vad self.asr = _asr @@ -153,6 +162,7 @@ class ConnectionHandler: # 认证通过,继续处理 self.websocket = ws + self.device_id = self.headers.get("device-id", None) self.session_id = str(uuid.uuid4()) # 启动超时检查任务 @@ -301,6 +311,26 @@ class ConnectionHandler: self._initialize_memory() """加载意图识别""" self._initialize_intent() + """初始化上报线程""" + self._init_report_threads() + + def _init_report_threads(self): + """初始化ASR和TTS上报线程""" + if self.asr_report_thread is None or not self.asr_report_thread.is_alive(): + self.asr_report_thread = threading.Thread( + target=self._asr_report_worker, + daemon=True + ) + self.asr_report_thread.start() + self.logger.bind(tag=TAG).info("ASR上报线程已启动") + + if self.tts_report_thread is None or not self.tts_report_thread.is_alive(): + self.tts_report_thread = threading.Thread( + target=self._tts_report_worker, + daemon=True + ) + self.tts_report_thread.start() + self.logger.bind(tag=TAG).info("TTS上报线程已启动") def _initialize_private_config(self): read_config_from_api = self.config.get("read_config_from_api", False) @@ -427,8 +457,7 @@ class ConnectionHandler: def _initialize_memory(self): """初始化记忆模块""" - device_id = self.headers.get("device-id", None) - self.memory.init_memory(device_id, self.llm) + self.memory.init_memory(self.device_id, self.llm) def _initialize_intent(self): if ( @@ -862,6 +891,9 @@ class ConnectionHandler: f"TTS生成:文件路径: {tts_file}" ) if os.path.exists(tts_file): + # 在这里上报TTS数据(使用文件路径) + enqueue_tts_report(self, text, tts_file) + opus_datas, duration = self.tts.audio_to_opus_data(tts_file) else: self.logger.bind(tag=TAG).error( @@ -918,6 +950,72 @@ class ConnectionHandler: f"audio_play_priority priority_thread: {text} {e}" ) + def _asr_report_worker(self): + """ASR上报工作线程""" + # 提前导入避免循环引用问题 + from core.handle.asrReportHandle import report_asr + + while not self.stop_event.is_set(): + try: + # 从队列获取数据,设置超时以便定期检查停止事件 + item = self.asr_report_queue.get(timeout=1) + if item is None: # 检测毒丸对象 + break + + text, file_path = item + + try: + # 执行上报(传入文件路径) + await_result = report_asr(self, text, file_path) + + # 使用asyncio.run_coroutine_threadsafe执行异步操作 + future = asyncio.run_coroutine_threadsafe(await_result, self.loop) + future.result() + except Exception as e: + self.logger.bind(tag=TAG).error(f"ASR上报线程异常: {e}") + finally: + # 标记任务完成 + self.asr_report_queue.task_done() + except queue.Empty: + continue + except Exception as e: + self.logger.bind(tag=TAG).error(f"ASR上报工作线程异常: {e}") + + self.logger.bind(tag=TAG).info("ASR上报线程已退出") + + def _tts_report_worker(self): + """TTS上报工作线程""" + # 提前导入避免循环引用问题 + from core.handle.ttsReportHandle import report_tts + + while not self.stop_event.is_set(): + try: + # 从队列获取数据,设置超时以便定期检查停止事件 + item = self.tts_report_queue.get(timeout=1) + if item is None: # 检测毒丸对象 + break + + text, audio_data = item + + try: + # 执行上报(传入二进制数据) + await_result = report_tts(self, text, audio_data) + + # 使用asyncio.run_coroutine_threadsafe执行异步操作 + future = asyncio.run_coroutine_threadsafe(await_result, self.loop) + future.result() + except Exception as e: + self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}") + finally: + # 标记任务完成 + self.tts_report_queue.task_done() + except queue.Empty: + continue + except Exception as e: + self.logger.bind(tag=TAG).error(f"TTS上报工作线程异常: {e}") + + self.logger.bind(tag=TAG).info("TTS上报线程已退出") + def speak_and_play(self, text, text_index=0): if text is None or len(text) <= 0: self.logger.bind(tag=TAG).info(f"无需tts转换,query为空,{text}") @@ -963,6 +1061,10 @@ class ConnectionHandler: self.executor.shutdown(wait=False, cancel_futures=True) self.executor = None + # 添加毒丸对象到上报队列确保线程退出 + self.asr_report_queue.put(None) + self.tts_report_queue.put(None) + # 清空任务队列 self.clear_queues() diff --git a/main/xiaozhi-server/core/handle/asrReportHandle.py b/main/xiaozhi-server/core/handle/asrReportHandle.py new file mode 100644 index 00000000..70601804 --- /dev/null +++ b/main/xiaozhi-server/core/handle/asrReportHandle.py @@ -0,0 +1,100 @@ +""" +ASR上报功能已集成到ConnectionHandler类中。 + +上报功能包括: +1. 每个连接对象拥有自己的上报队列和处理线程 +2. 上报线程的生命周期与连接对象绑定 +3. 使用ConnectionHandler.enqueue_asr_report方法进行上报 + +具体实现请参考core/connection.py中的相关代码。 +""" + +import os +from config.logger import setup_logging +from config.manage_api_client import report + +TAG = __name__ +logger = setup_logging() + +async def report_asr(conn, text, file_path): + """执行ASR上报操作 + + Args: + conn: 连接对象 + text: 识别文本 + file_path: 音频文件路径(可以为None或空字符串,表示纯文本上报) + """ + audio_data = None + try: + # 处理无音频的纯文本上报 + if not file_path or not os.path.exists(file_path): + # 纯文本上报时使用空音频数据 + result = await report( + mac_address=conn.device_id, + session_id=conn.session_id, + sort=int(conn.session_open_time), + chat_type=1, # ASR类型为1 + content=text, + audio=b'', # 空音频数据 + file_extension="wav" + ) + logger.bind(tag=TAG).info(f"纯文本上报成功: {conn.device_id}, {conn.session_id}") + else: + # 读取文件为二进制数据 + with open(file_path, 'rb') as f: + audio_data = f.read() + + # 正常ASR上报(带音频) + result = await report( + mac_address=conn.device_id, + session_id=conn.session_id, + sort=int(conn.session_open_time), + chat_type=1, # ASR类型为1 + content=text, + audio=audio_data, + file_extension="wav" + ) + logger.bind(tag=TAG).info(f"ASR上报成功: {conn.device_id}, {conn.session_id},文件: {file_path}") + + return result + except Exception as e: + logger.bind(tag=TAG).error(f"ASR上报失败: {e}") + return None + finally: + # 清理资源 + if file_path and os.path.exists(file_path): + try: + os.remove(file_path) + logger.bind(tag=TAG).debug(f"ASR上报后删除文件: {file_path}") + except Exception as e: + logger.bind(tag=TAG).error(f"ASR上报后删除文件失败: {e}") + + # 手动清理audio_data + if audio_data: + del audio_data + +def enqueue_asr_report(conn, text, audio): + """将ASR数据加入上报队列 + + Args: + conn: 连接对象 + text: 识别文本 + audio: 音频数据(可以为空列表,表示纯文本上报) + """ + try: + if not audio or len(audio) == 0: + # 纯文本上报,不需要保存文件 + file_path = None + else: + # 保存音频数据到文件 + file_path = conn.asr.save_audio_to_file(audio, conn.session_id) + + # 使用连接对象的队列,传入文件路径 + conn.asr_report_queue.put((text, file_path)) + + if not audio or len(audio) == 0: + logger.bind(tag=TAG).info(f"纯文本数据已加入上报队列: {conn.device_id}, {text[:20] if text else ''}...") + else: + logger.bind(tag=TAG).info(f"ASR数据已加入上报队列: {conn.device_id}, 文件: {file_path}") + except Exception as e: + logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {e}") diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index 485544f7..2d8b0eef 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -4,6 +4,7 @@ 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 +from core.handle.asrReportHandle import enqueue_asr_report TAG = __name__ logger = setup_logging() @@ -40,6 +41,9 @@ async def handleAudioMessage(conn, audio): logger.bind(tag=TAG).info(f"识别文本: {text}") text_len, _ = remove_punctuation_and_length(text) if text_len > 0: + # 使用自定义模块进行上报 + enqueue_asr_report(conn, text, conn.asr_audio) + await startToChat(conn, text) else: conn.asr_server_receive = True diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index a26f5bd9..fa81eb76 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -6,6 +6,7 @@ from core.utils.util import remove_punctuation_and_length from core.handle.receiveAudioHandle import startToChat, handleAudioMessage from core.handle.sendAudioHandle import send_stt_message, send_tts_message from core.handle.iotHandle import handleIotDescriptors, handleIotStatus +from core.handle.asrReportHandle import enqueue_asr_report import asyncio TAG = __name__ @@ -54,8 +55,12 @@ async def handleTextMessage(conn, message): await send_stt_message(conn, text) await send_tts_message(conn, "stop", None) elif is_wakeup_words: + # 上报纯文字数据(复用ASR上报功能,但不提供音频数据) + enqueue_asr_report(conn, "嘿,你好呀", []) await startToChat(conn, "嘿,你好呀") else: + # 上报纯文字数据(复用ASR上报功能,但不提供音频数据) + enqueue_asr_report(conn, text, []) # 否则需要LLM对文字内容进行答复 await startToChat(conn, text) elif msg_json["type"] == "iot": diff --git a/main/xiaozhi-server/core/handle/ttsReportHandle.py b/main/xiaozhi-server/core/handle/ttsReportHandle.py new file mode 100644 index 00000000..ed3a6a76 --- /dev/null +++ b/main/xiaozhi-server/core/handle/ttsReportHandle.py @@ -0,0 +1,70 @@ +""" +TTS上报功能已集成到ConnectionHandler类中。 + +上报功能包括: +1. 每个连接对象拥有自己的上报队列和处理线程 +2. 上报线程的生命周期与连接对象绑定 +3. 使用ConnectionHandler.enqueue_tts_report方法进行上报 + +具体实现请参考core/connection.py中的相关代码。 +""" + +import os +from config.logger import setup_logging +from config.manage_api_client import report + +TAG = __name__ +logger = setup_logging() + +async def report_tts(conn, text, audio_data): + """执行TTS上报操作 + + Args: + conn: 连接对象 + text: 合成文本 + audio_data: 音频二进制数据 + """ + try: + # 执行上报 + result = await report( + mac_address=conn.device_id, + session_id=conn.session_id, + sort=int(conn.session_open_time), + chat_type=2, # TTS类型为2 + content=text, + audio=audio_data, + file_extension="wav" + ) + logger.bind(tag=TAG).info(f"TTS上报成功: {conn.device_id}, {conn.session_id}, 数据大小: {len(audio_data)} 字节") + return result + except Exception as e: + logger.bind(tag=TAG).error(f"TTS上报失败: {e}") + return None + finally: + # 手动清理audio_data引用,帮助垃圾回收 + del audio_data + +def enqueue_tts_report(conn, text, file_path): + """将TTS数据加入上报队列 + + Args: + conn: 连接对象 + text: 合成文本 + file_path: TTS音频文件路径 + """ + try: + # 检查文件是否存在 + if not file_path or not os.path.exists(file_path): + logger.bind(tag=TAG).error(f"加入TTS上报队列失败: 文件不存在 {file_path}") + return + + # 立即读取文件为二进制数据,因为外部会删除文件 + with open(file_path, 'rb') as f: + audio_data = f.read() + + # 使用连接对象的队列,传入文本和二进制数据而非文件路径 + conn.tts_report_queue.put((text, audio_data)) + + logger.bind(tag=TAG).info(f"TTS数据已加入上报队列: {conn.device_id}, 文件大小: {len(audio_data)} 字节") + except Exception as e: + logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {e}, 文件: {file_path}") diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index 1cc9742b..a1956cee 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -484,4 +484,4 @@ def analyze_emotion(text): if emotion in top_emotions: return emotion - return top_emotions[0] # 如果都不在优先级列表里,返回第一个 \ No newline at end of file + return top_emotions[0] # 如果都不在优先级列表里,返回第一个 From 17192440158c70060e1e84612164fa9ac5c3c887 Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Wed, 30 Apr 2025 18:11:46 +0800 Subject: [PATCH 20/53] =?UTF-8?q?update:=E4=BF=AE=E6=94=B9python=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=9A=84=E2=80=9C=E5=8A=A8=E6=80=81=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E2=80=9D=E7=9A=84=E9=AA=8C=E8=AF=81=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 11 ----------- main/xiaozhi-server/core/handle/textHandle.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index fbd3569e..a8259e34 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -222,19 +222,8 @@ class ConnectionHandler: async def handle_config_update(self, message): """处理配置更新请求""" content = message.get("content", {}) - secret = content.pop("secret", None) new_config = content - # 密钥验证 - if secret != "43c716ae-37c4-49ba-84ab-2f0fbfcd7115": - self.logger.bind(tag=TAG).warning("配置更新请求的密钥不正确") - await self.websocket.send(json.dumps({ - "type": "config_update_response", - "status": "error", - "message": "密钥不正确" - })) - return - # 遍历所有支持的配置模块 updated_modules = [] for config_model in ["tts", "llm", "vad", "asr", "memory", "intent"]: diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index a26f5bd9..9afe7b62 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -64,6 +64,22 @@ async def handleTextMessage(conn, message): if "states" in msg_json: asyncio.create_task(handleIotStatus(conn, msg_json["states"])) elif msg_json["type"] == "server": + # 如果配置是从API读取的,则需要验证secret + read_config_from_api = conn.config.get("read_config_from_api", False) + if not read_config_from_api: + return + # 获取post请求的secret + post_secret = msg_json.get("content", {}).get("secret", "") + secret = conn.config["manager-api"].get("secret", "") + # 如果secret不匹配,则返回 + if post_secret != secret: + await conn.websocket.send(json.dumps({ + "type": "config_update_response", + "status": "error", + "message": "服务器密钥验证失败" + })) + return + # 动态更新配置 if msg_json["action"] == "update_config": await conn.handle_config_update(msg_json) except json.JSONDecodeError: From 54692859fde8782962d84b87a25a1a91aad3a8a6 Mon Sep 17 00:00:00 2001 From: MakerZorky <152245790+MakerZorky@users.noreply.github.com> Date: Wed, 30 Apr 2025 19:23:44 +0800 Subject: [PATCH 21/53] =?UTF-8?q?Update:config.yaml=E7=83=AD=E8=AF=8D?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 7565179c..6948aebd 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -232,8 +232,9 @@ ASR: appid: 你的火山引擎语音合成服务appid access_token: 你的火山引擎语音合成服务access_token cluster: volcengine_input_common - boosting_table_name: 你的热词文件名称 - correct_table_name: 你的替换词文件名称 + # 热词、替换词使用流程:https://www.volcengine.com/docs/6561/155738 + boosting_table_name: (选填)你的热词文件名称 + correct_table_name: (选填)你的替换词文件名称 output_dir: tmp/ TencentASR: # token申请地址:https://console.cloud.tencent.com/cam/capi From 48f7ef2023be058a82adee6e3575dff782dd9e1b Mon Sep 17 00:00:00 2001 From: zccaif Date: Wed, 30 Apr 2025 22:46:55 +0800 Subject: [PATCH 22/53] =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=AD=97=E5=85=B8?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sys/controller/AdminController.java | 1 - .../sys/controller/SysDictDataController.java | 96 +++++++++++++++++ .../sys/controller/SysDictTypeController.java | 93 ++++++++++++++++ .../modules/sys/dao/SysDictDataDao.java | 10 -- .../modules/sys/dao/SysDictTypeDao.java | 8 -- .../modules/sys/dto/SysDictTypeDTO.java | 1 + .../xiaozhi/modules/sys/entity/DictData.java | 16 --- .../xiaozhi/modules/sys/entity/DictType.java | 19 ---- .../modules/sys/entity/SysDictTypeEntity.java | 2 +- .../sys/service/SysDictDataService.java | 32 +++++- .../sys/service/SysDictTypeService.java | 42 ++++++-- .../service/impl/SysDictDataServiceImpl.java | 79 ++++++++++++-- .../service/impl/SysDictTypeServiceImpl.java | 102 +++++++++++++----- .../service/impl/SysUserUtilServiceImpl.java | 4 +- .../xiaozhi/modules/sys/vo/SysDictDataVO.java | 50 +++++++++ .../xiaozhi/modules/sys/vo/SysDictTypeVO.java | 47 ++++++++ .../resources/mapper/sys/SysDictDataDao.xml | 7 -- .../resources/mapper/sys/SysDictTypeDao.xml | 4 - 18 files changed, 498 insertions(+), 115 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java delete mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java delete mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java index e202e1e4..2ccc1e6a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/AdminController.java @@ -58,7 +58,6 @@ public class AdminController { dto.setLimit((String) params.get(Constant.LIMIT)); dto.setPage((String) params.get(Constant.PAGE)); ValidatorUtils.validateEntity(dto); - ValidatorUtils.validateEntity(dto); PageData page = sysUserService.page(dto); return new Result>().ok(page); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java new file mode 100644 index 00000000..e1a91df9 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictDataController.java @@ -0,0 +1,96 @@ +package xiaozhi.modules.sys.controller; + +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.Result; +import xiaozhi.common.validator.ValidatorUtils; +import xiaozhi.modules.sys.dto.SysDictDataDTO; +import xiaozhi.modules.sys.service.SysDictDataService; +import xiaozhi.modules.sys.vo.SysDictDataVO; + +/** + * 字典数据管理 + * + * @author czc + * @since 2025-04-30 + */ +@AllArgsConstructor +@RestController +@RequestMapping("/admin/dict/data") +@Tag(name = "字典数据管理") +public class SysDictDataController { + private final SysDictDataService sysDictDataService; + + @GetMapping("/page") + @Operation(summary = "分页查询字典数据") + @RequiresPermissions("sys:role:superAdmin") + @Parameters({@Parameter(name = "dictTypeId", description = "字典类型ID", required = true), + @Parameter(name = "dictLabel", description = "数据标签"), @Parameter(name = "dictValue", description = "数据值"), + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)}) + public Result> page(@Parameter(hidden = true) @RequestParam Map params) { + ValidatorUtils.validateEntity(params); + // 强制校验dictTypeId是否存在 + if (!params.containsKey("dictTypeId") || StringUtils.isEmpty(String.valueOf(params.get("dictTypeId")))) { + return new Result>().error("dictTypeId不能为空"); + } + + PageData page = sysDictDataService.page(params); + return new Result>().ok(page); + } + + @GetMapping("/{id}") + @Operation(summary = "获取字典数据详情") + @RequiresPermissions("sys:role:superAdmin") + public Result get(@PathVariable("id") Long id) { + SysDictDataVO vo = sysDictDataService.get(id); + return new Result().ok(vo); + } + + @PostMapping("/save") + @Operation(summary = "新增字典数据") + @RequiresPermissions("sys:role:superAdmin") + public Result save(@RequestBody SysDictDataDTO dto) { + ValidatorUtils.validateEntity(dto); + sysDictDataService.save(dto); + return new Result<>(); + } + + @PutMapping("/update") + @Operation(summary = "修改字典数据") + @RequiresPermissions("sys:role:superAdmin") + public Result update(@RequestBody SysDictDataDTO dto) { + ValidatorUtils.validateEntity(dto); + sysDictDataService.update(dto); + return new Result<>(); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典数据") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody Long[] ids) { + sysDictDataService.delete(ids); + return new Result<>(); + } + +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java new file mode 100644 index 00000000..bf152c19 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java @@ -0,0 +1,93 @@ +package xiaozhi.modules.sys.controller; + +import java.util.Map; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.page.PageData; +import xiaozhi.common.utils.Result; +import xiaozhi.common.validator.ValidatorUtils; +import xiaozhi.modules.sys.dto.SysDictTypeDTO; +import xiaozhi.modules.sys.service.SysDictTypeService; +import xiaozhi.modules.sys.vo.SysDictTypeVO; + +/** + * 字典类型管理 + * + * @author czc + * @since 2025-04-30 + */ +@AllArgsConstructor +@RestController +@RequestMapping("/admin/dict/type") +@Tag(name = "字典类型管理") +public class SysDictTypeController { + private final SysDictTypeService sysDictTypeService; + + @GetMapping("/page") + @Operation(summary = "分页查询字典类型") + @RequiresPermissions("sys:role:superAdmin") + @Parameters({@Parameter(name = "dictType", description = "字典类型编码"), + @Parameter(name = "dictName", description = "字典类型名称"), + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)}) + public Result> page(@Parameter(hidden = true) @RequestParam Map params) { + ValidatorUtils.validateEntity(params); + PageData page = sysDictTypeService.page(params); + return new Result>().ok(page); + } + + @GetMapping("/{id}") + @Operation(summary = "获取字典类型详情") + @RequiresPermissions("sys:role:superAdmin") + public Result get(@PathVariable("id") Long id) { + SysDictTypeVO vo = sysDictTypeService.get(id); + return new Result().ok(vo); + } + + @PostMapping("/save") + @Operation(summary = "保存字典类型") + @RequiresPermissions("sys:role:superAdmin") + public Result save(@RequestBody SysDictTypeDTO dto) { + // 参数校验 + ValidatorUtils.validateEntity(dto); + + sysDictTypeService.save(dto); + return new Result<>(); + } + + @PutMapping("/update") + @Operation(summary = "修改字典类型") + @RequiresPermissions("sys:role:superAdmin") + public Result update(@RequestBody SysDictTypeDTO dto) { + // 参数校验 + ValidatorUtils.validateEntity(dto); + + sysDictTypeService.update(dto); + return new Result<>(); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典类型") + @RequiresPermissions("sys:role:superAdmin") + @Parameter(name = "ids", description = "ID数组", required = true) + public Result delete(@RequestBody Long[] ids) { + sysDictTypeService.delete(ids); + return new Result<>(); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java index 9dd124fe..3ca5fb8c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java @@ -1,12 +1,8 @@ package xiaozhi.modules.sys.dao; -import java.util.List; - import org.apache.ibatis.annotations.Mapper; import xiaozhi.common.dao.BaseDao; -import xiaozhi.modules.sys.dto.SysDictDataDTO; -import xiaozhi.modules.sys.entity.DictData; import xiaozhi.modules.sys.entity.SysDictDataEntity; /** @@ -15,10 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity; @Mapper public interface SysDictDataDao extends BaseDao { - /** - * 字典数据列表 - */ - List getDictDataList(); - - List getDataByTypeCode(String dictType); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java index 5a9d5a27..6c271e31 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictTypeDao.java @@ -1,11 +1,8 @@ package xiaozhi.modules.sys.dao; -import java.util.List; - import org.apache.ibatis.annotations.Mapper; import xiaozhi.common.dao.BaseDao; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; /** @@ -14,9 +11,4 @@ import xiaozhi.modules.sys.entity.SysDictTypeEntity; @Mapper public interface SysDictTypeDao extends BaseDao { - /** - * 字典类型列表 - */ - List getDictTypeList(); - } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java index 01d69795..912c817c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/SysDictTypeDTO.java @@ -51,5 +51,6 @@ public class SysDictTypeDTO implements Serializable { @Schema(description = "更新时间") @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JsonFormat(pattern = DateUtils.DATE_TIME_PATTERN) private Date updateDate; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java deleted file mode 100644 index c8deac81..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictData.java +++ /dev/null @@ -1,16 +0,0 @@ -package xiaozhi.modules.sys.entity; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import lombok.Data; - -/** - * 字典数据 - */ -@Data -public class DictData { - @JsonIgnore - private Long dictTypeId; - private String dictLabel; - private String dictValue; -} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java deleted file mode 100644 index 4a1e2ca5..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/DictType.java +++ /dev/null @@ -1,19 +0,0 @@ -package xiaozhi.modules.sys.entity; - -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnore; - -import lombok.Data; - -/** - * 字典类型 - */ -@Data -public class DictType { - @JsonIgnore - private Long id; - private String dictType; - private List dataList = new ArrayList<>(); -} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java index fa372055..350d857b 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/entity/SysDictTypeEntity.java @@ -18,7 +18,7 @@ import xiaozhi.common.entity.BaseEntity; @TableName("sys_dict_type") public class SysDictTypeEntity extends BaseEntity { /** - * 字典类型 + * 字典类型编码 */ private String dictType; /** diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java index a33df029..5b4a9317 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java @@ -6,20 +6,48 @@ import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.vo.SysDictDataVO; /** * 数据字典 */ public interface SysDictDataService extends BaseService { - PageData page(Map params); + /** + * 分页查询数据字典信息 + * + * @param params 查询参数,包含分页信息和查询条件 + * @return 返回数据字典的分页查询结果 + */ + PageData page(Map params); - SysDictDataDTO get(Long id); + /** + * 根据ID获取数据字典实体 + * + * @param id 数据字典实体的唯一标识 + * @return 返回数据字典实体的详细信息 + */ + SysDictDataVO get(Long id); + /** + * 保存新的数据字典项 + * + * @param dto 数据字典项的保存数据传输对象 + */ void save(SysDictDataDTO dto); + /** + * 更新数据字典项 + * + * @param dto 数据字典项的更新数据传输对象 + */ void update(SysDictDataDTO dto); + /** + * 删除数据字典项 + * + * @param ids 要删除的数据字典项的ID数组 + */ void delete(Long[] ids); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java index a18e086f..697b2d10 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictTypeService.java @@ -6,25 +6,55 @@ import java.util.Map; import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; import xiaozhi.modules.sys.dto.SysDictTypeDTO; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; +import xiaozhi.modules.sys.vo.SysDictTypeVO; /** * 数据字典 */ public interface SysDictTypeService extends BaseService { - PageData page(Map params); + /** + * 分页查询字典类型信息 + * + * @param params 查询参数,包含分页信息和查询条件 + * @return 返回分页的字典类型数据 + */ + PageData page(Map params); - SysDictTypeDTO get(Long id); + /** + * 根据ID获取字典类型信息 + * + * @param id 字典类型ID + * @return 返回字典类型对象 + */ + SysDictTypeVO get(Long id); + /** + * 保存字典类型信息 + * + * @param dto 字典类型数据传输对象 + */ void save(SysDictTypeDTO dto); + /** + * 更新字典类型信息 + * + * @param dto 字典类型数据传输对象 + */ void update(SysDictTypeDTO dto); + /** + * 删除字典类型信息 + * + * @param ids 要删除的字典类型ID数组 + */ void delete(Long[] ids); - List getAllList(); - - List getDictTypeList(); + /** + * 列出所有字典类型信息 + * + * @return 返回字典类型列表 + */ + List list(Map params); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java index 20530e6b..6ca83eea 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java @@ -1,43 +1,58 @@ package xiaozhi.modules.sys.service.impl; import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.AllArgsConstructor; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.sys.dao.SysDictDataDao; +import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.service.SysDictDataService; +import xiaozhi.modules.sys.vo.SysDictDataVO; /** * 字典类型 */ @Service +@AllArgsConstructor public class SysDictDataServiceImpl extends BaseServiceImpl - implements SysDictDataService { + implements SysDictDataService { + private final SysUserDao sysUserDao; @Override - public PageData page(Map params) { - IPage page = baseDao.selectPage( - getPage(params, "sort", true), - getWrapper(params)); + public PageData page(Map params) { + IPage page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params)); - return getPageData(page, SysDictDataDTO.class); + PageData pageData = getPageData(page, SysDictDataVO.class); + + setUserName(pageData.getList()); + + return pageData; } private QueryWrapper getWrapper(Map params) { - String dictTypeId = (String) params.get("dictTypeId"); - String dictLabel = (String) params.get("dictLabel"); - String dictValue = (String) params.get("dictValue"); + String dictTypeId = (String)params.get("dictTypeId"); + String dictLabel = (String)params.get("dictLabel"); + String dictValue = (String)params.get("dictValue"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("dict_type_id", Long.parseLong(dictTypeId)); @@ -48,15 +63,18 @@ public class SysDictDataServiceImpl extends BaseServiceImpl sysDictDataList) { + // 收集所有用户 ID + Set userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) + .filter(Objects::nonNull).collect(Collectors.toSet()); + + // 设置更新者和创建者名称 + if (!userIds.isEmpty()) { + List sysUserEntities = sysUserDao.selectBatchIds(userIds); + // 把List转成Map,Map + Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, + SysUserEntity::getUsername, (existing, replacement) -> existing)); + + sysDictDataList.forEach(vo -> { + vo.setCreatorName(userNameMap.get(vo.getCreator())); + vo.setUpdaterName(userNameMap.get(vo.getUpdater())); + }); + } + } + + private void checkDictValueUnique(Long dictTypeId, String dictValue, String excludeId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId).eq(SysDictDataEntity::getDictLabel, dictValue); + if (StringUtils.isNotBlank(excludeId)) { + queryWrapper.ne(SysDictDataEntity::getId, excludeId); + } + boolean exists = baseDao.exists(queryWrapper); + if (exists) { + throw new RenException("字典标签重复"); + } + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java index ce2e423e..415b7adb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java @@ -3,47 +3,56 @@ package xiaozhi.modules.sys.service.impl; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; -import xiaozhi.modules.sys.dao.SysDictDataDao; import xiaozhi.modules.sys.dao.SysDictTypeDao; +import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dto.SysDictTypeDTO; -import xiaozhi.modules.sys.entity.DictData; -import xiaozhi.modules.sys.entity.DictType; import xiaozhi.modules.sys.entity.SysDictTypeEntity; +import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.service.SysDictTypeService; +import xiaozhi.modules.sys.vo.SysDictTypeVO; /** * 字典类型 */ -@AllArgsConstructor @Service +@AllArgsConstructor public class SysDictTypeServiceImpl extends BaseServiceImpl - implements SysDictTypeService { - private final SysDictDataDao sysDictDataDao; + implements SysDictTypeService { + + private final SysUserDao sysUserDao; @Override - public PageData page(Map params) { - IPage page = baseDao.selectPage( - getPage(params, "sort", true), - getWrapper(params)); + public PageData page(Map params) { + IPage page = baseDao.selectPage(getPage(params, "sort", true), getWrapper(params)); - return getPageData(page, SysDictTypeDTO.class); + PageData pageData = getPageData(page, SysDictTypeVO.class); + + setUserName(pageData.getList()); + + return pageData; } private QueryWrapper getWrapper(Map params) { - String dictType = (String) params.get("dictType"); - String dictName = (String) params.get("dictName"); + String dictType = (String)params.get("dictType"); + String dictName = (String)params.get("dictName"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType); @@ -53,15 +62,21 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl getAllList() { - List typeList = baseDao.getDictTypeList(); - List dataList = sysDictDataDao.getDictDataList(); - for (DictType type : typeList) { - for (DictData data : dataList) { - if (type.getId().equals(data.getDictTypeId())) { - type.getDataList().add(data); - } - } + public List list(Map params) { + List entityList = baseDao.selectList(getWrapper(params)); + List sysDictTypeVOList = ConvertUtils.sourceToTarget(entityList, SysDictTypeVO.class); + // 设置用户名 + setUserName(sysDictTypeVOList); + + return sysDictTypeVOList; + } + + /** + * 设置用户名 + * + * @param sysDictTypeList 字典类型集合 + */ + private void setUserName(List sysDictTypeList) { + // 收集所有用户 ID + Set userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) + .filter(Objects::nonNull).collect(Collectors.toSet()); + + // 设置更新者和创建者名称 + if (!userIds.isEmpty()) { + List sysUserEntities = sysUserDao.selectBatchIds(userIds); + // 把List转成Map,Map + Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, + SysUserEntity::getUsername, (existing, replacement) -> existing)); + + sysDictTypeList.forEach(vo -> { + vo.setCreatorName(userNameMap.get(vo.getCreator())); + vo.setUpdaterName(userNameMap.get(vo.getUpdater())); + }); } - return typeList; } - @Override - public List getDictTypeList() { - return baseDao.getDictTypeList(); + private void checkDictTypeUnique(String dictType, String excludeId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(SysDictTypeEntity::getDictType, dictType); + if (StringUtils.isNotBlank(excludeId)) { + queryWrapper.ne(SysDictTypeEntity::getId, excludeId); + } + boolean exists = baseDao.exists(queryWrapper); + if (exists) { + throw new RenException("字典类型编码重复"); + } } - } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java index 8fd0351a..f4fe601a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserUtilServiceImpl.java @@ -20,7 +20,9 @@ public class SysUserUtilServiceImpl extends BaseServiceImpl setter) { String userIdKey = RedisKeys.getUserIdKey(userId); - String username = redisUtils.get(userIdKey).toString(); + + Object value = redisUtils.get(userIdKey); + String username = (value != null) ? value.toString() : null; if(username != null){ setter.accept(username); }else { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java new file mode 100644 index 00000000..40025b70 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataVO.java @@ -0,0 +1,50 @@ +package xiaozhi.modules.sys.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 字典数据VO + */ +@Data +@Schema(description = "字典数据VO") +public class SysDictDataVO implements Serializable { + @Schema(description = "主键") + private Long id; + + @Schema(description = "字典类型ID") + private Long dictTypeId; + + @Schema(description = "字典标签") + private String dictLabel; + + @Schema(description = "字典值") + private String dictValue; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建者名称") + private String creatorName; + + @Schema(description = "创建时间") + private Date createDate; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新者名称") + private String updaterName; + + @Schema(description = "更新时间") + private Date updateDate; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java new file mode 100644 index 00000000..5e5844f3 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictTypeVO.java @@ -0,0 +1,47 @@ +package xiaozhi.modules.sys.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 字典类型VO + */ +@Data +@Schema(description = "字典类型VO") +public class SysDictTypeVO implements Serializable { + @Schema(description = "主键") + private Long id; + + @Schema(description = "字典类型") + private String dictType; + + @Schema(description = "字典名称") + private String dictName; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "排序") + private Integer sort; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建者名称") + private String creatorName; + + @Schema(description = "创建时间") + private Date createDate; + + @Schema(description = "更新者") + private Long updater; + + @Schema(description = "更新者名称") + private String updaterName; + + @Schema(description = "更新时间") + private Date updateDate; +} diff --git a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml index 7ea5e429..f1044e27 100644 --- a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml +++ b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml @@ -3,11 +3,4 @@ - - - \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml b/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml index 11887552..825db02b 100644 --- a/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml +++ b/main/manager-api/src/main/resources/mapper/sys/SysDictTypeDao.xml @@ -3,8 +3,4 @@ - - \ No newline at end of file From 9b6f894be23ddd5ebcd92ecc91b239cfd23beec3 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 23:33:17 +0800 Subject: [PATCH 23/53] =?UTF-8?q?update:=E6=99=BA=E6=8E=A7=E4=BD=93?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=98=BF=E9=87=8C=E4=BA=91ASR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../resources/db/changelog/202504301339.sql | 3 - .../resources/db/changelog/202504301340.sql | 20 ++++++ .../db/changelog/db.changelog-master.yaml | 4 +- main/xiaozhi-server/config.yaml | 65 +++++++++---------- 5 files changed, 55 insertions(+), 39 deletions(-) delete mode 100644 main/manager-api/src/main/resources/db/changelog/202504301339.sql create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301340.sql diff --git a/README.md b/README.md index a8d54c15..b1bf6dae 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ | 使用方式 | 支持平台 | 免费平台 | |:---:|:---:|:---:| | 本地使用 | FunASR、SherpaASR | FunASR、SherpaASR | -| 接口调用 | DoubaoASR | - | +| 接口调用 | DoubaoASR、FunASRServer、TencentASR、AliyunASR | FunASRServer | --- diff --git a/main/manager-api/src/main/resources/db/changelog/202504301339.sql b/main/manager-api/src/main/resources/db/changelog/202504301339.sql deleted file mode 100644 index e4393f6a..00000000 --- a/main/manager-api/src/main/resources/db/changelog/202504301339.sql +++ /dev/null @@ -1,3 +0,0 @@ -update `ai_model_provider` set `fields` = -'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' -where `id` = 'SYSTEM_TTS_doubao'; diff --git a/main/manager-api/src/main/resources/db/changelog/202504301340.sql b/main/manager-api/src/main/resources/db/changelog/202504301340.sql new file mode 100644 index 00000000..1f0e2f7c --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301340.sql @@ -0,0 +1,20 @@ +update `ai_model_provider` set `fields` = +'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' +where `id` = 'SYSTEM_TTS_doubao'; + +-- 添加阿里云ASR供应器 +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW()); + +-- 添加阿里云ASR模型配置 +INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL); + +-- 更新阿里云ASR模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://nls-portal.console.aliyun.com/', +`remark` = '阿里云ASR配置说明: +1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 +2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey +3. 访问 https://nls-portal.console.aliyun.com/overview 获取token +4. 获取access_key_id和access_key_secret +5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR'; 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 90735fe1..1d965a15 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 @@ -87,9 +87,9 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202504291043.sql - changeSet: - id: 202504301339 + id: 202504301340 author: Goody changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504301339.sql \ No newline at end of file + path: classpath:db/changelog/202504301340.sql \ No newline at end of file diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 568d395d..8b5cca86 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -95,6 +95,35 @@ wakeup_words: - "小滨小滨" - "小冰小冰" +# 插件的基础配置 +plugins: + # 获取天气插件的配置,这里填写你的api_key + # 这个密钥是项目共用的key,用多了可能会被限制 + # 想稳定一点就自行申请替换,每天有1000次免费调用 + # 申请地址:https://console.qweather.com/#/apps/create-key/over + get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } + # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻 + # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ + get_news: + default_rss_url: "https://www.chinanews.com.cn/rss/society.xml" + category_urls: + society: "https://www.chinanews.com.cn/rss/society.xml" + world: "https://www.chinanews.com.cn/rss/world.xml" + finance: "https://www.chinanews.com.cn/rss/finance.xml" + home_assistant: + devices: + - 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1 + - 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1 + base_url: http://homeassistant.local:8123 + api_key: 你的home assistant api访问令牌 + play_music: + music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 + music_ext: # 音乐文件类型,p3格式效率最高 + - ".mp3" + - ".wav" + - ".p3" + refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 + # ##################################################################################### # ################################以下是角色模型配置###################################### @@ -162,36 +191,6 @@ Intent: #- hass_set_state #- hass_play_music -# 插件的基础配置 -plugins: - # 获取天气插件的配置,这里填写你的api_key - # 这个密钥是项目共用的key,用多了可能会被限制 - # 想稳定一点就自行申请替换,每天有1000次免费调用 - # 申请地址:https://console.qweather.com/#/apps/create-key/over - get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" } - # 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻 - # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ - get_news: - default_rss_url: "https://www.chinanews.com.cn/rss/society.xml" - category_urls: - society: "https://www.chinanews.com.cn/rss/society.xml" - world: "https://www.chinanews.com.cn/rss/world.xml" - finance: "https://www.chinanews.com.cn/rss/finance.xml" - home_assistant: - devices: - - 客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1 - - 卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1 - base_url: http://homeassistant.local:8123 - api_key: 你的home assistant api访问令牌 - play_music: - music_dir: "./music" # 音乐文件存放路径,将从该目录及子目录下搜索音乐文件 - music_ext: # 音乐文件类型,p3格式效率最高 - - ".mp3" - - ".wav" - - ".p3" - refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒 - - Memory: mem0ai: type: mem0ai @@ -254,7 +253,7 @@ ASR: access_key_id: 的阿里云账号access_key_id access_key_secret: 你的阿里云账号access_key_secret output_dir: tmp/ - + VAD: SileroVAD: type: silero @@ -271,7 +270,7 @@ LLM: # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key base_url: https://dashscope.aliyuncs.com/compatible-mode/v1 model_name: qwen-turbo - api_key: 你的阿里云大模型 key + api_key: 你的deepseek web key temperature: 0.7 # 温度值 max_tokens: 500 # 最大生成token数 top_p: 1 @@ -657,4 +656,4 @@ TTS: headers: # 自定义请求头 # Authorization: Bearer xxxx format: wav # 接口返回的音频格式 - output_dir: tmp/ + output_dir: tmp/ \ No newline at end of file From 76f47bce37e34ee1010f53a363b6a01aa528b7c3 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 1 May 2025 16:57:58 +0800 Subject: [PATCH 24/53] =?UTF-8?q?update:=E5=AD=97=E5=85=B8=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E5=A2=9E=E5=88=A0=E6=94=B9=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xiaozhi/common/redis/RedisKeys.java | 7 + .../service/impl/ModelConfigServiceImpl.java | 4 - .../sys/controller/SysDictDataController.java | 21 +- .../sys/controller/SysDictTypeController.java | 11 +- .../modules/sys/dao/SysDictDataDao.java | 12 + .../sys/service/SysDictDataService.java | 17 + .../service/impl/SysDictDataServiceImpl.java | 65 +- .../service/impl/SysDictTypeServiceImpl.java | 19 +- .../sys/service/impl/SysUserServiceImpl.java | 1 + .../modules/sys/vo/AdminPageUserVO.java | 5 + .../modules/sys/vo/SysDictDataItem.java | 20 + .../resources/db/changelog/202504301340.sql | 20 - .../resources/db/changelog/202504301341.sql | 85 ++ .../db/changelog/db.changelog-master.yaml | 4 +- .../resources/mapper/sys/SysDictDataDao.xml | 12 + main/manager-web/src/apis/api.js | 7 +- main/manager-web/src/apis/module/dict.js | 227 +++++ .../src/components/DictDataDialog.vue | 105 +++ .../src/components/DictTypeDialog.vue | 86 ++ .../src/components/FirmwareDialog.vue | 10 +- main/manager-web/src/components/HeaderBar.vue | 60 +- main/manager-web/src/router/index.js | 8 +- main/manager-web/src/utils/index.js | 60 -- .../src/views/DeviceManagement.vue | 18 +- main/manager-web/src/views/DictManagement.vue | 835 ++++++++++++++++++ main/manager-web/src/views/OtaManagement.vue | 20 +- main/manager-web/src/views/UserManagement.vue | 12 +- main/manager-web/src/views/roleConfig.vue | 124 +-- 28 files changed, 1653 insertions(+), 222 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java delete mode 100644 main/manager-api/src/main/resources/db/changelog/202504301340.sql create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301341.sql create mode 100644 main/manager-web/src/apis/module/dict.js create mode 100644 main/manager-web/src/components/DictDataDialog.vue create mode 100644 main/manager-web/src/components/DictTypeDialog.vue create mode 100644 main/manager-web/src/views/DictManagement.vue 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 5d3b4f32..7cadad2b 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 @@ -96,4 +96,11 @@ public class RedisKeys { public static String getOtaDownloadCountKey(String uuid) { return "ota:download:count:" + uuid; } + + /** + * 获取字典数据的缓存key + */ + public static String getDictDataByTypeKey(String dictType) { + return "sys:dict:data:" + dictType; + } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index 25b03034..1f006d93 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -21,9 +21,7 @@ import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.agent.dao.AgentDao; -import xiaozhi.modules.agent.dao.AgentTemplateDao; import xiaozhi.modules.agent.entity.AgentEntity; -import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.model.dao.ModelConfigDao; import xiaozhi.modules.model.dto.ModelBasicInfoDTO; import xiaozhi.modules.model.dto.ModelConfigBodyDTO; @@ -41,8 +39,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl> page(@Parameter(hidden = true) @RequestParam Map params) { ValidatorUtils.validateEntity(params); // 强制校验dictTypeId是否存在 @@ -84,7 +85,7 @@ public class SysDictDataController { return new Result<>(); } - @DeleteMapping("/delete") + @PostMapping("/delete") @Operation(summary = "删除字典数据") @RequiresPermissions("sys:role:superAdmin") @Parameter(name = "ids", description = "ID数组", required = true) @@ -93,4 +94,12 @@ public class SysDictDataController { return new Result<>(); } + @GetMapping("/type/{dictType}") + @Operation(summary = "获取字典数据列表") + @RequiresPermissions("sys:role:superAdmin") + public Result> getDictDataByType(@PathVariable("dictType") String dictType) { + List list = sysDictDataService.getDictDataByType(dictType); + return new Result>().ok(list); + } + } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java index bf152c19..f16409b0 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/controller/SysDictTypeController.java @@ -3,7 +3,6 @@ package xiaozhi.modules.sys.controller; import java.util.Map; import org.apache.shiro.authz.annotation.RequiresPermissions; -import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -42,10 +41,10 @@ public class SysDictTypeController { @GetMapping("/page") @Operation(summary = "分页查询字典类型") @RequiresPermissions("sys:role:superAdmin") - @Parameters({@Parameter(name = "dictType", description = "字典类型编码"), - @Parameter(name = "dictName", description = "字典类型名称"), - @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), - @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)}) + @Parameters({ @Parameter(name = "dictType", description = "字典类型编码"), + @Parameter(name = "dictName", description = "字典类型名称"), + @Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true), + @Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true) }) public Result> page(@Parameter(hidden = true) @RequestParam Map params) { ValidatorUtils.validateEntity(params); PageData page = sysDictTypeService.page(params); @@ -82,7 +81,7 @@ public class SysDictTypeController { return new Result<>(); } - @DeleteMapping("/delete") + @PostMapping("/delete") @Operation(summary = "删除字典类型") @RequiresPermissions("sys:role:superAdmin") @Parameter(name = "ids", description = "ID数组", required = true) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java index 3ca5fb8c..e5c4c7bb 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/dao/SysDictDataDao.java @@ -1,9 +1,12 @@ package xiaozhi.modules.sys.dao; +import java.util.List; + import org.apache.ibatis.annotations.Mapper; import xiaozhi.common.dao.BaseDao; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.vo.SysDictDataItem; /** * 字典数据 @@ -11,4 +14,13 @@ import xiaozhi.modules.sys.entity.SysDictDataEntity; @Mapper public interface SysDictDataDao extends BaseDao { + List getDictDataByType(String dictType); + + /** + * 根据字典类型ID获取字典类型编码 + * + * @param dictTypeId 字典类型ID + * @return 字典类型编码 + */ + String getTypeByTypeId(Long dictTypeId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java index 5b4a9317..ea898447 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/SysDictDataService.java @@ -1,11 +1,13 @@ package xiaozhi.modules.sys.service; +import java.util.List; import java.util.Map; import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; +import xiaozhi.modules.sys.vo.SysDictDataItem; import xiaozhi.modules.sys.vo.SysDictDataVO; /** @@ -50,4 +52,19 @@ public interface SysDictDataService extends BaseService { */ void delete(Long[] ids); + /** + * 根据字典类型ID删除对应的字典数据 + * + * @param dictTypeId 字典类型ID + */ + void deleteByTypeId(Long dictTypeId); + + /** + * 根据字典类型获取字典数据列表 + * + * @param dictType 字典类型 + * @return 返回字典数据列表 + */ + List getDictDataByType(String dictType); + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java index 6ca83eea..ffe88986 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictDataServiceImpl.java @@ -1,6 +1,5 @@ package xiaozhi.modules.sys.service.impl; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; @@ -19,6 +18,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; +import xiaozhi.common.redis.RedisKeys; +import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; import xiaozhi.modules.sys.dao.SysDictDataDao; @@ -27,6 +28,7 @@ import xiaozhi.modules.sys.dto.SysDictDataDTO; import xiaozhi.modules.sys.entity.SysDictDataEntity; import xiaozhi.modules.sys.entity.SysUserEntity; import xiaozhi.modules.sys.service.SysDictDataService; +import xiaozhi.modules.sys.vo.SysDictDataItem; import xiaozhi.modules.sys.vo.SysDictDataVO; /** @@ -35,8 +37,9 @@ import xiaozhi.modules.sys.vo.SysDictDataVO; @Service @AllArgsConstructor public class SysDictDataServiceImpl extends BaseServiceImpl - implements SysDictDataService { + implements SysDictDataService { private final SysUserDao sysUserDao; + private final RedisUtils redisUtils; @Override public PageData page(Map params) { @@ -50,9 +53,9 @@ public class SysDictDataServiceImpl extends BaseServiceImpl getWrapper(Map params) { - String dictTypeId = (String)params.get("dictTypeId"); - String dictLabel = (String)params.get("dictLabel"); - String dictValue = (String)params.get("dictValue"); + String dictTypeId = (String) params.get("dictTypeId"); + String dictLabel = (String) params.get("dictLabel"); + String dictValue = (String) params.get("dictValue"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("dict_type_id", Long.parseLong(dictTypeId)); @@ -78,6 +81,9 @@ public class SysDictDataServiceImpl extends BaseServiceImpl wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(SysDictDataEntity::getDictTypeId, dictTypeId); + baseDao.delete(wrapper); } /** @@ -106,14 +129,14 @@ public class SysDictDataServiceImpl extends BaseServiceImpl sysDictDataList) { // 收集所有用户 ID Set userIds = sysDictDataList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) - .filter(Objects::nonNull).collect(Collectors.toSet()); + .filter(Objects::nonNull).collect(Collectors.toSet()); // 设置更新者和创建者名称 if (!userIds.isEmpty()) { List sysUserEntities = sysUserDao.selectBatchIds(userIds); // 把List转成Map,Map Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, - SysUserEntity::getUsername, (existing, replacement) -> existing)); + SysUserEntity::getUsername, (existing, replacement) -> existing)); sysDictDataList.forEach(vo -> { vo.setCreatorName(userNameMap.get(vo.getCreator())); @@ -133,4 +156,28 @@ public class SysDictDataServiceImpl extends BaseServiceImpl getDictDataByType(String dictType) { + if (StringUtils.isBlank(dictType)) { + return null; + } + + // 先从Redis获取缓存 + String key = RedisKeys.getDictDataByTypeKey(dictType); + List cachedData = (List) redisUtils.get(key); + if (cachedData != null) { + return cachedData; + } + + // 如果缓存中没有,则从数据库获取 + List data = baseDao.getDictDataByType(dictType); + + // 存入Redis缓存 + if (data != null) { + redisUtils.set(key, data); + } + + return data; + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java index 415b7adb..91522482 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysDictTypeServiceImpl.java @@ -26,6 +26,7 @@ import xiaozhi.modules.sys.dao.SysUserDao; import xiaozhi.modules.sys.dto.SysDictTypeDTO; import xiaozhi.modules.sys.entity.SysDictTypeEntity; import xiaozhi.modules.sys.entity.SysUserEntity; +import xiaozhi.modules.sys.service.SysDictDataService; import xiaozhi.modules.sys.service.SysDictTypeService; import xiaozhi.modules.sys.vo.SysDictTypeVO; @@ -35,9 +36,9 @@ import xiaozhi.modules.sys.vo.SysDictTypeVO; @Service @AllArgsConstructor public class SysDictTypeServiceImpl extends BaseServiceImpl - implements SysDictTypeService { - + implements SysDictTypeService { private final SysUserDao sysUserDao; + private final SysDictDataService sysDictDataService; @Override public PageData page(Map params) { @@ -51,8 +52,8 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl getWrapper(Map params) { - String dictType = (String)params.get("dictType"); - String dictName = (String)params.get("dictName"); + String dictType = (String) params.get("dictType"); + String dictName = (String) params.get("dictName"); QueryWrapper wrapper = new QueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(dictType), "dict_type", dictType); @@ -96,7 +97,11 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl sysDictTypeList) { // 收集所有用户 ID Set userIds = sysDictTypeList.stream().flatMap(vo -> Stream.of(vo.getCreator(), vo.getUpdater())) - .filter(Objects::nonNull).collect(Collectors.toSet()); + .filter(Objects::nonNull).collect(Collectors.toSet()); // 设置更新者和创建者名称 if (!userIds.isEmpty()) { List sysUserEntities = sysUserDao.selectBatchIds(userIds); // 把List转成Map,Map Map userNameMap = sysUserEntities.stream().collect(Collectors.toMap(SysUserEntity::getId, - SysUserEntity::getUsername, (existing, replacement) -> existing)); + SysUserEntity::getUsername, (existing, replacement) -> existing)); sysDictTypeList.forEach(vo -> { vo.setCreatorName(userNameMap.get(vo.getCreator())); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java index 30976229..97944ba9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/service/impl/SysUserServiceImpl.java @@ -169,6 +169,7 @@ public class SysUserServiceImpl extends BaseServiceImpl(list, page.getTotal()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java index a406848f..9f832372 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/AdminPageUserVO.java @@ -1,5 +1,7 @@ package xiaozhi.modules.sys.vo; +import java.util.Date; + import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -23,4 +25,7 @@ public class AdminPageUserVO { @Schema(description = "用户id") private String userid; + + @Schema(description = "注册时间") + private Date createDate; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java new file mode 100644 index 00000000..f6c81500 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/sys/vo/SysDictDataItem.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.sys.vo; + +import java.io.Serializable; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 字典数据VO + */ +@Data +@Schema(description = "字典数据项") +public class SysDictDataItem implements Serializable { + + @Schema(description = "字典标签") + private String name; + + @Schema(description = "字典值") + private String key; +} diff --git a/main/manager-api/src/main/resources/db/changelog/202504301340.sql b/main/manager-api/src/main/resources/db/changelog/202504301340.sql deleted file mode 100644 index 1f0e2f7c..00000000 --- a/main/manager-api/src/main/resources/db/changelog/202504301340.sql +++ /dev/null @@ -1,20 +0,0 @@ -update `ai_model_provider` set `fields` = -'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' -where `id` = 'SYSTEM_TTS_doubao'; - --- 添加阿里云ASR供应器 -INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES -('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW()); - --- 添加阿里云ASR模型配置 -INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL); - --- 更新阿里云ASR模型配置的说明文档 -UPDATE `ai_model_config` SET -`doc_link` = 'https://nls-portal.console.aliyun.com/', -`remark` = '阿里云ASR配置说明: -1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 -2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey -3. 访问 https://nls-portal.console.aliyun.com/overview 获取token -4. 获取access_key_id和access_key_secret -5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR'; diff --git a/main/manager-api/src/main/resources/db/changelog/202504301341.sql b/main/manager-api/src/main/resources/db/changelog/202504301341.sql new file mode 100644 index 00000000..a88ee783 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301341.sql @@ -0,0 +1,85 @@ +update `ai_model_provider` set `fields` = +'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' +where `id` = 'SYSTEM_TTS_doubao'; + +-- 添加阿里云ASR供应器 +delete from `ai_model_provider` where `id` = 'SYSTEM_ASR_AliyunASR'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_AliyunASR', 'ASR', 'aliyun', '阿里云语音识别', '[{"key":"appkey","label":"应用AppKey","type":"string"},{"key":"token","label":"临时Token","type":"string"},{"key":"access_key_id","label":"AccessKey ID","type":"string"},{"key":"access_key_secret","label":"AccessKey Secret","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 5, 1, NOW(), 1, NOW()); + +-- 添加阿里云ASR模型配置 +delete from `ai_model_config` where `id` = 'ASR_AliyunASR'; +INSERT INTO `ai_model_config` VALUES ('ASR_AliyunASR', 'ASR', 'AliyunASR', '阿里云语音识别', 0, 1, '{\"type\": \"aliyun\", \"appkey\": \"\", \"token\": \"\", \"access_key_id\": \"\", \"access_key_secret\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL); + +-- 更新阿里云ASR模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://nls-portal.console.aliyun.com/', +`remark` = '阿里云ASR配置说明: +1. 访问 https://nls-portal.console.aliyun.com/ 开通服务 +2. 访问 https://nls-portal.console.aliyun.com/applist 获取appkey +3. 访问 https://nls-portal.console.aliyun.com/overview 获取token +4. 获取access_key_id和access_key_secret +5. 填入配置文件中' WHERE `id` = 'ASR_AliyunASR'; + +-- 插入固件类型字典类型 +delete from `sys_dict_type` where `id` = 101; +INSERT INTO `sys_dict_type` (`id`, `dict_type`, `dict_name`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(101, 'FIRMWARE_TYPE', '固件类型', '固件类型字典', 0, 1, NOW(), 1, NOW()); + +-- 插入固件类型字典数据 +delete from `sys_dict_data` where `dict_type_id` = 101; +INSERT INTO `sys_dict_data` (`id`, `dict_type_id`, `dict_label`, `dict_value`, `remark`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +(101001, 101, '面包板新版接线(WiFi)', 'bread-compact-wifi', '面包板新版接线(WiFi)', 1, 1, NOW(), 1, NOW()), +(101002, 101, '面包板新版接线(WiFi)+ LCD', 'bread-compact-wifi-lcd', '面包板新版接线(WiFi)+ LCD', 2, 1, NOW(), 1, NOW()), +(101003, 101, '面包板新版接线(ML307 AT)', 'bread-compact-ml307', '面包板新版接线(ML307 AT)', 3, 1, NOW(), 1, NOW()), +(101004, 101, '面包板(WiFi) ESP32 DevKit', 'bread-compact-esp32', '面包板(WiFi) ESP32 DevKit', 4, 1, NOW(), 1, NOW()), +(101005, 101, '面包板(WiFi+ LCD) ESP32 DevKit', 'bread-compact-esp32-lcd', '面包板(WiFi+ LCD) ESP32 DevKit', 5, 1, NOW(), 1, NOW()), +(101006, 101, 'DFRobot 行空板 k10', 'df-k10', 'DFRobot 行空板 k10', 6, 1, NOW(), 1, NOW()), +(101007, 101, 'ESP32 CGC', 'esp32-cgc', 'ESP32 CGC', 7, 1, NOW(), 1, NOW()), +(101008, 101, 'ESP BOX 3', 'esp-box-3', 'ESP BOX 3', 8, 1, NOW(), 1, NOW()), +(101009, 101, 'ESP BOX', 'esp-box', 'ESP BOX', 9, 1, NOW(), 1, NOW()), +(101010, 101, 'ESP BOX Lite', 'esp-box-lite', 'ESP BOX Lite', 10, 1, NOW(), 1, NOW()), +(101011, 101, 'Kevin Box 1', 'kevin-box-1', 'Kevin Box 1', 11, 1, NOW(), 1, NOW()), +(101012, 101, 'Kevin Box 2', 'kevin-box-2', 'Kevin Box 2', 12, 1, NOW(), 1, NOW()), +(101013, 101, 'Kevin C3', 'kevin-c3', 'Kevin C3', 13, 1, NOW(), 1, NOW()), +(101014, 101, 'Kevin SP V3开发板', 'kevin-sp-v3-dev', 'Kevin SP V3开发板', 14, 1, NOW(), 1, NOW()), +(101015, 101, 'Kevin SP V4开发板', 'kevin-sp-v4-dev', 'Kevin SP V4开发板', 15, 1, NOW(), 1, NOW()), +(101016, 101, '鱼鹰科技3.13LCD开发板', 'kevin-yuying-313lcd', '鱼鹰科技3.13LCD开发板', 16, 1, NOW(), 1, NOW()), +(101017, 101, '立创·实战派ESP32-S3开发板', 'lichuang-dev', '立创·实战派ESP32-S3开发板', 17, 1, NOW(), 1, NOW()), +(101018, 101, '立创·实战派ESP32-C3开发板', 'lichuang-c3-dev', '立创·实战派ESP32-C3开发板', 18, 1, NOW(), 1, NOW()), +(101019, 101, '神奇按钮 Magiclick_2.4', 'magiclick-2p4', '神奇按钮 Magiclick_2.4', 19, 1, NOW(), 1, NOW()), +(101020, 101, '神奇按钮 Magiclick_2.5', 'magiclick-2p5', '神奇按钮 Magiclick_2.5', 20, 1, NOW(), 1, NOW()), +(101021, 101, '神奇按钮 Magiclick_C3', 'magiclick-c3', '神奇按钮 Magiclick_C3', 21, 1, NOW(), 1, NOW()), +(101022, 101, '神奇按钮 Magiclick_C3_v2', 'magiclick-c3-v2', '神奇按钮 Magiclick_C3_v2', 22, 1, NOW(), 1, NOW()), +(101023, 101, 'M5Stack CoreS3', 'm5stack-core-s3', 'M5Stack CoreS3', 23, 1, NOW(), 1, NOW()), +(101024, 101, 'AtomS3 + Echo Base', 'atoms3-echo-base', 'AtomS3 + Echo Base', 24, 1, NOW(), 1, NOW()), +(101025, 101, 'AtomS3R + Echo Base', 'atoms3r-echo-base', 'AtomS3R + Echo Base', 25, 1, NOW(), 1, NOW()), +(101026, 101, 'AtomS3R CAM/M12 + Echo Base', 'atoms3r-cam-m12-echo-base', 'AtomS3R CAM/M12 + Echo Base', 26, 1, NOW(), 1, NOW()), +(101027, 101, 'AtomMatrix + Echo Base', 'atommatrix-echo-base', 'AtomMatrix + Echo Base', 27, 1, NOW(), 1, NOW()), +(101028, 101, '虾哥 Mini C3', 'xmini-c3', '虾哥 Mini C3', 28, 1, NOW(), 1, NOW()), +(101029, 101, 'ESP32S3_KORVO2_V3开发板', 'esp32s3-korvo2-v3', 'ESP32S3_KORVO2_V3开发板', 29, 1, NOW(), 1, NOW()), +(101030, 101, 'ESP-SparkBot开发板', 'esp-sparkbot', 'ESP-SparkBot开发板', 30, 1, NOW(), 1, NOW()), +(101031, 101, 'ESP-Spot-S3', 'esp-spot-s3', 'ESP-Spot-S3', 31, 1, NOW(), 1, NOW()), +(101032, 101, 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 'esp32-s3-touch-amoled-1.8', 'Waveshare ESP32-S3-Touch-AMOLED-1.8', 32, 1, NOW(), 1, NOW()), +(101033, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85C', 'esp32-s3-touch-lcd-1.85c', 'Waveshare ESP32-S3-Touch-LCD-1.85C', 33, 1, NOW(), 1, NOW()), +(101034, 101, 'Waveshare ESP32-S3-Touch-LCD-1.85', 'esp32-s3-touch-lcd-1.85', 'Waveshare ESP32-S3-Touch-LCD-1.85', 34, 1, NOW(), 1, NOW()), +(101035, 101, 'Waveshare ESP32-S3-Touch-LCD-1.46', 'esp32-s3-touch-lcd-1.46', 'Waveshare ESP32-S3-Touch-LCD-1.46', 35, 1, NOW(), 1, NOW()), +(101036, 101, 'Waveshare ESP32-S3-Touch-LCD-3.5', 'esp32-s3-touch-lcd-3.5', 'Waveshare ESP32-S3-Touch-LCD-3.5', 36, 1, NOW(), 1, NOW()), +(101037, 101, '土豆子', 'tudouzi', '土豆子', 37, 1, NOW(), 1, NOW()), +(101038, 101, 'LILYGO T-Circle-S3', 'lilygo-t-circle-s3', 'LILYGO T-Circle-S3', 38, 1, NOW(), 1, NOW()), +(101039, 101, 'LILYGO T-CameraPlus-S3', 'lilygo-t-cameraplus-s3', 'LILYGO T-CameraPlus-S3', 39, 1, NOW(), 1, NOW()), +(101040, 101, 'Movecall Moji 小智AI衍生版', 'movecall-moji-esp32s3', 'Movecall Moji 小智AI衍生版', 40, 1, NOW(), 1, NOW()), +(101041, 101, 'Movecall CuiCan 璀璨·AI吊坠', 'movecall-cuican-esp32s3', 'Movecall CuiCan 璀璨·AI吊坠', 41, 1, NOW(), 1, NOW()), +(101042, 101, '正点原子DNESP32S3开发板', 'atk-dnesp32s3', '正点原子DNESP32S3开发板', 42, 1, NOW(), 1, NOW()), +(101043, 101, '正点原子DNESP32S3-BOX', 'atk-dnesp32s3-box', '正点原子DNESP32S3-BOX', 43, 1, NOW(), 1, NOW()), +(101044, 101, '嘟嘟开发板CHATX(wifi)', 'du-chatx', '嘟嘟开发板CHATX(wifi)', 44, 1, NOW(), 1, NOW()), +(101045, 101, '太极小派esp32s3', 'taiji-pi-s3', '太极小派esp32s3', 45, 1, NOW(), 1, NOW()), +(101046, 101, '无名科技星智0.85(WIFI)', 'xingzhi-cube-0.85tft-wifi', '无名科技星智0.85(WIFI)', 46, 1, NOW(), 1, NOW()), +(101047, 101, '无名科技星智0.85(ML307)', 'xingzhi-cube-0.85tft-ml307', '无名科技星智0.85(ML307)', 47, 1, NOW(), 1, NOW()), +(101048, 101, '无名科技星智0.96(WIFI)', 'xingzhi-cube-0.96oled-wifi', '无名科技星智0.96(WIFI)', 48, 1, NOW(), 1, NOW()), +(101049, 101, '无名科技星智0.96(ML307)', 'xingzhi-cube-0.96oled-ml307', '无名科技星智0.96(ML307)', 49, 1, NOW(), 1, NOW()), +(101050, 101, '无名科技星智1.54(WIFI)', 'xingzhi-cube-1.54tft-wifi', '无名科技星智1.54(WIFI)', 50, 1, NOW(), 1, NOW()), +(101051, 101, '无名科技星智1.54(ML307)', 'xingzhi-cube-1.54tft-ml307', '无名科技星智1.54(ML307)', 51, 1, NOW(), 1, NOW()), +(101052, 101, 'SenseCAP Watcher', 'sensecap-watcher', 'SenseCAP Watcher', 52, 1, NOW(), 1, NOW()), +(101053, 101, '四博智联AI陪伴盒子', 'doit-s3-aibox', '四博智联AI陪伴盒子', 53, 1, NOW(), 1, NOW()), +(101054, 101, '元控·青春', 'mixgo-nova', '元控·青春', 54, 1, NOW(), 1, NOW()); 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 1d965a15..ffe96a9b 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 @@ -87,9 +87,9 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202504291043.sql - changeSet: - id: 202504301340 + id: 202504301341 author: Goody changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504301340.sql \ No newline at end of file + path: classpath:db/changelog/202504301341.sql \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml index f1044e27..39848831 100644 --- a/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml +++ b/main/manager-api/src/main/resources/mapper/sys/SysDictDataDao.xml @@ -2,5 +2,17 @@ + + \ No newline at end of file diff --git a/main/manager-web/src/apis/api.js b/main/manager-web/src/apis/api.js index 1b9f0a9e..0a3e8d18 100755 --- a/main/manager-web/src/apis/api.js +++ b/main/manager-web/src/apis/api.js @@ -2,11 +2,11 @@ import admin from './module/admin.js' import agent from './module/agent.js' import device from './module/device.js' +import dict from './module/dict.js' import model from './module/model.js' +import ota from './module/ota.js' import timbre from "./module/timbre.js" import user from './module/user.js' -import ota from './module/ota.js' - /** * 接口地址 * 开发时自动读取使用.env.development文件 @@ -32,5 +32,6 @@ export default { device, model, timbre, - ota + ota, + dict } diff --git a/main/manager-web/src/apis/module/dict.js b/main/manager-web/src/apis/module/dict.js new file mode 100644 index 00000000..caf78279 --- /dev/null +++ b/main/manager-web/src/apis/module/dict.js @@ -0,0 +1,227 @@ +import { getServiceUrl } from '../api'; +import RequestService from '../httpRequest'; + +export default { + // 获取字典类型列表 + getDictTypeList(params, callback) { + const queryParams = new URLSearchParams({ + dictType: params.dictType || '', + dictName: params.dictName || '', + page: params.page || 1, + limit: params.limit || 10 + }).toString(); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/page?${queryParams}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典类型列表失败:', err) + this.$message.error(err.msg || '获取字典类型列表失败') + RequestService.reAjaxFun(() => { + this.getDictTypeList(params, callback) + }) + }).send() + }, + + // 获取字典类型详情 + getDictTypeDetail(id, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/${id}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典类型详情失败:', err) + this.$message.error(err.msg || '获取字典类型详情失败') + RequestService.reAjaxFun(() => { + this.getDictTypeDetail(id, callback) + }) + }).send() + }, + + // 新增字典类型 + addDictType(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/save`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('新增字典类型失败:', err) + this.$message.error(err.msg || '新增字典类型失败') + RequestService.reAjaxFun(() => { + this.addDictType(data, callback) + }) + }).send() + }, + + // 更新字典类型 + updateDictType(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/update`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('更新字典类型失败:', err) + this.$message.error(err.msg || '更新字典类型失败') + RequestService.reAjaxFun(() => { + this.updateDictType(data, callback) + }) + }).send() + }, + + // 删除字典类型 + deleteDictType(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/type/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('删除字典类型失败:', err) + this.$message.error(err.msg || '删除字典类型失败') + RequestService.reAjaxFun(() => { + this.deleteDictType(ids, callback) + }) + }).send() + }, + + // 获取字典数据列表 + getDictDataList(params, callback) { + const queryParams = new URLSearchParams({ + dictTypeId: params.dictTypeId, + dictLabel: params.dictLabel || '', + dictValue: params.dictValue || '', + page: params.page || 1, + limit: params.limit || 10 + }).toString(); + + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/page?${queryParams}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典数据列表失败:', err) + this.$message.error(err.msg || '获取字典数据列表失败') + RequestService.reAjaxFun(() => { + this.getDictDataList(params, callback) + }) + }).send() + }, + + // 获取字典数据详情 + getDictDataDetail(id, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/${id}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('获取字典数据详情失败:', err) + this.$message.error(err.msg || '获取字典数据详情失败') + RequestService.reAjaxFun(() => { + this.getDictDataDetail(id, callback) + }) + }).send() + }, + + // 新增字典数据 + addDictData(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/save`) + .method('POST') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('新增字典数据失败:', err) + this.$message.error(err.msg || '新增字典数据失败') + RequestService.reAjaxFun(() => { + this.addDictData(data, callback) + }) + }).send() + }, + + // 更新字典数据 + updateDictData(data, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/update`) + .method('PUT') + .data(data) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('更新字典数据失败:', err) + this.$message.error(err.msg || '更新字典数据失败') + RequestService.reAjaxFun(() => { + this.updateDictData(data, callback) + }) + }).send() + }, + + // 删除字典数据 + deleteDictData(ids, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/delete`) + .method('POST') + .data(ids) + .success((res) => { + RequestService.clearRequestTime() + callback(res) + }) + .fail((err) => { + console.error('删除字典数据失败:', err) + this.$message.error(err.msg || '删除字典数据失败') + RequestService.reAjaxFun(() => { + this.deleteDictData(ids, callback) + }) + }).send() + }, + + // 获取字典数据列表 + getDictDataByType(dictType) { + return new Promise((resolve, reject) => { + RequestService.sendRequest() + .url(`${getServiceUrl()}/admin/dict/data/type/${dictType}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime() + if (res.data && res.data.code === 0) { + resolve(res.data) + } else { + reject(new Error(res.data?.msg || '获取字典数据列表失败')) + } + }) + .fail((err) => { + console.error('获取字典数据列表失败:', err) + reject(err) + }).send() + }) + } + +} \ No newline at end of file diff --git a/main/manager-web/src/components/DictDataDialog.vue b/main/manager-web/src/components/DictDataDialog.vue new file mode 100644 index 00000000..7934602b --- /dev/null +++ b/main/manager-web/src/components/DictDataDialog.vue @@ -0,0 +1,105 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/DictTypeDialog.vue b/main/manager-web/src/components/DictTypeDialog.vue new file mode 100644 index 00000000..a9a7aa07 --- /dev/null +++ b/main/manager-web/src/components/DictTypeDialog.vue @@ -0,0 +1,86 @@ + + + + + \ No newline at end of file diff --git a/main/manager-web/src/components/FirmwareDialog.vue b/main/manager-web/src/components/FirmwareDialog.vue index fab99a57..940fbbeb 100644 --- a/main/manager-web/src/components/FirmwareDialog.vue +++ b/main/manager-web/src/components/FirmwareDialog.vue @@ -38,7 +38,6 @@ + + \ No newline at end of file diff --git a/main/manager-web/src/views/OtaManagement.vue b/main/manager-web/src/views/OtaManagement.vue index 106e12fb..300e83ad 100644 --- a/main/manager-web/src/views/OtaManagement.vue +++ b/main/manager-web/src/views/OtaManagement.vue @@ -96,8 +96,8 @@ - + @@ -109,7 +109,6 @@ import Api from "@/apis/api"; import FirmwareDialog from "@/components/FirmwareDialog.vue"; import HeaderBar from "@/components/HeaderBar.vue"; import VersionFooter from "@/components/VersionFooter.vue"; -import { FIRMWARE_TYPES } from "@/utils"; import { formatDate, formatFileSize } from "@/utils/format"; export default { @@ -136,10 +135,12 @@ export default { remark: "", firmwarePath: "" }, + firmwareTypes: [], }; }, created() { this.fetchFirmwareList(); + this.getFirmwareTypes(); }, computed: { @@ -387,9 +388,18 @@ export default { }, formatDate, formatFileSize, + async getFirmwareTypes() { + try { + const res = await Api.dict.getDictDataByType('FIRMWARE_TYPE') + this.firmwareTypes = res.data + } catch (error) { + console.error('获取固件类型失败:', error) + this.$message.error(error.message || '获取固件类型失败') + } + }, getFirmwareTypeName(type) { - const firmwareType = FIRMWARE_TYPES.find(item => item.key === type); - return firmwareType ? firmwareType.name : type; + const firmwareType = this.firmwareTypes.find(item => item.key === type) + return firmwareType ? firmwareType.name : type }, }, }; diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index eb43a959..87ce3747 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -26,6 +26,11 @@ + + +