mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 08:03:53 +08:00
增加通过认证码绑定设备和管理账户的功能,需要开启私有设备配置
增加登录用户鉴权
This commit is contained in:
+59
-14
@@ -17,6 +17,7 @@ from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.audioHandle import handleAudioMessage, sendAudioMessage
|
||||
from config.private_config import PrivateConfig
|
||||
from core.auth import AuthMiddleware, AuthenticationError
|
||||
from core.utils.auth_code_gen import AuthCodeGenerator # 添加导入
|
||||
|
||||
class ConnectionHandler:
|
||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
|
||||
@@ -76,6 +77,9 @@ class ConnectionHandler:
|
||||
self.max_cmd_length = len(cmd)
|
||||
|
||||
self.private_config = None
|
||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
||||
self.is_device_verified = False # 添加设备验证状态标志
|
||||
|
||||
|
||||
async def handle_connection(self, ws):
|
||||
try:
|
||||
@@ -92,21 +96,30 @@ class ConnectionHandler:
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
logging.info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
||||
if bUsePrivateConfig and device_id:
|
||||
self.private_config = PrivateConfig(device_id, self.config)
|
||||
await self.private_config.load_or_create()
|
||||
# Create private instances using private config
|
||||
vad, asr, llm, tts = self.private_config.create_private_instances()
|
||||
if vad is not None and asr is not None and llm is not None and tts is not None:
|
||||
self.vad = vad
|
||||
self.asr = asr
|
||||
self.llm = llm
|
||||
self.tts = tts
|
||||
|
||||
self.logger.info(f"Loaded private config and instances for device {device_id}")
|
||||
self.private_config.update_last_chat_time()
|
||||
else:
|
||||
self.logger.error(f"Failed to load private config for device {device_id}")
|
||||
try:
|
||||
self.private_config = PrivateConfig(device_id, self.config, self.auth_code_gen)
|
||||
await self.private_config.load_or_create()
|
||||
# 判断是否已经绑定
|
||||
owner = self.private_config.get_owner()
|
||||
self.is_device_verified = owner is not None
|
||||
|
||||
if self.is_device_verified:
|
||||
await self.private_config.update_last_chat_time()
|
||||
|
||||
vad, asr, llm, tts = self.private_config.create_private_instances()
|
||||
if all([vad, asr, llm, tts]):
|
||||
self.vad = vad
|
||||
self.asr = asr
|
||||
self.llm = llm
|
||||
self.tts = tts
|
||||
self.logger.info(f"Loaded private config and instances for device {device_id}")
|
||||
else:
|
||||
self.logger.error(f"Failed to create instances for device {device_id}")
|
||||
self.private_config = None
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error initializing private config: {e}")
|
||||
self.private_config = None
|
||||
raise
|
||||
|
||||
# 认证通过,继续处理
|
||||
self.websocket = ws
|
||||
@@ -153,8 +166,40 @@ class ConnectionHandler:
|
||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||
self.prompt = self.prompt.replace("{date_time}", date_time)
|
||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||
|
||||
async def _check_and_broadcast_auth_code(self):
|
||||
"""检查设备绑定状态并广播认证码"""
|
||||
if not self.private_config.get_owner():
|
||||
auth_code = self.private_config.get_auth_code()
|
||||
if auth_code:
|
||||
# 发送验证码语音提示
|
||||
text = f"请在后台输入验证码:{' '.join(auth_code)}"
|
||||
self.recode_first_last_text(text)
|
||||
future = self.executor.submit(self.speak_and_play, text)
|
||||
self.tts_queue.put(future)
|
||||
return False
|
||||
return True
|
||||
|
||||
def isNeedAuth(self):
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
if not bUsePrivateConfig:
|
||||
# 如果不使用私有配置,就不需要验证
|
||||
return False
|
||||
return not self.is_device_verified
|
||||
|
||||
def chat(self, query):
|
||||
# 如果设备未验证,就发送验证码
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
# 创建一个新的事件循环来运行异步函数
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(self._check_and_broadcast_auth_code())
|
||||
finally:
|
||||
loop.close()
|
||||
return True
|
||||
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
response_message = []
|
||||
start = 0
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from typing import Set
|
||||
|
||||
class AuthCodeGenerator:
|
||||
_instance = None
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
if not cls._instance:
|
||||
with cls._instance_lock:
|
||||
if not cls._instance:
|
||||
cls._instance = super(AuthCodeGenerator, cls).__new__(cls)
|
||||
# 初始化随机种子
|
||||
random.seed(time.time())
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
# 确保 __init__ 只被调用一次
|
||||
if not hasattr(self, '_initialized'):
|
||||
self._used_codes: Set[str] = set()
|
||||
self._code_timestamps = {}
|
||||
self._lock = threading.Lock()
|
||||
self._code_timeout = 3 * 24 * 60 * 60
|
||||
self._initialized = True
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""获取AuthCodeGenerator的单例实例"""
|
||||
return cls()
|
||||
|
||||
def generate_code(self) -> str:
|
||||
"""
|
||||
生成6位数字认证码,确保不重复
|
||||
返回: 6位数字字符串
|
||||
"""
|
||||
with self._lock:
|
||||
self._clean_expired_codes() # 清理过期code
|
||||
while True:
|
||||
# 使用时间戳和已用码数量作为种子,确保每次生成不同的随机数
|
||||
seed = int(time.time() * 1000) + len(self._used_codes)
|
||||
random.seed(seed)
|
||||
|
||||
# 生成6位随机数字
|
||||
code = ''.join(str(random.randint(0, 9)) for _ in range(6))
|
||||
|
||||
# 检查是否已存在
|
||||
if code not in self._used_codes:
|
||||
self._used_codes.add(code)
|
||||
self._code_timestamps[code] = time.time()
|
||||
return code
|
||||
|
||||
def remove_code(self, code: str) -> bool:
|
||||
"""
|
||||
删除已使用的认证码
|
||||
参数:
|
||||
code: 要删除的认证码
|
||||
返回:
|
||||
bool: 删除成功返回True,码不存在返回False
|
||||
"""
|
||||
print('remove_code', code)
|
||||
with self._lock:
|
||||
if code in self._used_codes:
|
||||
self._used_codes.remove(code)
|
||||
if code in self._code_timestamps:
|
||||
del self._code_timestamps[code]
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_code_used(self, code: str) -> bool:
|
||||
"""
|
||||
检查认证码是否已被使用
|
||||
参数:
|
||||
code: 要检查的认证码
|
||||
返回:
|
||||
bool: 如果码存在返回True,否则返回False
|
||||
"""
|
||||
with self._lock:
|
||||
return code in self._used_codes
|
||||
|
||||
def clear_codes(self):
|
||||
"""清空所有已使用的认证码"""
|
||||
with self._lock:
|
||||
self._used_codes.clear()
|
||||
self._code_timestamps.clear()
|
||||
|
||||
def _clean_expired_codes(self):
|
||||
"""清理过期的认证码"""
|
||||
current_time = time.time()
|
||||
expired_codes = [
|
||||
code for code, timestamp in self._code_timestamps.items()
|
||||
if (current_time - timestamp) > self._code_timeout
|
||||
]
|
||||
for code in expired_codes:
|
||||
self._used_codes.remove(code)
|
||||
del self._code_timestamps[code]
|
||||
@@ -0,0 +1,38 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class FileLockManager:
|
||||
_instance = None
|
||||
_locks: Dict[str, asyncio.Lock] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(FileLockManager, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_lock(cls, file_path: str) -> asyncio.Lock:
|
||||
"""获取指定文件的锁"""
|
||||
if file_path not in cls._locks:
|
||||
cls._locks[file_path] = asyncio.Lock()
|
||||
return cls._locks[file_path]
|
||||
|
||||
@classmethod
|
||||
async def acquire_lock(cls, file_path: str):
|
||||
"""获取锁"""
|
||||
lock = cls.get_lock(file_path)
|
||||
await lock.acquire()
|
||||
logger.debug(f"Acquired lock for {file_path}")
|
||||
|
||||
@classmethod
|
||||
def release_lock(cls, file_path: str):
|
||||
"""释放锁"""
|
||||
if file_path in cls._locks:
|
||||
try:
|
||||
cls._locks[file_path].release()
|
||||
logger.debug(f"Released lock for {file_path}")
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"Failed to release lock for {file_path}: {e}")
|
||||
Reference in New Issue
Block a user