Merge pull request #41 from Eric0308/main

增加通过认证码绑定管理账户和设备的功能,需要开启私有配置
This commit is contained in:
TOM88812
2025-02-17 18:46:45 +08:00
committed by GitHub
16 changed files with 1112 additions and 309 deletions
+1 -1
View File
@@ -1 +1 @@
VITE_API_BASE_URL=''
VITE_API_BASE_URL='http://127.0.0.1:8002'
+13 -1
View File
@@ -16,7 +16,7 @@
</div>
<div class="device-actions">
<button class="action-btn primary" @click="$emit('configure')">配置角色</button>
<button class="action-btn primary" @click="handleConfigure">配置角色</button>
<button class="action-btn" @click="$emit('voiceprint')">声纹识别</button>
<button class="action-btn" @click="$emit('history')">历史对话</button>
<div class="delete-container">
@@ -81,6 +81,18 @@ const handleDelete = () => {
emit('delete');
}
};
const handleConfigure = () => {
// 保存设备配置到 localStorage,确保 RoleSetting 可以访问
if (props.deviceId && props.deviceConfig) {
localStorage.setItem(`deviceConfig_${props.deviceId}`, JSON.stringify({
selected_module: props.selectedModules,
prompt: props.deviceConfig.prompt || '',
nickname: props.deviceConfig.nickname || '小智'
}));
}
emit('configure');
};
</script>
<style scoped>
+6 -14
View File
@@ -19,7 +19,7 @@
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { API_BASE_URL } from '../config/api';
import apiClient from '../utils/api';
const router = useRouter();
const username = ref('');
@@ -34,24 +34,16 @@ const handleLogin = async () => {
isLoading.value = true;
try {
const response = await fetch(`${API_BASE_URL}/api/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: username.value,
password: password.value
})
const response = await apiClient.post('/api/login', {
username: username.value,
password: password.value
});
const data = await response.json();
const data = response.data;
if (data.success) {
// 存储token和登录状态
localStorage.setItem('token', data.token);
localStorage.setItem('session_id', data.session_id);
localStorage.setItem('isLoggedIn', 'true');
// 使用路由导航到panel页面
router.push('/panel');
} else {
alert(data.message || '登录失败');
+37 -34
View File
@@ -110,7 +110,7 @@ import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import NavBar from './NavBar.vue';
import RoleTemplates from '../utils/RoleTemplates';
import { API_BASE_URL } from '../config/api';
import apiClient from '../utils/api'; // 替换 API_BASE_URL 导入
const router = useRouter();
const route = useRoute();
@@ -125,7 +125,6 @@ const activeMemoryTab = ref('recent');
const memoryContent = ref('');
const selectedModel = ref('qianwen');
const baseUrl = API_BASE_URL;
const moduleOptions = ref({
LLM: [],
TTS: [],
@@ -170,15 +169,15 @@ const loadModuleOptions = async () => {
const refreshModuleOptions = async () => {
try {
const response = await fetch(`${baseUrl}/api/config/module-options`);
const data = await response.json();
if (data.success) {
moduleOptions.value = data.data;
const response = await apiClient.get('/api/config/module-options');
if (response.data.success) {
moduleOptions.value = response.data.data;
// Update cache
localStorage.setItem('moduleOptions', JSON.stringify(data.data));
localStorage.setItem('moduleOptions', JSON.stringify(response.data.data));
}
} catch (error) {
console.error('Error refreshing module options:', error);
alert(error.response?.data?.message || '刷新配置选项失败');
}
};
@@ -208,16 +207,9 @@ const saveConfig = async () => {
}
};
const response = await fetch(`${baseUrl}/api/config/save_device_config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const response = await apiClient.post('/api/config/save_device_config', config);
const data = await response.json();
if (data.success) {
if (response.data.success) {
// Save the original description and nickname to local storage
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: selectedModules.value,
@@ -226,46 +218,57 @@ const saveConfig = async () => {
}));
alert('保存成功');
} else {
throw new Error(data.message || '保存失败');
throw new Error(response.data.message || '保存失败');
}
} catch (error) {
console.error('Error saving config:', error);
alert(error.message || '保存失败');
alert(error.response?.data?.message || '保存失败');
}
};
const loadDeviceConfig = async () => {
try {
// First try to get config from local storage
// 首先尝试从 localStorage 获取配置
const localConfig = localStorage.getItem(`deviceConfig_${deviceId.value}`);
if (localConfig) {
const config = JSON.parse(localConfig);
selectedModules.value = config.selected_module || {};
selectedModules.value = config.selected_module || {
LLM: '',
TTS: '',
VAD: '',
ASR: ''
};
roleDescription.value = config.prompt || '';
nickname.value = config.nickname || '小智'; // Load saved nickname
return;
nickname.value = config.nickname || '小智';
return; // 如果找到本地配置就直接返回
}
// If no local config, fetch from server
const response = await fetch(`${baseUrl}/api/config/devices`);
const data = await response.json();
if (data.success) {
const deviceConfig = data.data.find(d => d.id === deviceId.value);
if (deviceConfig) {
selectedModules.value = deviceConfig.config.selected_module || {};
// 如果没有本地配置,则从服务器获取
const response = await apiClient.get('/api/config/devices');
if (response.data.success && response.data.data) {
const deviceConfig = response.data.data[deviceId.value];
if (deviceConfig && deviceConfig.config) {
selectedModules.value = deviceConfig.config.selected_module || {
LLM: '',
TTS: '',
VAD: '',
ASR: ''
};
roleDescription.value = deviceConfig.config.prompt || '';
nickname.value = deviceConfig.config.nickname || '小智'; // Load nickname from server
nickname.value = deviceConfig.config.nickname || '小智';
// Save to local storage
// 保存到 localStorage
localStorage.setItem(`deviceConfig_${deviceId.value}`, JSON.stringify({
selected_module: deviceConfig.config.selected_module,
prompt: deviceConfig.config.prompt,
nickname: deviceConfig.config.nickname
selected_module: selectedModules.value,
prompt: roleDescription.value,
nickname: nickname.value
}));
}
}
} catch (error) {
console.error('Error loading device config:', error);
alert(error.response?.data?.message || '加载设备配置失败');
}
};
+232 -40
View File
@@ -2,11 +2,46 @@
<div class="app">
<NavBar current-tab="device" @tab-change="handleTabChange"/>
<main class="content">
<div class="breadcrumb">
<router-link to="/">首页</router-link> /
<span>设备管理</span>
<div class="page-header">
<div class="header-left">
<div class="breadcrumb">
<router-link to="/">首页</router-link> /
<span>设备管理</span>
</div>
</div>
</div>
<button class="add-btn" @click="showBindDialog = true">
<i class="icon-plus"></i>添加设备
</button>
<!-- 绑定设备弹窗 -->
<div v-if="showBindDialog" class="dialog-overlay">
<div class="dialog">
<h3>绑定新设备</h3>
<div class="form-group">
<label>请输入6位认证码</label>
<input
type="text"
v-model="authCode"
maxlength="6"
pattern="\d*"
placeholder="请输入6位数字认证码"
@input="handleAuthCodeInput"
/>
</div>
<div class="dialog-buttons">
<button @click="showBindDialog = false">取消</button>
<button
class="primary"
@click="handleBindDevice"
:disabled="authCode.length !== 6 || isBinding"
>
{{ isBinding ? '绑定中...' : '确认绑定' }}
</button>
</div>
</div>
</div>
<template v-if="devices.length > 0">
<div class="device-list">
<DeviceCard
@@ -25,14 +60,7 @@
/>
</div>
</template>
<template v-else>
<div class="empty-state">
<div class="empty-message">
<i class="icon-info"></i>
<p>目前没有设备请确认是否启用私有配置并且和设备进行一次对话</p>
</div>
</div>
</template>
</main>
</div>
</template>
@@ -42,12 +70,78 @@ import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import NavBar from './NavBar.vue';
import DeviceCard from './DeviceCard.vue';
import { API_BASE_URL } from '../config/api';
import apiClient from '../utils/api';
const router = useRouter();
const baseUrl = API_BASE_URL;
const devices = ref([]);
// 绑定设备相关的状态
const showBindDialog = ref(false);
const authCode = ref('');
const isBinding = ref(false);
// 处理认证码输入,只允许数字
const handleAuthCodeInput = (event) => {
authCode.value = event.target.value.replace(/\D/g, '').slice(0, 6);
};
// 处理设备绑定
const handleBindDevice = async () => {
if (authCode.value.length !== 6) {
alert('请输入6位数字认证码');
return;
}
isBinding.value = true;
try {
const response = await apiClient.post('/api/config/bind_device', {
auth_code: authCode.value
});
if (response.data.success) {
alert('设备绑定成功');
showBindDialog.value = false;
authCode.value = '';
// 刷新设备列表
loadDevices();
} else {
throw new Error(response.data.message);
}
} catch (error) {
alert(error.response?.data?.message || error.message || '绑定失败');
} finally {
isBinding.value = false;
}
};
// 将现有的加载设备方法提取出来
const loadDevices = async () => {
try {
const response = await apiClient.get('/api/config/devices');
if (response.data.success) {
const deviceArray = Object.entries(response.data.data).map(([id, config]) => ({
id,
config,
type: '面包板(WiFi',
version: '0.9.9',
lastActivity: '3 天前',
note: ''
}));
devices.value = deviceArray;
} else {
throw new Error(response.data.message || '加载设备失败');
}
} catch (error) {
console.error('Error loading devices:', error);
// Show error message to user
const errorMessage = error.message || '加载设备失败,请检查网络连接';
alert(errorMessage);
// If user is not logged in, redirect will be handled by api interceptor
}
};
const formatLastActivity = (timestamp) => {
if (!timestamp) return '从未对话';
@@ -69,6 +163,14 @@ const formatLastActivity = (timestamp) => {
};
const handleRoleConfig = (device) => {
// 在跳转前保存完整的设备配置到 localStorage
localStorage.setItem(`deviceConfig_${device.id}`, JSON.stringify({
selected_module: device.config.selected_module || {},
prompt: device.config.prompt || '',
nickname: device.config.nickname || '小智'
}));
// 跳转到角色配置页面
router.push(`/role-setting/${device.id}`);
};
@@ -82,20 +184,15 @@ const handleHistory = (device) => {
const handleDelete = async (device) => {
try {
const response = await fetch(`${baseUrl}/api/config/delete_device`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ device_id: device.id })
const response = await apiClient.post('/api/config/delete_device', {
device_id: device.id
});
const data = await response.json();
if (data.success) {
if (response.data.success) {
devices.value = devices.value.filter(d => d.id !== device.id);
alert('设备已删除');
} else {
throw new Error(data.message || '删除失败');
throw new Error(response.data.message || '删除失败');
}
} catch (error) {
console.error('Error deleting device:', error);
@@ -110,23 +207,7 @@ const handleTabChange = (tab) => {
};
// Load devices on mount
onMounted(async () => {
try {
const response = await fetch(`${baseUrl}/api/config/devices`);
const data = await response.json();
if (data.success) {
devices.value = data.data.map(device => ({
...device,
type: '面包板(WiFi',
version: '0.9.9',
lastActivity: '3 天前',
note: ''
}));
}
} catch (error) {
console.error('Error loading devices:', error);
}
});
onMounted(loadDevices);
</script>
<style scoped>
@@ -305,4 +386,115 @@ onMounted(async () => {
background-size: contain;
opacity: 0.6;
}
.page-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 20px;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.add-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
height: 36px;
}
.add-btn:hover {
background-color: #218838;
}
.icon-plus {
font-size: 16px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: white;
padding: 24px;
border-radius: 8px;
width: 90%;
max-width: 400px;
}
.dialog h3 {
margin: 0 0 20px;
font-size: 18px;
color: #2c3e50;
}
.dialog .form-group {
margin-bottom: 20px;
}
.dialog label {
display: block;
margin-bottom: 8px;
color: #4a5568;
}
.dialog input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-size: 16px;
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.dialog-buttons button {
padding: 8px 16px;
border: 1px solid #e2e8f0;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.dialog-buttons button.primary {
background-color: #28a745;
color: white;
border-color: #28a745;
}
.dialog-buttons button.primary:disabled {
background-color: #90be9c;
border-color: #90be9c;
cursor: not-allowed;
}
.breadcrumb {
margin-bottom: 0;
}
</style>
+61
View File
@@ -0,0 +1,61 @@
import axios from 'axios';
import { API_BASE_URL } from '../config/api';
// Add server status check utility
export const checkServerStatus = async () => {
try {
await axios.get(`${API_BASE_URL}/health`, { timeout: 5000 });
return true;
} catch (error) {
return false;
}
};
const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
timeout: 10000 // Add timeout
});
// 添加请求拦截器,自动添加 session_id
apiClient.interceptors.request.use(config => {
const sessionId = localStorage.getItem('session_id');
if (sessionId) {
config.headers.Authorization = sessionId;
}
return config;
});
// 响应拦截器
apiClient.interceptors.response.use(
response => response,
error => {
// Network error or server not reachable
if (!error.response || error.code === 'ERR_NETWORK' || error.code === 'ECONNABORTED') {
localStorage.removeItem('session_id');
localStorage.removeItem('isLoggedIn');
const errorMessage = error.code === 'ECONNABORTED'
? '服务器响应超时'
: '无法连接到服务器,请检查服务器是否正常运行';
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
return Promise.reject(new Error(errorMessage));
}
// Unauthorized error
if (error.response && error.response.status === 401) {
localStorage.removeItem('session_id');
localStorage.removeItem('isLoggedIn');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default apiClient;
+229 -100
View File
@@ -2,60 +2,76 @@ import os
import time
import yaml
import logging
from typing import Dict, Any
from typing import Dict, Any, Optional
from copy import deepcopy
from core.utils.util import get_project_dir
from core.utils import asr, vad, llm, tts
from manager.api.user_manager import UserManager
from core.utils.lock_manager import FileLockManager
class PrivateConfig:
def __init__(self, device_id: str, default_config: Dict[str, Any]):
def __init__(self, device_id: str, default_config: Dict[str, Any], auth_code_gen=None):
self.device_id = device_id
self.default_config = default_config
self.config_path = get_project_dir() + 'data/.private_config.yaml'
self.logger = logging.getLogger(__name__)
self.private_config = {}
self.auth_code_gen = auth_code_gen
self.user_manager = UserManager()
self.lock_manager = FileLockManager()
async def load_or_create(self):
try:
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
await self.lock_manager.acquire_lock(self.config_path)
try:
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
if self.device_id not in all_configs:
# Get selected module names
selected_modules = self.default_config['selected_module']
selected_tts = selected_modules['TTS']
selected_llm = selected_modules['LLM']
selected_asr = selected_modules['ASR']
selected_vad = selected_modules['VAD']
if self.device_id not in all_configs:
# Get selected module names
selected_modules = self.default_config['selected_module']
selected_tts = selected_modules['TTS']
selected_llm = selected_modules['LLM']
selected_asr = selected_modules['ASR']
selected_vad = selected_modules['VAD']
# Initialize device config with only necessary configurations
device_config = {
'selected_module': deepcopy(selected_modules),
'prompt': self.default_config['prompt'],
'LLM': {
selected_llm: deepcopy(self.default_config['LLM'][selected_llm])
},
'TTS': {
selected_tts: deepcopy(self.default_config['TTS'][selected_tts])
},
'ASR': {
selected_asr: deepcopy(self.default_config['ASR'][selected_asr])
},
'VAD': {
selected_vad: deepcopy(self.default_config['VAD'][selected_vad])
# 生成认证码
auth_code = None
if self.auth_code_gen:
auth_code = self.auth_code_gen.generate_code()
# Initialize device config with only necessary configurations
device_config = {
'selected_module': deepcopy(selected_modules),
'prompt': self.default_config['prompt'],
'LLM': {
selected_llm: deepcopy(self.default_config['LLM'][selected_llm])
},
'TTS': {
selected_tts: deepcopy(self.default_config['TTS'][selected_tts])
},
'ASR': {
selected_asr: deepcopy(self.default_config['ASR'][selected_asr])
},
'VAD': {
selected_vad: deepcopy(self.default_config['VAD'][selected_vad])
},
'auth_code': auth_code # 添加认证码字段
}
}
all_configs[self.device_id] = device_config
# Save updated configs
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
all_configs[self.device_id] = device_config
# Save updated configs
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = all_configs[self.device_id]
self.private_config = all_configs[self.device_id]
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.error(f"Error handling private config: {e}")
@@ -70,41 +86,47 @@ class PrivateConfig:
bool: 更新是否成功
"""
try:
# Read main config to get full module configurations
main_config = self.default_config
await self.lock_manager.acquire_lock(self.config_path)
try:
# Read main config to get full module configurations
main_config = self.default_config
# Create new device config
device_config = {
'selected_module': selected_modules,
'prompt': prompt,
'nickname': nickname,
}
if self.private_config.get('last_chat_time'):
device_config['last_chat_time'] = self.private_config['last_chat_time']
# Create new device config
device_config = {
'selected_module': selected_modules,
'prompt': prompt,
'nickname': nickname,
}
if self.private_config.get('last_chat_time'):
device_config['last_chat_time'] = self.private_config['last_chat_time']
if self.private_config.get('owner'):
device_config['owner'] = self.private_config['owner']
# Copy full module configurations from main config
for module_type, selected_name in selected_modules.items():
if selected_name and selected_name in main_config.get(module_type, {}):
device_config[module_type] = {
selected_name: main_config[module_type][selected_name]
}
# Copy full module configurations from main config
for module_type, selected_name in selected_modules.items():
if selected_name and selected_name in main_config.get(module_type, {}):
device_config[module_type] = {
selected_name: main_config[module_type][selected_name]
}
# Read all configs
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
# Read all configs
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
# Update device config
all_configs[self.device_id] = device_config
self.private_config = device_config
# Update device config
all_configs[self.device_id] = device_config
self.private_config = device_config
# Save back to file
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
# Save back to file
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
return True
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.error(f"Error updating config: {e}")
@@ -116,25 +138,29 @@ class PrivateConfig:
bool: 删除是否成功
"""
try:
# 读取所有配置
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
return False
await self.lock_manager.acquire_lock(self.config_path)
try:
# 读取所有配置
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
return False
# 删除设备配置
if self.device_id in all_configs:
del all_configs[self.device_id]
# 删除设备配置
if self.device_id in all_configs:
del all_configs[self.device_id]
# 保存更新后的配置
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = {}
return True
# 保存更新后的配置
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = {}
return True
return False
return False
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.error(f"Error deleting config: {e}")
@@ -176,7 +202,7 @@ class PrivateConfig:
)
)
def update_last_chat_time(self, timestamp=None):
async def update_last_chat_time(self, timestamp=None):
"""更新设备最近一次的聊天时间
Args:
timestamp: 指定的时间戳,不传则使用当前时间
@@ -186,24 +212,127 @@ class PrivateConfig:
return False
try:
if timestamp is None:
timestamp = int(time.time())
await self.lock_manager.acquire_lock(self.config_path)
try:
if timestamp is None:
timestamp = int(time.time())
self.private_config['last_chat_time'] = timestamp
self.private_config['last_chat_time'] = timestamp
# 读取所有配置
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
# 读取所有配置
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
# 更新当前设备配置
all_configs[self.device_id] = self.private_config
# 更新当前设备配置
all_configs[self.device_id] = self.private_config
# 保存回文件
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
# 保存回文件
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
finally:
self.lock_manager.release_lock(self.config_path)
return True
except Exception as e:
self.logger.error(f"Error updating last chat time: {e}")
return False
return False
def get_auth_code(self) -> str:
"""获取设备的认证码
Returns:
str: 认证码,如果没有返回空字符串
"""
return self.private_config.get('auth_code', '')
async def bind_user(self, username: str) -> bool:
"""绑定用户到设备"""
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
# 检查用户是否存在
if not self.user_manager.get_user(username):
self.logger.error(f"User {username} not found")
return False
# 读取所有配置
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
if self.device_id not in all_configs:
self.logger.error(f"Device {self.device_id} not found")
return False
# 删除认证码
auth_code = all_configs[self.device_id].get('auth_code')
self.logger.info(f"Binding user {username} to device {self.device_id}")
if auth_code:
del all_configs[self.device_id]['auth_code']
if self.auth_code_gen:
self.auth_code_gen.remove_code(auth_code)
# 更新设备所有者
all_configs[self.device_id]['owner'] = username
self.private_config = all_configs[self.device_id]
# 更新用户的设备列表
user_data = await self.user_manager.get_user(username)
if 'devices' not in user_data:
user_data['devices'] = []
if self.device_id not in user_data['devices']:
user_data['devices'].append(self.device_id)
await self.user_manager.update_user(username, user_data)
# 保存配置
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.error(f"Error binding user: {e}")
return False
async def unbind_user(self) -> bool:
"""解绑设备当前用户"""
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
if not self.private_config.get('owner'):
return True
username = self.private_config['owner']
# 从用户数据中移除设备
user_data = self.user_manager.get_user(username)
if user_data and 'devices' in user_data:
if self.device_id in user_data['devices']:
user_data['devices'].remove(self.device_id)
self.user_manager.update_user(username, user_data)
# 从设备配置中移除所有者
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
if self.device_id in all_configs:
if 'owner' in all_configs[self.device_id]:
del all_configs[self.device_id]['owner']
self.private_config = all_configs[self.device_id]
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.error(f"Error unbinding user: {e}")
return False
def get_owner(self) -> Optional[str]:
"""获取设备当前所有者"""
return self.private_config.get('owner')
+59 -14
View File
@@ -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
+97
View File
@@ -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]
+38
View File
@@ -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}")
+107 -32
View File
@@ -4,18 +4,22 @@ import logging
from aiohttp import web
from core.utils.util import get_project_dir
from config.private_config import PrivateConfig
from manager.api.user_manager import UserManager # 添加导入
from core.utils.auth_code_gen import AuthCodeGenerator # 添加导入
logger = logging.getLogger(__name__)
class ConfigHandler:
def __init__(self):
def __init__(self, session_manager):
self.session_manager = session_manager
self.user_manager = UserManager() # 添加 user_manager 实例
self.private_config_path = get_project_dir() + 'data/.private_config.yaml'
self.config_path = get_project_dir() + 'config.yaml'
# 如果存在.config.yaml文件,则使用该文件
if os.path.exists(get_project_dir() + "data/.config.yaml"):
self.config_path = get_project_dir() + "data/.config.yaml"
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
self.config = yaml.safe_load(f)
async def get_module_options(self, request):
"""Get all available module options from config.yaml"""
@@ -45,46 +49,41 @@ class ConfigHandler:
})
async def get_private_configs(self, request):
"""获取所有私有配置设备列表及其配置"""
"""只返回用户绑定的设备配置"""
try:
username = request['username']
logger.info(f"Getting devices for user: {username}")
# 从用户管理器获取用户的设备列表
user_devices = await self.user_manager.get_user_devices(username)
logger.info(f"User {username} has devices: {user_devices}")
# 读取所有配置
all_configs = {}
if os.path.exists(self.private_config_path):
with open(self.private_config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
# 转换配置为前端友好的格式
devices = []
for device_id, config in all_configs.items():
device_info = {
'id': device_id,
'config': {
'selected_module': config.get('selected_module', {}),
'prompt': config.get('prompt', ''),
'last_chat_time': config.get('last_chat_time', ''),
'nickname': config.get('nickname', '小智'),
'modules': {
'LLM': config.get('LLM', {}),
'TTS': config.get('TTS', {}),
'ASR': config.get('ASR', {}),
'VAD': config.get('VAD', {})
}
}
}
devices.append(device_info)
# 只返回用户有权限的设备配置
user_configs = {
device_id: config
for device_id, config in all_configs.items()
if device_id in user_devices
}
logger.info(f"Returning {len(user_configs)} device configs for user {username}")
return web.json_response({
'success': True,
'data': devices,
'data': user_configs,
'message': '获取成功'
})
except Exception as e:
logger.error(f"Error getting private configs: {str(e)}", exc_info=True)
logger.error(f"Error getting devices for user {request.get('username')}: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': '获取配置失败'
})
'message': f'获取设备列表失败: {str(e)}'
}, status=400)
async def save_device_config(self, request):
"""保存单个设备的配置"""
@@ -92,6 +91,16 @@ class ConfigHandler:
data = await request.json()
device_id = data.get('id')
config = data.get('config')
username = request['username'] # 从请求中获取用户名
# 检查设备所有权
user_devices = await self.user_manager.get_user_devices(username)
if device_id not in user_devices:
return web.json_response({
'success': False,
'message': '无权操作此设备'
}, status=403)
logger.info(f"Device config updated: {device_id} :\n{config}")
if not device_id or not config:
return web.json_response({
@@ -130,10 +139,20 @@ class ConfigHandler:
try:
data = await request.json()
device_id = data.get('device_id')
username = request['username']
# 检查设备所有权
user_devices = await self.user_manager.get_user_devices(username)
if device_id not in user_devices:
return web.json_response({
'success': False,
'message': '无权删除此设备'
}, status=403)
# 使用PrivateConfig处理配置删除
private_config = PrivateConfig(device_id, self.config)
success = await private_config.delete_config()
await self.user_manager.remove_device(username, device_id)
if not success:
raise Exception("Failed to delete device config")
@@ -149,3 +168,59 @@ class ConfigHandler:
'success': False,
'message': f'删除配置失败: {str(e)}'
})
async def bind_device(self, request):
"""绑定设备到用户"""
try:
data = await request.json()
auth_code = data.get('auth_code')
username = request['username']
if not auth_code or len(auth_code) != 6:
return web.json_response({
'success': False,
'message': '请输入6位认证码'
}, status=400)
# 读取所有设备配置
with open(self.private_config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
# 查找匹配认证码的设备
device_found = None
for device_id, config in all_configs.items():
if config.get('auth_code') == auth_code and not config.get('owner'):
device_found = device_id
break
if not device_found:
return web.json_response({
'success': False,
'message': '认证码无效或设备已被绑定'
}, status=400)
# 使用 PrivateConfig 进行绑定
private_config = PrivateConfig(device_found, self.config, AuthCodeGenerator())
await private_config.load_or_create()
# 绑定设备到用户 - 修改为异步调用
success = await private_config.bind_user(username)
if success:
# 同时更新用户的设备列表 - 修改为异步调用
await self.user_manager.add_device(username, device_found)
return web.json_response({
'success': True,
'message': '设备绑定成功'
})
else:
return web.json_response({
'success': False,
'message': '设备绑定失败'
}, status=500)
except Exception as e:
logger.error(f"Error binding device: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': f'绑定设备失败: {str(e)}'
}, status=500)
+10 -7
View File
@@ -5,8 +5,9 @@ import datetime
logger = logging.getLogger(__name__)
class LoginHandler:
def __init__(self, config):
self.config = config
def __init__(self, user_manager, session_manager):
self.user_manager = user_manager
self.session_manager = session_manager
async def handle_login(self, request):
"""处理登录请求"""
@@ -22,8 +23,8 @@ class LoginHandler:
'message': '用户名和密码不能为空'
})
stored_user = self.config['get_user'](username)
if not stored_user or stored_user['password'] != self.config['hash_password'](password):
stored_user = await self.user_manager.get_user(username)
if not stored_user or stored_user['password'] != self.user_manager.hash_password(password):
logger.warning(f"Failed login attempt for user {username} from {request.remote}")
return web.json_response({
'success': False,
@@ -31,14 +32,16 @@ class LoginHandler:
})
# 更新最后登录时间
self.config['update_user'](username, {
await self.user_manager.update_user(username, {
'last_login': datetime.datetime.now().isoformat()
})
logger.info(f"Successful login for user {username} from {request.remote}")
# 创建会话并返回session_id
session_id = self.session_manager.create_session(username)
return web.json_response({
'success': True,
'message': '登录成功'
'message': '登录成功',
'session_id': session_id
})
except Exception as e:
+13 -25
View File
@@ -1,14 +1,13 @@
import logging
from aiohttp import web
import datetime
from core.utils.util import check_password
logger = logging.getLogger(__name__)
class RegisterHandler:
def __init__(self, config):
self.config = config
def __init__(self, user_manager):
self.user_manager = user_manager
async def handle_register(self, request):
"""处理注册请求"""
@@ -17,12 +16,6 @@ class RegisterHandler:
username = data.get('username')
password = data.get('password')
if not check_password(password):
return web.json_response({
'success': False,
'message': '密码必须包含大小写字母、数字且长度至少8位'
})
if not username or not password:
logger.warning(f"Registration attempt with empty credentials from {request.remote}")
return web.json_response({
@@ -30,28 +23,23 @@ class RegisterHandler:
'message': '用户名和密码不能为空'
})
users = self.config.get('users', {})
# 由于现在所有用户都能看到所有设备,从安全角度上考虑,只允许注册一个用户
# 未来绑定设备功能完成后,再放开任意注册
if len(users) >= 1:
return web.json_response({
'success': False,
'message': '系统已经初始化过了,如果忘记了密码,请直接删除“.secrets.yaml”文件,删除后重启本服务'
})
if username in users:
# 检查用户是否已存在
if await self.user_manager.get_user(username):
logger.warning(f"Registration attempt with existing username {username} from {request.remote}")
return web.json_response({
'success': False,
'message': '用户名已存在'
})
# 存储新用户
self.config['users'][username] = {
'password': self.config['hash_password'](password),
'created_at': datetime.datetime.now().isoformat()
# 创建用户
user_data = {
'username': username,
'password': self.user_manager.hash_password(password),
'devices': [],
'created_at': datetime.datetime.now().isoformat(),
'last_login': ''
}
self.config['save_user_data']()
await self.user_manager.add_user(username, user_data)
logger.info(f"Successfully registered new user {username} from {request.remote}")
return web.json_response({
@@ -60,7 +48,7 @@ class RegisterHandler:
})
except Exception as e:
logger.error(f"Registration error: {str(e)}", exc_info=True)
logger.error(f"Register error: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': '注册失败,请稍后重试'
+128 -23
View File
@@ -3,16 +3,15 @@ import yaml
import hashlib
import logging
from core.utils.util import get_project_dir
from core.utils.lock_manager import FileLockManager
logger = logging.getLogger(__name__)
class UserManager:
def __init__(self):
self.secrets_path = get_project_dir() + 'data/.secrets.yaml'
self.users = {}
self.lock_manager = FileLockManager()
self.ensure_secrets_file()
self.load_user_data()
def ensure_secrets_file(self):
"""确保 .secrets.yaml 文件存在"""
@@ -28,24 +27,51 @@ class UserManager:
except Exception as e:
logger.error(f"Failed to create .secrets.yaml: {e}")
raise
def load_user_data(self):
"""加载用户数据"""
async def _load_user_data_internal(self):
"""内部加载用户数据方法 - 不获取锁"""
try:
with open(self.secrets_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f) or {'users': {}}
self.users = data['users']
logger.info("Successfully loaded user data")
users = data['users']
logger.debug("Successfully loaded user data")
except Exception as e:
logger.error(f"Failed to load user data: {e}")
self.users = {}
users = {}
return users
async def load_user_data(self):
"""加载用户数据"""
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
users = await self._load_user_data_internal()
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Failed to load user data: {e}")
users = {}
return users
def save_user_data(self):
"""保存用户数据"""
async def _save_user_data_internal(self, users):
"""内部保存用户数据方法 - 不获取锁"""
try:
with open(self.secrets_path, 'w', encoding='utf-8') as f:
yaml.dump({'users': self.users}, f)
logger.info("Successfully saved user data")
yaml.dump({'users': users}, f)
logger.debug("Successfully saved user data")
except Exception as e:
logger.error(f"Failed to save user data: {e}")
raise
async def save_user_data(self, users):
"""外部保存用户数据方法 - 获取锁"""
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
await self._save_user_data_internal(users)
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Failed to save user data: {e}")
raise
@@ -54,16 +80,95 @@ class UserManager:
"""密码哈希"""
return hashlib.sha256(password.encode()).hexdigest()
def get_users(self):
"""获取所有用户"""
return self.users
async def get_users(self):
"""异步获取所有用户"""
users = await self.load_user_data() # 确保获取最新数据
return users
def get_user(self, username):
"""获取指定用户"""
return self.users.get(username)
async def get_user(self, username):
"""异步获取指定用户"""
users = await self.load_user_data() # 确保获取最新数据
return users.get(username)
def update_user(self, username, data):
async def add_user(self, username: str, user_data: dict):
"""异步添加新用户"""
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
users = await self._load_user_data_internal() # 确保获取最新数据
if username in users:
raise ValueError("User already exists")
users[username] = user_data
await self._save_user_data_internal(users)
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Error adding user: {e}")
raise
async def update_user(self, username, data):
"""更新用户数据"""
if username in self.users:
self.users[username].update(data)
self.save_user_data()
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
users = await self._load_user_data_internal() # 确保获取最新数据
if username in users:
users[username].update(data)
await self._save_user_data_internal(users)
return True
return False
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Error updating user: {e}")
return False
async def get_user_devices(self, username: str) -> list:
"""获取用户的设备列表"""
user = await self.get_user(username)
print(user)
if user and user.get('devices'):
return user['devices']
return []
async def add_device(self, username: str, device_id: str) -> bool:
"""添加设备到用户的设备列表"""
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
users = await self._load_user_data_internal() # 确保获取最新数据
user = users.get(username) # 直接从内存获取,因为已经有锁
if not user:
return False
if 'devices' not in user:
user['devices'] = []
if device_id not in user['devices']:
user['devices'].append(device_id)
await self._save_user_data_internal(users)
return True
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Error adding device: {e}")
return False
async def remove_device(self, username: str, device_id: str) -> bool:
"""从用户的设备列表中移除设备"""
try:
await self.lock_manager.acquire_lock(self.secrets_path)
try:
users = await self._load_user_data_internal() # 确保获取最新数据
user = users.get(username) # 直接从内存获取,因为已经有锁
if user and 'devices' in user:
if device_id in user['devices']:
user['devices'].remove(device_id)
await self._save_user_data_internal(users)
return True
return False
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
logger.error(f"Error removing device: {e}")
return False
+48 -18
View File
@@ -13,26 +13,33 @@ from manager.api.login import LoginHandler
from manager.api.register import RegisterHandler
from manager.api.user_manager import UserManager
from manager.api.config import ConfigHandler
from manager.session import SessionManager
from functools import wraps
logger = logging.getLogger(__name__)
def auth_required(handler):
"""鉴权装饰器"""
@wraps(handler)
async def wrapper(self, request):
session_id = request.cookies.get('session_id')
username = self.session_manager.validate_session(session_id)
if not username:
return web.json_response({'error': 'Unauthorized'}, status=401)
# 将用户名添加到请求对象
request['username'] = username
return await handler(self, request)
return wrapper
class WebUI:
def __init__(self):
self.app = web.Application()
self.user_manager = UserManager()
self.session_manager = SessionManager()
# 添加静态文件路径
self.static_path = os.path.join(root_dir, 'manager', 'static', 'webui')
# 创建配置字典
self.config = {
'users': self.user_manager.get_users(),
'hash_password': self.user_manager.hash_password,
'save_user_data': self.user_manager.save_user_data,
'get_user': self.user_manager.get_user,
'update_user': self.user_manager.update_user
}
self.setup_routes()
self.setup_cors()
@@ -52,18 +59,21 @@ class WebUI:
def setup_routes(self):
"""设置路由"""
login_handler = LoginHandler(self.config)
register_handler = RegisterHandler(self.config)
config_handler = ConfigHandler()
login_handler = LoginHandler(self.user_manager, self.session_manager)
register_handler = RegisterHandler(self.user_manager)
config_handler = ConfigHandler(self.session_manager)
# API 路由
# Public APIs
self.app.router.add_post('/api/login', login_handler.handle_login)
self.app.router.add_post('/api/register', register_handler.handle_register)
self.app.router.add_get('/api/config/devices', config_handler.get_private_configs)
self.app.router.add_post('/api/config/device', config_handler.save_device_config)
self.app.router.add_get('/api/config/module-options', config_handler.get_module_options)
self.app.router.add_post('/api/config/save_device_config', config_handler.save_device_config)
self.app.router.add_post('/api/config/delete_device', config_handler.delete_device_config)
# Protected APIs
self.app.router.add_get('/api/config/devices', self.auth_wrapper(config_handler.get_private_configs))
self.app.router.add_get('/api/config/module-options', self.auth_wrapper(config_handler.get_module_options))
self.app.router.add_post('/api/config/save_device_config', self.auth_wrapper(config_handler.save_device_config))
self.app.router.add_post('/api/config/delete_device', self.auth_wrapper(config_handler.delete_device_config))
self.app.router.add_post('/api/config/bind_device', self.auth_wrapper(config_handler.bind_device))
# 添加静态文件服务
self.app.router.add_static('/assets/', path=os.path.join(self.static_path, 'assets'))
@@ -77,6 +87,26 @@ class WebUI:
return web.FileResponse(index_file)
return web.Response(status=404, text='Not found')
def auth_wrapper(self, handler):
"""包装处理器添加鉴权"""
@wraps(handler)
async def wrapper(request):
# 从请求头获取session_id
session_id = request.headers.get('Authorization')
if not session_id:
logger.warning("No session_id in Authorization header")
return web.json_response({'error': 'Unauthorized'}, status=401)
username = self.session_manager.validate_session(session_id)
if not username:
logger.warning(f"Invalid session_id: {session_id}")
return web.json_response({'error': 'Unauthorized'}, status=401)
request['username'] = username
logger.debug(f"Auth success for user: {username}")
return await handler(request)
return wrapper
def run(self, host='0.0.0.0', port=8002):
"""运行服务器"""
local_ip = get_local_ip()
+33
View File
@@ -0,0 +1,33 @@
import time
from typing import Dict, Optional
class SessionManager:
def __init__(self):
self.sessions: Dict[str, Dict] = {}
self.session_timeout = 24 * 60 * 60 # 24小时过期
def create_session(self, username: str) -> str:
"""创建新会话"""
session_id = str(hash(f"{username}:{time.time()}"))
self.sessions[session_id] = {
'username': username,
'created_at': time.time()
}
return session_id
def validate_session(self, session_id: str) -> Optional[str]:
"""验证会话是否有效,返回用户名"""
if session_id not in self.sessions:
return None
session = self.sessions[session_id]
if time.time() - session['created_at'] > self.session_timeout:
del self.sessions[session_id]
return None
return session['username']
def remove_session(self, session_id: str):
"""删除会话"""
if session_id in self.sessions:
del self.sessions[session_id]