add banbanmini backend

This commit is contained in:
HycJack
2026-03-24 15:04:36 +08:00
parent 0d0f995dc2
commit 7510ca6df1
197 changed files with 13008 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
from fastapi import APIRouter
from api.websocket import router as websocket_router
from api.roles import router as roles_router
from api.auth import router as auth_router
from api.device_control import router as device_control_router
from api.ota import router as ota_router # 新增OTA路由
api_router = APIRouter()
api_router.include_router(websocket_router, tags=["WebSocket"])
api_router.include_router(roles_router, tags=["Roles"])
api_router.include_router(auth_router, tags=["Auth"])
api_router.include_router(device_control_router, tags=["Device Control"])
api_router.include_router(ota_router, tags=["OTA"]) # 注册OTA路由

View File

@@ -0,0 +1,16 @@
import os
from fastapi import APIRouter
from fastapi.staticfiles import StaticFiles
from config import settings
router = APIRouter()
def configure_static_assets(app):
"""配置静态资源文件夹"""
tts_audio_directory = os.path.join(settings.assets_dir, "tts_audio")
os.makedirs(tts_audio_directory, exist_ok=True)
firmware_directory = os.path.join(settings.assets_dir, "firmware")
os.makedirs(firmware_directory, exist_ok=True)
app.mount("/assets", StaticFiles(directory=settings.assets_dir), name="assets")

250
talkingq-url/api/auth.py Normal file
View File

@@ -0,0 +1,250 @@
from fastapi import Depends, HTTPException, status, Request, APIRouter, Path, Query
from fastapi.security import APIKeyHeader
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any, Tuple
from services.device_auth_manager import device_auth_manager
from utils.logger import session_logger
from config import settings
import re
router = APIRouter(prefix="/api/auth", tags=["Auth"])
device_id_header = APIKeyHeader(name="X-Device-ID", auto_error=False)
device_serial_header = APIKeyHeader(name="X-Device-Serial", auto_error=False)
admin_api_key_header = APIKeyHeader(name="X-Admin-API-Key", auto_error=False)
client_api_key_header = APIKeyHeader(name="X-Client-Key", auto_error=False)
async def verify_device(
device_id: str = Depends(device_id_header),
serial_number: str = Depends(device_serial_header)
):
"""验证设备ID和序列号"""
if not device_id or not serial_number:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少设备认证信息",
headers={"WWW-Authenticate": "DeviceAuth"},
)
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
if not is_valid:
session_logger.warning("system", "auth", f"设备 {device_id} 认证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="设备认证失败",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return device_id
async def api_auth(
device_id: str = Depends(verify_device)
):
"""API认证依赖项只使用设备ID和序列号认证"""
return device_id
async def admin_auth(api_key: str = Depends(admin_api_key_header)) -> bool:
"""验证管理员API密钥"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少管理员API密钥",
headers={"WWW-Authenticate": "AdminAuth"},
)
return api_key == settings.admin_api_key
async def client_auth(client_api_key: str = Depends(client_api_key_header)) -> bool:
"""验证小程序客户端API密钥"""
if not client_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少客户端API密钥",
headers={"WWW-Authenticate": "ClientAuth"},
)
if client_api_key != settings.client_api_key:
session_logger.warning("system", "auth", "客户端API密钥验证失败")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="客户端API密钥无效",
headers={"WWW-Authenticate": "ClientAuth"},
)
return True
async def admin_or_api_auth(
device_id: str = Path(...),
admin_api_key: str = Depends(admin_api_key_header),
authenticated_device_id: str = Depends(api_auth)
) -> Tuple[str, bool]:
"""允许管理员或设备本身访问,返回(认证设备ID, 是否管理员)"""
is_admin = admin_api_key == settings.admin_api_key if admin_api_key else False
if not is_admin and authenticated_device_id != device_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="没有权限查询其他设备",
headers={"WWW-Authenticate": "DeviceAuth"},
)
return authenticated_device_id, is_admin
class DeviceRegistrationRequest(BaseModel):
device_id: str = Field(..., min_length=16, max_length=64, description="设备ID基于ESP32的MAC地址生成")
serial_number: str = Field(..., min_length=18, max_length=64, description="序列号,包含批次前缀和唯一码")
batch_id: Optional[str] = Field(None, description="批次ID使用YYYYMMDD格式")
is_active: bool = Field(True, description="设备激活状态")
class DeviceRegistrationResponse(BaseModel):
status: str
device_id: str
serial_number: str
batch_id: Optional[str] = None
is_active: bool
@router.post("/register-device", response_model=DeviceRegistrationResponse)
async def register_device(
request: DeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""注册新设备到认证白名单"""
batch_id = request.batch_id
if not batch_id and len(request.serial_number) >= 8:
batch_id = request.serial_number[:8]
success = await device_auth_manager.register_device(
request.device_id,
request.serial_number,
batch_id,
request.is_active
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="设备注册失败",
)
return {
"status": "success",
"device_id": request.device_id,
"serial_number": request.serial_number,
"batch_id": batch_id,
"is_active": request.is_active
}
class BatchDeviceRegistrationRequest(BaseModel):
devices: List[DeviceRegistrationRequest]
class BatchDeviceRegistrationResponse(BaseModel):
status: str
registered_count: int
failed_count: int
details: List[Dict[str, Any]]
@router.post("/register-devices-batch", response_model=BatchDeviceRegistrationResponse)
async def register_devices_batch(
request: BatchDeviceRegistrationRequest,
admin_authenticated: bool = Depends(admin_auth)
):
"""批量注册设备到认证白名单"""
results = []
success_count = 0
failed_count = 0
for device in request.devices:
batch_id = device.batch_id
if not batch_id and len(device.serial_number) >= 8:
batch_id = device.serial_number[:8]
success = await device_auth_manager.register_device(
device.device_id,
device.serial_number,
batch_id,
device.is_active
)
if success:
success_count += 1
results.append({
"status": "success",
"device_id": device.device_id,
"serial_number": device.serial_number,
"batch_id": batch_id
})
else:
failed_count += 1
results.append({
"status": "failed",
"device_id": device.device_id,
"serial_number": device.serial_number
})
return {
"status": "completed",
"registered_count": success_count,
"failed_count": failed_count,
"details": results
}
@router.get("/verify-device/{device_id}")
async def verify_device(
device_id: str = Path(...),
serial_number: str = None,
auth_result: Tuple[str, bool] = Depends(admin_or_api_auth)
):
"""验证设备是否已正确注册 (管理员或设备自身可使用)"""
authenticated_device_id, is_admin = auth_result
if is_admin and serial_number:
is_valid = await device_auth_manager.authenticate_device(device_id, serial_number)
elif not is_admin:
is_valid = True
else:
is_valid = None
device_info = await device_auth_manager.get_device_info(device_id)
return {
"device_id": device_id,
"is_valid": is_valid,
"device_info": device_info
}
class SerialQueryResponse(BaseModel):
device_id: str
serial_number: str
@router.get("/query-serial", response_model=SerialQueryResponse)
async def query_serial_by_mac(
mac_address: str = Query(..., description="设备MAC地址"),
client_authenticated: bool = Depends(client_auth)
):
"""通过MAC地址查询设备ID和序列号"""
mac_pattern = re.compile(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$')
if not mac_pattern.match(mac_address):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="MAC地址格式无效应为XX:XX:XX:XX:XX:XX或XX-XX-XX-XX-XX-XX格式"
)
normalized_mac = mac_address.replace(":", "").replace("-", "").upper()
expected_device_id = f"TalkingQ_{normalized_mac}"
device_info = await device_auth_manager.get_device_info(expected_device_id)
if not device_info or not device_info.get("is_active", False):
session_logger.warning("system", "auth", f"未找到MAC地址{mac_address}对应的设备或设备未激活")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="未找到对应的设备信息或设备未激活"
)
return {
"device_id": expected_device_id,
"serial_number": device_info["serial_number"]
}
@router.get("/verify")
async def verify_auth(device_id: str = Depends(api_auth)):
"""验证设备认证状态"""
return {"status": "authenticated", "device_id": device_id}

View File

@@ -0,0 +1,97 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Body
from pydantic import BaseModel, Field
from typing import Optional
from api.auth import api_auth
from services.connection_manager import connection_manager
from services.device_volume_manager import device_volume_manager
from utils.logger import session_logger
router = APIRouter(prefix="/api/device", tags=["Device Control"])
class VolumeRequest(BaseModel):
volume: int = Field(..., ge=0, le=100, description="音量值(0-100)")
class DeviceResponse(BaseModel):
status: str
message: str
device_id: str
class VolumeResponse(BaseModel):
volume: int
device_id: str
@router.post("/volume/{device_id}", response_model=DeviceResponse)
async def set_device_volume(
device_id: str = Path(..., description="设备ID"),
volume_request: VolumeRequest = Body(...),
authenticated_device_id: str = Depends(api_auth)
):
"""设置设备音量"""
try:
await device_volume_manager.set_volume(device_id, volume_request.volume)
session_logger.info(device_id, "volume", f"设备音量设置为 {volume_request.volume}")
websocket = await connection_manager.get_connection(device_id)
if websocket:
try:
await websocket.send_text(f"VOLUME:{volume_request.volume}")
session_logger.info(device_id, "volume", f"已发送实时音量设置到设备")
except Exception as e:
session_logger.warning(device_id, "volume", f"发送音量通知失败,但数据库已更新: {str(e)}")
else:
session_logger.info(device_id, "volume", "设备当前不在线,仅保存设置到数据库")
return {
"status": "success",
"message": f"Volume set to {volume_request.volume}",
"device_id": device_id
}
except Exception as e:
session_logger.error(device_id, "volume", f"设置音量失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"设置音量失败: {str(e)}")
@router.get("/volume/{device_id}", response_model=VolumeResponse)
async def get_device_volume(
device_id: str = Path(..., description="设备ID"),
authenticated_device_id: str = Depends(api_auth)
):
"""获取设备当前音量设置"""
try:
volume = await device_volume_manager.get_volume(device_id)
return {
"volume": volume,
"device_id": device_id
}
except Exception as e:
session_logger.error(device_id, "volume", f"获取音量失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"获取音量失败: {str(e)}")
class NetworkResetResponse(BaseModel):
status: str
message: str
device_id: str
@router.post("/reset-network/{device_id}", response_model=NetworkResetResponse)
async def reset_device_network(
device_id: str = Path(..., description="设备ID"),
authenticated_device_id: str = Depends(api_auth)
):
"""重置设备网络配置"""
websocket = await connection_manager.get_connection(device_id)
if not websocket:
raise HTTPException(status_code=404, detail=f"设备 {device_id} 未在线或未找到")
try:
await websocket.send_text("RESET_NETWORK")
session_logger.info(device_id, "network", "已发送网络重置命令")
return {
"status": "success",
"message": "Network reset initiated",
"device_id": device_id
}
except Exception as e:
session_logger.error(device_id, "network", f"网络重置命令发送失败: {str(e)}")
raise HTTPException(status_code=500, detail=f"网络重置命令发送失败: {str(e)}")

103
talkingq-url/api/ota.py Normal file
View File

@@ -0,0 +1,103 @@
from fastapi import APIRouter, HTTPException, Depends, Path, Query
from pydantic import BaseModel
from typing import Dict, Any, Optional
from api.auth import api_auth, client_auth
from api.response import Response
from services.ota_flow_controller import ota_flow_controller
from utils.logger import session_logger
from services.connection_manager import connection_manager
from services.system_config_manager import system_config_manager
router = APIRouter(prefix="/api/ota", tags=["OTA"])
class OTAStatusResponse(BaseModel):
status: str
progress: float
version: str
class FirmwareUpdateSettings(BaseModel):
version: str
url: str
@router.get("/check/{device_id}", response_model=Response)
async def check_update(
device_id: str = Path(..., description="设备ID"),
authenticated: bool = Depends(api_auth)
):
"""检查设备是否需要更新 (APP调用)"""
try:
result = await ota_flow_controller.start_update_flow(device_id)
if result["status"] == "error":
return Response(code=-1, msg=result["message"], data={})
return Response(code=0, msg="success", data=result)
except Exception as e:
session_logger.error(device_id, "ota", f"检查更新失败: {str(e)}")
return Response(code=-1, msg=f"检查更新失败: {str(e)}", data={})
@router.post("/start/{device_id}", response_model=Response)
async def start_update(
device_id: str = Path(..., description="设备ID"),
authenticated: bool = Depends(api_auth)
):
"""启动设备更新 (APP调用)"""
try:
result = await ota_flow_controller.execute_update(device_id)
if result["status"] == "error":
return Response(code=-1, msg=result["message"], data={})
return Response(code=0, msg="success", data={"updating": True})
except Exception as e:
session_logger.error(device_id, "ota", f"启动更新失败: {str(e)}")
return Response(code=-1, msg=f"启动更新失败: {str(e)}", data={"updating": False})
@router.get("/status/{device_id}", response_model=Response)
async def get_update_status(
device_id: str = Path(..., description="设备ID"),
authenticated: bool = Depends(api_auth)
):
"""获取设备更新状态 (APP调用)"""
try:
status = await ota_flow_controller.get_update_status(device_id)
websocket = await connection_manager.get_connection(device_id)
status["device_online"] = websocket is not None
return Response(code=0, msg="success", data=status)
except Exception as e:
session_logger.error(device_id, "ota", f"获取更新状态失败: {str(e)}")
return Response(code=-1, msg=f"获取更新状态失败: {str(e)}", data={
"status": "error",
"progress": 0,
"version": "unknown",
"device_online": False
})
@router.post("/config/firmware", response_model=Response)
async def update_firmware_settings(
settings: FirmwareUpdateSettings,
admin_api_key: str = Depends(api_auth)
):
"""更新固件配置信息版本和URL"""
try:
await system_config_manager.update_config('latest_firmware_version', settings.version)
await system_config_manager.update_config('update_firmware_url', settings.url)
return Response(code=0, msg="固件配置已更新", data={
"version": settings.version,
"url": settings.url
})
except Exception as e:
session_logger.error("system", "ota", f"更新固件配置失败: {str(e)}")
return Response(code=-1, msg=f"更新固件配置失败: {str(e)}", data={})
@router.get("/config/firmware", response_model=Response)
async def get_firmware_settings(
admin_api_key: str = Depends(api_auth)
):
"""获取当前固件配置信息"""
try:
version_config = await system_config_manager.get_config('latest_firmware_version')
url_config = await system_config_manager.get_config('update_firmware_url')
return Response(code=0, msg="success", data={
"version": version_config.config_value if version_config else "",
"url": url_config.config_value if url_config else ""
})
except Exception as e:
session_logger.error("system", "ota", f"获取固件配置失败: {str(e)}")
return Response(code=-1, msg=f"获取固件配置失败: {str(e)}", data={})

View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel, Field
class Response(BaseModel):
code: int
msg: str
data: object = None

127
talkingq-url/api/roles.py Normal file
View File

@@ -0,0 +1,127 @@
from fastapi import APIRouter, HTTPException, Query, Depends
from typing import Dict, List, Optional
from pydantic import BaseModel
from services.role_service import role_service
from services.device_service import device_service
from services.history_service import history_service
from api.auth import api_auth
router = APIRouter(prefix="/api/roles")
class RoleResponse(BaseModel):
role_key: str
name: str
description: Optional[str] = None
languages: Optional[List[str]] = None
class RoleSummaryResponse(BaseModel):
role_key: str
name: str
class DeviceRoleUpdate(BaseModel):
role_key: str
language: Optional[str] = None
class ConversationMessage(BaseModel):
user: str
assistant: str
timestamp: Optional[float] = None
class PaginatedRoleResponse(BaseModel):
total: int
page: int
page_size: int
data: List[RoleResponse]
class PaginatedHistoryResponse(BaseModel):
total: int
page: int
page_size: int
data: List[ConversationMessage]
role_key: str
role_name: str
@router.get("/list", response_model=PaginatedRoleResponse)
async def list_roles(
authenticated_device_id: str = Depends(api_auth),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
search: Optional[str] = None
):
"""获取角色分页列表,支持搜索"""
return await role_service.get_roles_paginated(page, page_size, search)
@router.get("/summaries", response_model=List[RoleSummaryResponse])
async def get_role_summaries(
authenticated_device_id: str = Depends(api_auth)
):
"""获取角色简要信息列表仅包含key和name"""
return await role_service.get_role_summaries()
@router.get("/device/{device_id}")
async def get_device_role(
device_id: str,
authenticated_device_id: str = Depends(api_auth)
):
"""获取设备当前使用的角色"""
if device_id != authenticated_device_id:
raise HTTPException(status_code=403, detail="无权访问其他设备的配置")
return await device_service.get_device_role(device_id)
@router.put("/device/{device_id}")
async def update_device_role(
device_id: str,
role_update: DeviceRoleUpdate,
authenticated_device_id: str = Depends(api_auth)
):
"""更新设备的角色配置"""
if device_id != authenticated_device_id:
raise HTTPException(status_code=403, detail="无权修改其他设备的配置")
try:
return await device_service.update_device_role(
device_id,
role_update.role_key,
role_update.language
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"更新角色时出错: {str(e)}")
@router.get("/history/{device_id}", response_model=PaginatedHistoryResponse)
async def get_conversation_history(
device_id: str,
authenticated_device_id: str = Depends(api_auth),
role_key: Optional[str] = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
since: Optional[float] = None,
until: Optional[float] = None
):
"""获取设备的对话历史记录,支持分页和时间范围筛选"""
if device_id != authenticated_device_id:
raise HTTPException(status_code=403, detail="无权访问其他设备的对话历史")
try:
return await history_service.get_history_paginated(
device_id, role_key, page, page_size, since, until
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取对话历史失败: {str(e)}")
@router.get("/history-summary/{device_id}")
async def get_conversation_history_summary(
device_id: str,
authenticated_device_id: str = Depends(api_auth),
days: int = Query(7, ge=1, le=30)
):
"""获取设备的对话历史摘要信息,包括每个角色的最后交互时间和消息数量"""
if device_id != authenticated_device_id:
raise HTTPException(status_code=403, detail="无权访问其他设备的对话历史摘要")
try:
return await history_service.get_history_summary(device_id, days)
except Exception as e:
raise HTTPException(status_code=500, detail=f"获取对话历史摘要失败: {str(e)}")

View File

@@ -0,0 +1,67 @@
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import Optional, List
from pydantic import BaseModel
from services.system_config_manager import system_config_manager
from api.auth import api_auth
router = APIRouter(prefix="/api/system-config")
class SystemConfigResponse(BaseModel):
key: str
value: str
created_at: Optional[float] = None
updated_at: Optional[float] = None
class SystemConfigCreate(BaseModel):
key: str
value: str
class SystemConfigUpdate(BaseModel):
value: str
@router.get("/", response_model=List[SystemConfigResponse])
async def list_configs(authenticated_device_id: str = Depends(api_auth)):
"""列出所有系统配置"""
configs = await system_config_manager.list_configs()
return [SystemConfigResponse(**c.__dict__) for c in configs]
@router.get("/{key}", response_model=SystemConfigResponse)
async def get_config(key: str, authenticated_device_id: str = Depends(api_auth)):
"""获取指定key的系统配置"""
config = await system_config_manager.get_config(key)
if not config:
raise HTTPException(status_code=404, detail="配置不存在")
return SystemConfigResponse(**config.__dict__)
@router.post("/", response_model=SystemConfigResponse)
async def create_config(
config: SystemConfigCreate,
authenticated_device_id: str = Depends(api_auth)
):
"""新增系统配置"""
success = await system_config_manager.create_config(config.key, config.value)
if not success:
raise HTTPException(status_code=400, detail="创建配置失败")
new_config = await system_config_manager.get_config(config.key)
return SystemConfigResponse(**new_config.__dict__)
@router.put("/{key}", response_model=SystemConfigResponse)
async def update_config(
key: str,
config: SystemConfigUpdate,
authenticated_device_id: str = Depends(api_auth)
):
"""更新系统配置"""
success = await system_config_manager.update_config(key, config.value)
if not success:
raise HTTPException(status_code=400, detail="更新配置失败")
updated_config = await system_config_manager.get_config(key)
return SystemConfigResponse(**updated_config.__dict__)
@router.delete("/{key}")
async def delete_config(key: str, authenticated_device_id: str = Depends(api_auth)):
"""删除系统配置"""
success = await system_config_manager.delete_config(key)
if not success:
raise HTTPException(status_code=400, detail="删除配置失败")
return {"detail": "删除成功"}

View File

@@ -0,0 +1,9 @@
from fastapi import APIRouter, WebSocket
from handlers.websocket_handler import websocket_endpoint
router = APIRouter()
@router.websocket("/ws")
async def websocket_route(websocket: WebSocket):
await websocket_endpoint(websocket)