mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 17:43:55 +08:00
Merge branch 'main' into main
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from config.settings import load_config
|
from config.settings import load_config
|
||||||
from core.server import WebSocketServer
|
from core.websocket_server import WebSocketServer
|
||||||
from core.http_server import ConfigServer
|
from manager.http_server import ConfigServer
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
setup_logging() # 最先初始化日志
|
setup_logging() # 最先初始化日志
|
||||||
|
|||||||
+32
@@ -1,3 +1,7 @@
|
|||||||
|
# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。
|
||||||
|
# 在开发中,建议将【config.yaml】复制一份,改成【.config.yaml】。 系统会优先读取【.config.yaml】文件的配置。
|
||||||
|
# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。
|
||||||
|
|
||||||
# 服务器基础配置(Basic server configuration)
|
# 服务器基础配置(Basic server configuration)
|
||||||
server:
|
server:
|
||||||
# 服务器监听地址和端口(Server listening address and port)
|
# 服务器监听地址和端口(Server listening address and port)
|
||||||
@@ -140,3 +144,31 @@ TTS:
|
|||||||
output_file: tmp/
|
output_file: tmp/
|
||||||
access_token: 你的硅基流动API密钥
|
access_token: 你的硅基流动API密钥
|
||||||
response_format: wav
|
response_format: wav
|
||||||
|
FishSpeech:
|
||||||
|
# 定义TTS API类型
|
||||||
|
#启动tts方法:
|
||||||
|
#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
|
||||||
|
type: fishspeech
|
||||||
|
output_file: tmp/
|
||||||
|
response_format: wav
|
||||||
|
reference_id: null
|
||||||
|
reference_audio: ["/tmp/test.wav",]
|
||||||
|
reference_text: ["你弄来这些吟词宴曲来看,还是这些混话来欺负我。",]
|
||||||
|
normalize: true
|
||||||
|
max_new_tokens: 1024
|
||||||
|
chunk_length: 200
|
||||||
|
top_p: 0.7
|
||||||
|
repetition_penalty: 1.2
|
||||||
|
temperature: 0.7
|
||||||
|
streaming: false
|
||||||
|
use_memory_cache: "on"
|
||||||
|
seed: null
|
||||||
|
channels: 1
|
||||||
|
rate: 44100
|
||||||
|
api_key: "YOUR_API_KEY"
|
||||||
|
api_url: "http://127.0.0.1:8080/v1/tts"
|
||||||
+18
-4
@@ -1,15 +1,29 @@
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
|
from ruamel.yaml import YAML
|
||||||
from core.utils.util import read_config, get_project_dir
|
from core.utils.util import read_config, get_project_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_file():
|
||||||
|
default_config_file = "config.yaml"
|
||||||
|
# 判断是否存在私有的配置文件
|
||||||
|
if os.path.exists(get_project_dir() + "." + default_config_file):
|
||||||
|
default_config_file = "." + default_config_file
|
||||||
|
return default_config_file
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""加载配置文件"""
|
"""加载配置文件"""
|
||||||
parser = argparse.ArgumentParser(description="Server configuration")
|
parser = argparse.ArgumentParser(description="Server configuration")
|
||||||
default_config_file = "config.yaml"
|
default_config_file = get_config_file()
|
||||||
# 判断是否存在私有的配置文件
|
|
||||||
if os.path.exists(get_project_dir() + "." + default_config_file):
|
|
||||||
default_config_file = "." + default_config_file
|
|
||||||
parser.add_argument("--config_path", type=str, default=default_config_file)
|
parser.add_argument("--config_path", type=str, default=default_config_file)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return read_config(args.config_path)
|
return read_config(args.config_path)
|
||||||
|
|
||||||
|
|
||||||
|
def update_config(config):
|
||||||
|
yaml = YAML()
|
||||||
|
yaml.preserve_quotes = True
|
||||||
|
"""将配置保存到YAML文件"""
|
||||||
|
with open(get_config_file(), 'w') as f:
|
||||||
|
yaml.dump(config, f)
|
||||||
|
|||||||
+2
-2
@@ -15,8 +15,8 @@ from core.handle.textHandle import handleTextMessage
|
|||||||
from core.utils.util import get_string_no_punctuation_or_emoji
|
from core.utils.util import get_string_no_punctuation_or_emoji
|
||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||||
from core.handle.audioHandle import handleAudioMessage, sendAudioMessage
|
from core.handle.audioHandle import handleAudioMessage, sendAudioMessage
|
||||||
from .auth import AuthMiddleware, AuthenticationError
|
from config.private_config import PrivateConfig
|
||||||
from config.private_config import PrivateConfig # Updated import path
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
|
|
||||||
class ConnectionHandler:
|
class ConnectionHandler:
|
||||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
|
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
import ormsgpack
|
||||||
|
from pathlib import Path
|
||||||
|
from pydantic import BaseModel, Field, conint, model_validator
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Literal
|
||||||
|
# from base import TTSProviderBase
|
||||||
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
|
||||||
|
|
||||||
|
class ServeReferenceAudio(BaseModel):
|
||||||
|
audio: bytes
|
||||||
|
text: str
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
def decode_audio(cls, values):
|
||||||
|
audio = values.get("audio")
|
||||||
|
if (
|
||||||
|
isinstance(audio, str) and len(audio) > 255
|
||||||
|
): # Check if audio is a string (Base64)
|
||||||
|
try:
|
||||||
|
values["audio"] = base64.b64decode(audio)
|
||||||
|
except Exception as e:
|
||||||
|
# If the audio is not a valid base64 string, we will just ignore it and let the server handle it
|
||||||
|
pass
|
||||||
|
return values
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ServeReferenceAudio(text={self.text!r}, audio_size={len(self.audio)})"
|
||||||
|
|
||||||
|
class ServeTTSRequest(BaseModel):
|
||||||
|
text: str
|
||||||
|
chunk_length: Annotated[int, conint(ge=100, le=300, strict=True)] = 200
|
||||||
|
# Audio format
|
||||||
|
format: Literal["wav", "pcm", "mp3"] = "wav"
|
||||||
|
# References audios for in-context learning
|
||||||
|
references: list[ServeReferenceAudio] = []
|
||||||
|
# Reference id
|
||||||
|
# For example, if you want use https://fish.audio/m/7f92f8afb8ec43bf81429cc1c9199cb1/
|
||||||
|
# Just pass 7f92f8afb8ec43bf81429cc1c9199cb1
|
||||||
|
reference_id: str | None = None
|
||||||
|
seed: int | None = None
|
||||||
|
use_memory_cache: Literal["on", "off"] = "off"
|
||||||
|
# Normalize text for en & zh, this increase stability for numbers
|
||||||
|
normalize: bool = True
|
||||||
|
# not usually used below
|
||||||
|
streaming: bool = False
|
||||||
|
max_new_tokens: int = 1024
|
||||||
|
top_p: Annotated[float, Field(ge=0.1, le=1.0, strict=True)] = 0.7
|
||||||
|
repetition_penalty: Annotated[float, Field(ge=0.9, le=2.0, strict=True)] = 1.2
|
||||||
|
temperature: Annotated[float, Field(ge=0.1, le=1.0, strict=True)] = 0.7
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# Allow arbitrary types for pytorch related types
|
||||||
|
arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
def audio_to_bytes(file_path):
|
||||||
|
if not file_path or not Path(file_path).exists():
|
||||||
|
return None
|
||||||
|
with open(file_path, "rb") as wav_file:
|
||||||
|
wav = wav_file.read()
|
||||||
|
return wav
|
||||||
|
|
||||||
|
def read_ref_text(ref_text):
|
||||||
|
path = Path(ref_text)
|
||||||
|
if path.exists() and path.is_file():
|
||||||
|
with path.open("r", encoding="utf-8") as file:
|
||||||
|
return file.read()
|
||||||
|
return ref_text
|
||||||
|
|
||||||
|
class TTSProvider(TTSProviderBase):
|
||||||
|
|
||||||
|
def __init__(self, config, delete_audio_file):
|
||||||
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
|
self.reference_id = config.get("reference_id")
|
||||||
|
self.reference_audio = config.get("reference_audio",[])
|
||||||
|
self.reference_text = config.get("reference_text",[])
|
||||||
|
self.format = config.get("format","wav")
|
||||||
|
self.channels = config.get("channels",1)
|
||||||
|
self.rate = config.get("rate",44100)
|
||||||
|
self.api_key = config.get("api_key","YOUR_API_KEY")
|
||||||
|
self.normalize = config.get("normalize",True)
|
||||||
|
self.max_new_tokens = config.get("max_new_tokens",1024)
|
||||||
|
self.chunk_length = config.get("chunk_length",200)
|
||||||
|
self.top_p = config.get("top_p",0.7)
|
||||||
|
self.repetition_penalty = config.get("repetition_penalty",1.2)
|
||||||
|
self.temperature = config.get("temperature",0.7)
|
||||||
|
self.streaming = config.get("streaming",False)
|
||||||
|
self.use_memory_cache = config.get("use_memory_cache","on")
|
||||||
|
self.seed = config.get("seed")
|
||||||
|
self.api_url = config.get("api_url","http://127.0.0.1:8080/v1/tts")
|
||||||
|
|
||||||
|
def generate_filename(self, extension=".wav"):
|
||||||
|
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||||
|
|
||||||
|
async def text_to_speak(self, text, output_file):
|
||||||
|
# Prepare reference data
|
||||||
|
byte_audios = [audio_to_bytes(ref_audio) for ref_audio in self.reference_audio]
|
||||||
|
ref_texts = [read_ref_text(ref_text) for ref_text in self.reference_text]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"text": text,
|
||||||
|
"references": [
|
||||||
|
ServeReferenceAudio(
|
||||||
|
audio=audio if audio else b"", text=text
|
||||||
|
)
|
||||||
|
for text, audio in zip(ref_texts, byte_audios)
|
||||||
|
],
|
||||||
|
"reference_id": self.reference_id,
|
||||||
|
"normalize": self.normalize,
|
||||||
|
"format": self.format,
|
||||||
|
"max_new_tokens": self.max_new_tokens,
|
||||||
|
"chunk_length": self.chunk_length,
|
||||||
|
"top_p": self.top_p,
|
||||||
|
"repetition_penalty": self.repetition_penalty,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"streaming": self.streaming,
|
||||||
|
"use_memory_cache": self.use_memory_cache,
|
||||||
|
"seed": self.seed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pydantic_data = ServeTTSRequest(**data)
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
self.api_url,
|
||||||
|
data=ormsgpack.packb(pydantic_data, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/msgpack",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
audio_content = response.content
|
||||||
|
|
||||||
|
with open(output_file, "wb") as audio_file:
|
||||||
|
audio_file.write(audio_content)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Request failed with status code {response.status_code}")
|
||||||
|
print(response.json())
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
from config.settings import update_config
|
||||||
|
from ruamel.yaml.scalarstring import PreservedScalarString
|
||||||
from manager.api.auth import verify_token
|
from manager.api.auth import verify_token
|
||||||
from manager.api.response import response_unauthorized, response_success, response_error
|
from manager.api.response import response_unauthorized, response_success, response_error
|
||||||
|
|
||||||
@@ -28,10 +30,13 @@ class PromptApi:
|
|||||||
if 'prompt' not in data:
|
if 'prompt' not in data:
|
||||||
return response_success()
|
return response_success()
|
||||||
|
|
||||||
self.config['prompt'] = data['prompt']
|
# 使用PreservedScalarString保留多行文本格式
|
||||||
# TODO 保存到配置文件
|
self.config['prompt'] = PreservedScalarString(data['prompt'])
|
||||||
return response_success()
|
|
||||||
|
|
||||||
|
# 保存到配置文件
|
||||||
|
update_config(self.config)
|
||||||
|
|
||||||
|
return response_success()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to update prompt: {e}")
|
logger.error(f"Failed to update prompt: {e}")
|
||||||
return response_error(str(e))
|
return response_error(str(e))
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/* common.css */
|
||||||
|
/* 基础样式 */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: linear-gradient(135deg, #1a1f25 0%, #0d1117 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头部组件 */
|
||||||
|
.app-header {
|
||||||
|
background: rgba(13, 17, 23, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
color: #fff;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页脚组件 */
|
||||||
|
.app-footer {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 20px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 背景动画 */
|
||||||
|
.animated-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: -1;
|
||||||
|
background: linear-gradient(-45deg, #1a1f25, #0d1117, #162030, #1c1c1c);
|
||||||
|
background-size: 400% 400%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.user-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 23px;
|
||||||
|
right: 32px;
|
||||||
|
z-index: 1000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
+35
-66
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||||
@@ -8,39 +8,11 @@
|
|||||||
<script src="js/element-plus2.9.4.js"></script>
|
<script src="js/element-plus2.9.4.js"></script>
|
||||||
<script src="js/icons-vue2.3.1.js"></script>
|
<script src="js/icons-vue2.3.1.js"></script>
|
||||||
<script src="js/common.js"></script>
|
<script src="js/common.js"></script>
|
||||||
|
<link rel="icon" href="images/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="css/common.css">
|
||||||
<title>小智-server</title>
|
<title>小智-server</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
/* 页面特有样式 */
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(135deg, #1a1f25 0%, #0d1117 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: rgba(13, 17, 23, 0.8);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
color: #fff;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
@@ -56,15 +28,6 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.role-id {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-description {
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character-count {
|
.character-count {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: #999;
|
color: #999;
|
||||||
@@ -81,35 +44,33 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 20px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="animated-bg"></div>
|
<div class="animated-bg"></div>
|
||||||
<div class="header">
|
<app-header>xiaozhi-esp32-server</app-header>
|
||||||
<div class="header-logo">AI</div>
|
|
||||||
xiaozhi-esp32-server
|
<div class="user-menu">
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="32" src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"/>
|
||||||
|
<span class="user-name">管理员</span>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="logout">退出</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<div class="role-title">配置角色:</div>
|
|
||||||
<div class="role-id">cc:ba:97:11:a6:ac</div>
|
|
||||||
|
|
||||||
<div class="role-description">角色介绍</div>
|
<div class="role-description">角色介绍</div>
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
v-model="form.prompt"
|
v-model="form.prompt"
|
||||||
:rows="6"
|
:rows="10"
|
||||||
resize="vertical"
|
resize="vertical"
|
||||||
@input="updateCharCount"
|
@input="updateCharCount"
|
||||||
></el-input>
|
></el-input>
|
||||||
@@ -117,15 +78,13 @@
|
|||||||
|
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<el-button type="primary" @click="savePrompt">保存配置</el-button>
|
<el-button type="primary" @click="savePrompt">保存配置</el-button>
|
||||||
<el-button @click="resetPrompt">重置</el-button>
|
<el-button @click="loadPrompt">重置</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="note">注意:保存配置后,需要重启设备,新的配置才会生效。</div>
|
<div class="note">注意:保存配置后,需要重启设备,新的配置才会生效。</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer">
|
<app-footer></app-footer>
|
||||||
© 2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -173,14 +132,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
// 新增退出处理方法
|
||||||
|
handleCommand(command) {
|
||||||
|
if (command === 'logout') {
|
||||||
|
this.handleLogout();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLogout() {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
location.href = 'login.html';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.loadPrompt();
|
this.loadPrompt();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const app = Vue.createApp(App);
|
|
||||||
app.use(ElementPlus);
|
const app = createVueApp(App);
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -37,4 +37,32 @@ function get(api, fn) {
|
|||||||
}
|
}
|
||||||
fn(data);
|
fn(data);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册全局组件
|
||||||
|
const AppHeader = {
|
||||||
|
template: `
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="header-logo">AI</div>
|
||||||
|
<slot>xiaozhi-esp32-server</slot>
|
||||||
|
</header>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppFooter = {
|
||||||
|
template: `
|
||||||
|
<footer class="app-footer">
|
||||||
|
<slot>© 2025 xiaozhi-esp32-server</slot>
|
||||||
|
</footer>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Vue应用的通用配置
|
||||||
|
function createVueApp(options) {
|
||||||
|
const app = Vue.createApp({
|
||||||
|
...options,
|
||||||
|
components: { AppHeader, AppFooter }
|
||||||
|
});
|
||||||
|
app.use(ElementPlus);
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
+12
-80
@@ -1,46 +1,18 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="zh">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="icon" href="images/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||||
<script src="js/vue3.5.13.js"></script>
|
<script src="js/vue3.5.13.js"></script>
|
||||||
<link rel="stylesheet" href="css/element-plus2.9.4.css">
|
<link rel="stylesheet" href="css/element-plus2.9.4.css">
|
||||||
<script src="js/element-plus2.9.4.js"></script>
|
<script src="js/element-plus2.9.4.js"></script>
|
||||||
<script src="js/icons-vue2.3.1.js"></script>
|
<script src="js/icons-vue2.3.1.js"></script>
|
||||||
<script src="js/common.js"></script>
|
<script src="js/common.js"></script>
|
||||||
|
<link rel="stylesheet" href="css/common.css">
|
||||||
<title>小智-server</title>
|
<title>小智-server</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
/* 页面特有样式 */
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(135deg, #1a1f25 0%, #0d1117 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: rgba(13, 17, 23, 0.8);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
color: #fff;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-logo {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
.login-container {
|
||||||
max-width: 440px;
|
max-width: 440px;
|
||||||
margin: 60px auto;
|
margin: 60px auto;
|
||||||
@@ -60,10 +32,6 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-input :deep(.el-input__wrapper) {
|
.custom-input :deep(.el-input__wrapper) {
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||||
@@ -88,49 +56,13 @@
|
|||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 15px rgba(0, 198, 251, 0.3) !important;
|
box-shadow: 0 4px 15px rgba(0, 198, 251, 0.3) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 20px;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gradient {
|
|
||||||
0% {
|
|
||||||
background-position: 0% 50%
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
background-position: 100% 50%
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 0% 50%
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animated-bg {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
|
||||||
background: linear-gradient(-45deg, #1a1f25, #0d1117, #162030, #1c1c1c);
|
|
||||||
background-size: 400% 400%;
|
|
||||||
animation: gradient 15s ease infinite;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div class="animated-bg"></div>
|
<div class="animated-bg"></div>
|
||||||
<div class="header">
|
<app-header>xiaozhi-esp32-server</app-header>
|
||||||
<div class="header-logo">AI</div>
|
|
||||||
xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<h2 class="login-title">安全验证</h2>
|
<h2 class="login-title">安全验证</h2>
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
@@ -152,10 +84,10 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
|
||||||
© 2025 xiaozhi-esp32-server
|
<app-footer></app-footer>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const {ElMessage} = ElementPlus;
|
const {ElMessage} = ElementPlus;
|
||||||
const App = {
|
const App = {
|
||||||
@@ -198,8 +130,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const app = Vue.createApp(App);
|
|
||||||
app.use(ElementPlus);
|
const app = createVueApp(App);
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+3
-1
@@ -12,4 +12,6 @@ google-generativeai==0.8.4
|
|||||||
edge_tts==7.0.0
|
edge_tts==7.0.0
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
aiohttp==3.9.3
|
aiohttp==3.9.3
|
||||||
aiohttp_cors==0.7.0
|
aiohttp_cors==0.7.0
|
||||||
|
ormsgpack==1.7.0
|
||||||
|
ruamel.yaml==0.18.10
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
<!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