update:server连接api (#747)

* update:server连接manager-api

* update:读取智能体模型配置

* update:添加默认模型的按钮

* update:优化配置读取方式

* update:server兼容manager接口改造

* update:优化私有配置加载

* update:加载私有模型配置
This commit is contained in:
hrz
2025-04-12 17:36:04 +08:00
committed by GitHub
parent c39ad97b8e
commit 5d69ba0796
57 changed files with 1618 additions and 1066 deletions
+178
View File
@@ -0,0 +1,178 @@
import os
import argparse
import requests
import yaml
import time
# 添加全局配置缓存
_config_cache = None
def get_project_dir():
"""获取项目根目录"""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/"
def read_config(config_path):
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
def load_config():
"""加载配置文件"""
global _config_cache
if _config_cache is not None:
return _config_cache
parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file()
parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args()
config = read_config(args.config_path)
if config.get("manager-api", {}).get("url"):
config = get_config_from_api(config)
# 初始化目录
ensure_directories(config)
_config_cache = config
return config
def get_config_file():
"""获取配置文件路径,优先使用私有配置文件(若存在)。
Returns:
str: 配置文件路径(相对路径或默认路径)
"""
default_config_file = "config.yaml"
config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file
return config_file
def _make_api_request(api_url, secret, endpoint, json_data=None):
"""执行API请求的通用函数
Args:
api_url: API的基础URL
secret: API密钥
endpoint: API端点
json_data: 请求的JSON数据
Returns:
dict: API返回的数据
Raises:
Exception: 当请求失败时抛出异常
"""
if not api_url or not secret:
raise Exception("manager-api的url或secret配置错误")
if "" in secret:
raise Exception("请先配置manager-api的secret")
max_retries = 10
retry_delay = 2 # 秒
for attempt in range(max_retries):
try:
response = requests.post(f"{api_url}{endpoint}", json=json_data)
if response.status_code == 200:
result = response.json()
if result.get("code") != 0:
raise Exception(f"API返回错误: {result.get('msg', '未知错误')}")
return result.get("data")
error_msg = f"manager-api请求失败,状态码: {response.status_code}"
try:
error_data = response.json()
if "msg" in error_data:
error_msg = f"{error_msg}, 错误信息: {error_data['msg']}"
except:
error_msg = f"{error_msg}, 响应内容: {response.text}"
if attempt < max_retries - 1:
print(f"请求manager-api失败,正在重试 ({attempt + 1}/{max_retries})...")
time.sleep(retry_delay)
else:
raise Exception(error_msg)
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
print(f"请求manager-api异常,正在重试 ({attempt + 1}/{max_retries})...")
time.sleep(retry_delay)
else:
raise Exception(f"manager-api请求异常: {str(e)}")
def get_config_from_api(config):
"""从Java API获取配置"""
api_url = config["manager-api"].get("url", "")
secret = config["manager-api"].get("secret", "")
config_data = _make_api_request(
api_url, secret, "/config/server-base", {"secret": secret}
)
config_data["read_config_from_api"] = True
config_data["manager-api"] = {
"url": api_url,
"secret": secret,
}
return config_data
def get_private_config_from_api(config, device_id, client_id):
"""从Java API获取私有配置"""
api_url = config["manager-api"].get("url", "")
secret = config["manager-api"].get("secret", "")
return _make_api_request(
api_url,
secret,
"/config/agent-models",
{
"secret": secret,
"macAddress": device_id,
"clientId": client_id,
"selectedModule": config["selected_module"],
},
)
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get("log", {}).get("log_dir", "tmp")
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ["ASR", "TTS"]:
for provider in config.get(module, {}).values():
output_dir = provider.get("output_dir", "")
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get("selected_module", {})
for module_type in ["ASR", "LLM", "TTS"]:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get("output_dir")
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
+21 -6
View File
@@ -1,11 +1,30 @@
import os
import sys
from loguru import logger
from config.settings import load_config
from config.config_loader import load_config
SERVER_VERSION = "0.2.1"
def get_module_abbreviation(module_name, module_dict):
"""获取模块名称的缩写,如果为空则返回00"""
return (
module_dict.get(module_name, "")[:2] if module_dict.get(module_name) else "00"
)
def build_module_string(selected_module):
"""构建模块字符串"""
return (
get_module_abbreviation("VAD", selected_module)
+ get_module_abbreviation("ASR", selected_module)
+ get_module_abbreviation("LLM", selected_module)
+ get_module_abbreviation("TTS", selected_module)
+ get_module_abbreviation("Memory", selected_module)
+ get_module_abbreviation("Intent", selected_module)
)
def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config()
@@ -18,11 +37,7 @@ def setup_logging():
"log_format_file",
"{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}",
)
selected_module = config.get("selected_module")
selected_module_str = "".join(
[value[0] + value[1] for key, value in selected_module.items()]
)
selected_module_str = build_module_string(config.get("selected_module", {}))
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
@@ -1,241 +0,0 @@
import os
import time
import yaml
from config.logger import setup_logging
from typing import Dict, Any, Optional
from copy import deepcopy
from core.utils.util import get_project_dir
from core.utils import llm, tts
from core.utils.lock_manager import FileLockManager
TAG = __name__
class PrivateConfig:
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 = setup_logging()
self.private_config = {}
self.auth_code_gen = auth_code_gen
self.lock_manager = FileLockManager()
async def load_or_create(self):
try:
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']
# 生成认证码
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)
self.private_config = all_configs[self.device_id]
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error handling private config: {e}")
self.private_config = {}
async def update_config(self, selected_modules: Dict[str, str], prompt: str, nickname: str) -> bool:
"""更新设备配置
Args:
selected_modules: 选择的模块配置,格式如 {'LLM': 'AliLLM', 'TTS': 'EdgeTTS',...}
prompt: 提示词配置
Returns:
bool: 更新是否成功
"""
try:
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']
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]
}
# 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
# 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
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error updating config: {e}")
return False
async def delete_config(self) -> bool:
"""删除设备配置
Returns:
bool: 删除是否成功
"""
try:
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]
# 保存更新后的配置
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
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error deleting config: {e}")
return False
def create_private_instances(self):
# 判断存在私有配置,并且self.device_id在私有配置中
if not self.private_config:
self.logger.bind(tag=TAG).error("Private config not found for device_id: {}", self.device_id)
return None, None
"""创建私有处理模块实例"""
config = self.private_config
selected_modules = config['selected_module']
return (
llm.create_instance(
selected_modules["LLM"]
if not 'type' in config["LLM"][selected_modules["LLM"]]
else
config["LLM"][selected_modules["LLM"]]['type'],
config["LLM"][selected_modules["LLM"]],
),
tts.create_instance(
selected_modules["TTS"]
if not 'type' in config["TTS"][selected_modules["TTS"]]
else
config["TTS"][selected_modules["TTS"]]["type"],
config["TTS"][selected_modules["TTS"]],
self.default_config.get("delete_audio", True) # Using default_config for global settings
)
)
async def update_last_chat_time(self, timestamp=None):
"""更新设备最近一次的聊天时间
Args:
timestamp: 指定的时间戳,不传则使用当前时间
"""
if not self.private_config:
self.logger.bind(tag=TAG).error("Private config not found")
return False
try:
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
# 读取所有配置
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
# 保存回文件
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.bind(tag=TAG).error(f"Error updating last chat time: {e}")
return False
def get_auth_code(self) -> str:
"""获取设备的认证码
Returns:
str: 认证码,如果没有返回空字符串
"""
return self.private_config.get('auth_code', '')
def get_owner(self) -> Optional[str]:
"""获取设备当前所有者"""
return self.private_config.get('owner')
+11 -82
View File
@@ -1,82 +1,11 @@
import os
import argparse
from ruamel.yaml import YAML
from collections.abc import Mapping
from core.utils.util import read_config, get_project_dir
from config.config_loader import read_config, get_project_dir, load_config
default_config_file = "config.yaml"
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get('log', {}).get('log_dir', 'tmp')
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ['ASR', 'TTS']:
for provider in config.get(module, {}).values():
output_dir = provider.get('output_dir', '')
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get('selected_module', {})
for module_type in ['ASR', 'LLM', 'TTS']:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get('output_dir')
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
def get_config_file():
global default_config_file
"""获取配置文件路径,优先使用私有配置文件(若存在)。
Returns:
str: 配置文件路径(相对路径或默认路径)
"""
config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file
return config_file
def load_config():
"""加载配置文件"""
parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file()
parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args()
config = read_config(args.config_path)
# 初始化目录
ensure_directories(config)
return config
def update_config(config):
yaml = YAML()
yaml.preserve_quotes = True
"""将配置保存到YAML文件"""
with open(get_config_file(), 'w') as f:
yaml.dump(config, f)
def find_missing_keys(new_config, old_config, parent_key=''):
def find_missing_keys(new_config, old_config, parent_key=""):
"""
递归查找缺失的配置项
返回格式:[缺失配置路径]
@@ -98,28 +27,28 @@ def find_missing_keys(new_config, old_config, parent_key=''):
# 递归检查嵌套字典
if isinstance(value, Mapping):
sub_missing = find_missing_keys(
value,
old_config[key],
parent_key=full_path
value, old_config[key], parent_key=full_path
)
missing_keys.extend(sub_missing)
return missing_keys
def check_config_file():
old_config_file = get_config_file()
global default_config_file
if not 'data' in old_config_file:
old_config_file = get_project_dir() + "data/." + default_config_file
if not os.path.exists(old_config_file):
return
old_config = read_config(get_project_dir() + old_config_file)
old_config = load_config()
new_config = read_config(get_project_dir() + default_config_file)
# 查找缺失的配置项
missing_keys = find_missing_keys(new_config, old_config)
read_config_from_api = old_config.get("read_config_from_api", False)
if read_config_from_api:
return
if missing_keys:
missing_keys_str = "\n".join(f"- {key}" for key in missing_keys)
error_msg = "您的配置文件太旧了,缺少了:\n"
error_msg += "\n".join(f"- {key}" for key in missing_keys)
error_msg += missing_keys_str
error_msg += "\n建议您:\n"
error_msg += "1、备份data/.config.yaml文件\n"
error_msg += "2、将根目录的config.yaml文件复制到data下,重命名为.config.yaml\n"