mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
update:优化web端目录结构
This commit is contained in:
@@ -14,7 +14,7 @@ async def main():
|
||||
|
||||
# 启动 HTTP 配置服务器
|
||||
http_runner = None
|
||||
if config['server'].get('http', {}).get('enabled', False):
|
||||
if config['manager'].get('enabled', False):
|
||||
config_server = ConfigServer(config)
|
||||
try:
|
||||
http_runner = await config_server.start()
|
||||
|
||||
+5
-5
@@ -17,11 +17,11 @@ server:
|
||||
# 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。
|
||||
#allowed_devices:
|
||||
# - "24:0A:C4:1D:3B:F0" # MAC地址列表
|
||||
http:
|
||||
enabled: false
|
||||
ip: 0.0.0.0
|
||||
port: 8001
|
||||
token: password
|
||||
manager:
|
||||
enabled: false
|
||||
ip: 0.0.0.0
|
||||
port: 8001
|
||||
token: password
|
||||
|
||||
xiaozhi:
|
||||
type: hello
|
||||
|
||||
+33
-116
@@ -1,135 +1,51 @@
|
||||
import logging
|
||||
import yaml
|
||||
import os
|
||||
from aiohttp import web
|
||||
from core.utils.util import get_project_dir
|
||||
from core.utils.util import get_local_ip, get_project_dir
|
||||
from manager.api.prompt import PromptHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def handle_options(request):
|
||||
headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
return web.Response(headers=headers)
|
||||
|
||||
|
||||
class ConfigServer:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.app = web.Application()
|
||||
|
||||
# 初始化接口处理器
|
||||
self.prompt_handler = PromptHandler(config)
|
||||
self.setup_routes()
|
||||
|
||||
def setup_routes(self):
|
||||
self.app.router.add_get('/', self.index)
|
||||
self.app.router.add_get('/api/prompt', self.get_prompt)
|
||||
self.app.router.add_post('/api/prompt', self.update_prompt)
|
||||
self.app.router.add_options('/api/prompt', self.handle_options)
|
||||
# 注册prompt接口
|
||||
self.app.router.add_get('/api/prompt', self.prompt_handler.get_prompt)
|
||||
self.app.router.add_post('/api/prompt', self.prompt_handler.update_prompt)
|
||||
self.app.router.add_options('/api/prompt', handle_options)
|
||||
|
||||
async def index(self, request):
|
||||
html = '''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>小智提示词配置</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { max-width: 800px; margin: 20px auto; padding: 0 20px; font-family: Arial, sans-serif; }
|
||||
textarea { width: 100%; height: 300px; margin: 20px 0; padding: 10px; }
|
||||
button { padding: 10px 20px; font-size: 16px; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>小智提示词配置</h1>
|
||||
<textarea id="prompt"></textarea>
|
||||
<div>
|
||||
<button onclick="updatePrompt()">更新提示词</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
<script>
|
||||
// 加载当前prompt
|
||||
fetch('/api/prompt')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('prompt').value = data.prompt;
|
||||
});
|
||||
# 注册静态文件路由
|
||||
static_dir = os.path.join(get_project_dir(), 'manager/static') # 获取static目录绝对路径
|
||||
self.app.router.add_static(
|
||||
prefix='/manager/', # 匹配前缀
|
||||
path=static_dir, # 静态文件目录
|
||||
name='static'
|
||||
)
|
||||
self.app.router.add_get('/manager', self.redirect_to_index)
|
||||
|
||||
function updatePrompt() {
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
fetch('/api/prompt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({prompt: prompt})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const status = document.getElementById('status');
|
||||
if(data.success) {
|
||||
status.textContent = '更新成功!';
|
||||
status.className = 'success';
|
||||
} else {
|
||||
status.textContent = '更新失败: ' + data.error;
|
||||
status.className = 'error';
|
||||
}
|
||||
setTimeout(() => status.textContent = '', 3000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
return web.Response(text=html, content_type='text/html')
|
||||
|
||||
async def handle_options(self, request):
|
||||
headers = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
|
||||
}
|
||||
return web.Response(headers=headers)
|
||||
|
||||
async def verify_token(self, request):
|
||||
if 'token' not in self.config['server']['http']:
|
||||
return True
|
||||
|
||||
expected_token = self.config['server']['http']['token']
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
|
||||
if not token or token != expected_token:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_prompt(self, request):
|
||||
if not await self.verify_token(request):
|
||||
return web.json_response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
headers = {'Access-Control-Allow-Origin': '*'}
|
||||
return web.json_response({
|
||||
'prompt': self.config['prompt']
|
||||
}, headers=headers)
|
||||
|
||||
async def update_prompt(self, request):
|
||||
if not await self.verify_token(request):
|
||||
return web.json_response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
if 'prompt' not in data:
|
||||
return web.json_response({'error': 'Missing prompt field'}, status=400)
|
||||
|
||||
# 更新内存中的配置
|
||||
self.config['prompt'] = data['prompt']
|
||||
|
||||
# 更新配置文件
|
||||
config_path = get_project_dir() + 'config.yaml'
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(self.config, f, allow_unicode=True)
|
||||
|
||||
headers = {'Access-Control-Allow-Origin': '*'}
|
||||
return web.json_response({'success': True}, headers=headers)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update prompt: {e}")
|
||||
return web.json_response({'error': str(e)}, status=500)
|
||||
async def redirect_to_index(self, _):
|
||||
raise web.HTTPFound('/manager/')
|
||||
|
||||
async def start(self):
|
||||
try:
|
||||
http_config = self.config['server']['http']
|
||||
http_config = self.config['manager']
|
||||
if not http_config.get('enabled', False):
|
||||
logger.info("HTTP server is disabled")
|
||||
return
|
||||
@@ -138,7 +54,8 @@ class ConfigServer:
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, http_config['ip'], http_config['port'])
|
||||
await site.start()
|
||||
logger.info(f"Config HTTP server is running at http://{http_config['ip']}:{http_config['port']}")
|
||||
logger.info(
|
||||
f"Config HTTP server is running at http://{get_local_ip()}:{http_config['port']}/manager/index.html")
|
||||
return runner # 返回runner以便后续清理
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start HTTP server: {e}")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
async def verify_token(config, request):
|
||||
if 'token' not in config['manager']:
|
||||
return True
|
||||
|
||||
expected_token = config['manager']['token']
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
|
||||
if not token or token != expected_token:
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,36 @@
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from manager.api.auth import verify_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PromptHandler:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
async def get_prompt(self, request):
|
||||
if not await verify_token(self.config, request):
|
||||
return web.json_response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
return web.json_response({
|
||||
'prompt': self.config['prompt'],
|
||||
'Access-Control-Allow-Origin': '*'
|
||||
})
|
||||
|
||||
async def update_prompt(self, request):
|
||||
if not await verify_token(self.config, request):
|
||||
return web.json_response({'error': 'Unauthorized'}, status=401)
|
||||
|
||||
try:
|
||||
data = await request.json()
|
||||
if 'prompt' not in data:
|
||||
return web.json_response({'error': 'Missing prompt field'}, status=400)
|
||||
|
||||
# 通过config参数回传修改能力
|
||||
self.config['prompt'] = data['prompt']
|
||||
return web.json_response({'success': True}, headers={'Access-Control-Allow-Origin': '*'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update prompt: {e}")
|
||||
return web.json_response({'error': str(e)}, status=500)
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>小智提示词配置</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { max-width: 800px; margin: 20px auto; padding: 0 20px; font-family: Arial, sans-serif; }
|
||||
textarea { width: 100%; height: 300px; margin: 20px 0; padding: 10px; }
|
||||
button { padding: 10px 20px; font-size: 16px; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>小智提示词配置</h1>
|
||||
<textarea id="prompt"></textarea>
|
||||
<div>
|
||||
<button onclick="updatePrompt()">更新提示词</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
<script>
|
||||
// 加载当前prompt
|
||||
fetch('/api/prompt')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('prompt').value = data.prompt;
|
||||
});
|
||||
|
||||
function updatePrompt() {
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
fetch('/api/prompt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({prompt: prompt})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const status = document.getElementById('status');
|
||||
if(data.success) {
|
||||
status.textContent = '更新成功!';
|
||||
status.className = 'success';
|
||||
} else {
|
||||
status.textContent = '更新失败: ' + data.error;
|
||||
status.className = 'error';
|
||||
}
|
||||
setTimeout(() => status.textContent = '', 3000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>login</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
login
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>小智提示词配置</title>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { max-width: 800px; margin: 20px auto; padding: 0 20px; font-family: Arial, sans-serif; }
|
||||
textarea { width: 100%; height: 300px; margin: 20px 0; padding: 10px; }
|
||||
button { padding: 10px 20px; font-size: 16px; }
|
||||
.success { color: green; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>小智提示词配置</h1>
|
||||
<textarea id="prompt"></textarea>
|
||||
<div>
|
||||
<button onclick="updatePrompt()">更新提示词</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
<script>
|
||||
// 加载当前prompt
|
||||
fetch('/api/prompt')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('prompt').value = data.prompt;
|
||||
});
|
||||
|
||||
function updatePrompt() {
|
||||
const prompt = document.getElementById('prompt').value;
|
||||
fetch('/api/prompt', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({prompt: prompt})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const status = document.getElementById('status');
|
||||
if(data.success) {
|
||||
status.textContent = '更新成功!';
|
||||
status.className = 'success';
|
||||
} else {
|
||||
status.textContent = '更新失败: ' + data.error;
|
||||
status.className = 'error';
|
||||
}
|
||||
setTimeout(() => status.textContent = '', 3000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user