From 2acee60d7361f100622e4dbc25a99b7bdb0b810d Mon Sep 17 00:00:00 2001
From: kalicyh <34980061+kaliCYH@users.noreply.github.com>
Date: Tue, 18 Feb 2025 00:07:19 +0800
Subject: [PATCH] =?UTF-8?q?=F0=9F=A6=84=20refactor(log):=20colorful=20log?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ZhiKongTaiWeb/package.json | 1 +
ZhiKongTaiWeb/yarn.lock | 160 ++++++++++++++++++++++++++++
app.py | 8 +-
config/logger.py | 33 +++---
config/private_config.py | 28 ++---
core/auth.py | 10 +-
core/connection.py | 58 +++++-----
core/handle/abortHandle.py | 9 +-
core/handle/audioHandle.py | 14 +--
core/handle/helloHandle.py | 4 +-
core/handle/textHandle.py | 9 +-
core/providers/llm/coze/coze.py | 17 +--
core/providers/llm/dify/dify.py | 8 +-
core/providers/llm/gemini/gemini.py | 11 +-
core/providers/llm/ollama/ollama.py | 7 +-
core/providers/llm/openai/openai.py | 9 +-
core/providers/tts/base.py | 11 +-
core/providers/tts/gpt_sovits_v2.py | 8 +-
core/utils/asr.py | 18 ++--
core/utils/llm.py | 4 +-
core/utils/lock_manager.py | 11 +-
core/utils/tts.py | 4 +-
core/utils/vad.py | 12 +--
core/websocket_server.py | 10 +-
manager/api/config.py | 23 ++--
manager/api/login.py | 11 +-
manager/api/prompt.py | 8 +-
manager/api/register.py | 14 +--
manager/api/user_manager.py | 29 ++---
manager/http_server.py | 14 ++-
poetry.lock | 49 ++++++++-
pyproject.toml | 1 +
32 files changed, 423 insertions(+), 190 deletions(-)
diff --git a/ZhiKongTaiWeb/package.json b/ZhiKongTaiWeb/package.json
index f67f2ffe..7b208585 100644
--- a/ZhiKongTaiWeb/package.json
+++ b/ZhiKongTaiWeb/package.json
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.7.9",
"vue": "^3.5.10",
"vue-router": "^4.0.0"
},
diff --git a/ZhiKongTaiWeb/yarn.lock b/ZhiKongTaiWeb/yarn.lock
index 593b431c..32182378 100644
--- a/ZhiKongTaiWeb/yarn.lock
+++ b/ZhiKongTaiWeb/yarn.lock
@@ -337,16 +337,86 @@
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f"
integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.7.9:
+ version "1.7.9"
+ resolved "https://registry.npmmirror.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
+ integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.0"
+ proxy-from-env "^1.1.0"
+
+call-bind-apply-helpers@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
csstype@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
entities@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+ integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+ dependencies:
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
esbuild@^0.21.3:
version "0.21.5"
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d"
@@ -381,11 +451,79 @@ estree-walker@^2.0.2:
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+follow-redirects@^1.15.6:
+ version "1.15.9"
+ resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
+ integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
+
+form-data@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
+ integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ es-set-tostringtag "^2.1.0"
+ mime-types "^2.1.12"
+
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.2.6:
+ version "1.2.7"
+ resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.7.tgz#dcfcb33d3272e15f445d15124bc0a216189b9044"
+ integrity sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.0.0"
+ function-bind "^1.1.2"
+ get-proto "^1.0.0"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
magic-string@^0.30.11:
version "0.30.17"
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453"
@@ -393,6 +531,23 @@ magic-string@^0.30.11:
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
nanoid@^3.3.8:
version "3.3.8"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
@@ -412,6 +567,11 @@ postcss@^8.4.43, postcss@^8.4.48:
picocolors "^1.1.1"
source-map-js "^1.2.1"
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
rollup@^4.20.0:
version "4.34.7"
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.34.7.tgz#e00d8550688a616a3481c6446bb688d4c753ba8f"
diff --git a/app.py b/app.py
index e7949885..f2288840 100644
--- a/app.py
+++ b/app.py
@@ -6,8 +6,10 @@ from manager.http_server import WebUI
from aiohttp import web
from core.utils.util import get_local_ip
+TAG = __name__
+
async def main():
- setup_logging() # 最先初始化日志
+ logger = setup_logging()
config = load_config()
# 启动 WebSocket 服务器
@@ -28,9 +30,9 @@ async def main():
await site.start()
webui_runner = runner
local_ip = get_local_ip()
- print(f"WebUI server is running at http://{local_ip}:{port}")
+ logger.bind(tag=TAG).info(f"WebUI server is running at http://{local_ip}:{port}")
except Exception as e:
- print(f"Failed to start WebUI server: {e}")
+ logger.bind(tag=TAG).error(f"Failed to start WebUI server: {e}")
try:
# 等待 WebSocket 服务器运行
diff --git a/config/logger.py b/config/logger.py
index c3b82992..d4e87bc6 100644
--- a/config/logger.py
+++ b/config/logger.py
@@ -1,20 +1,27 @@
-import logging
-import sys
import os
-
+import sys
+from loguru import logger
def setup_logging(log_dir='tmp', data_dir='data'):
- """配置全局日志"""
+ """配置全局彩色日志(不同区块不同标签)"""
os.makedirs(log_dir, exist_ok=True)
os.makedirs(data_dir, exist_ok=True)
- logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
- handlers=[
- logging.StreamHandler(sys.stdout),
- logging.FileHandler(os.path.join(log_dir, "server.log"), encoding='utf-8')
- ],
- force=True
+ # 设置日志格式,时间、日志级别、标签、消息
+ log_format = (
+ "{time:YYYY-MM-DD HH:mm:ss}"
+ "[{extra[tag]}]"
+ " - {level} - "
+ "{message}"
)
- return logging.getLogger(__name__)
+
+ # 配置日志输出
+ logger.remove()
+
+ # 输出到控制台
+ logger.add(sys.stdout, format=log_format, level="INFO")
+
+ # 输出到文件
+ logger.add(os.path.join(log_dir, "server.log"), format="{time:YYYY-MM-DD HH:mm:ss} - {name} - {level} - {extra[tag]} - {message}", level="INFO")
+
+ return logger
diff --git a/config/private_config.py b/config/private_config.py
index e5839b46..9b925e1e 100644
--- a/config/private_config.py
+++ b/config/private_config.py
@@ -1,7 +1,7 @@
import os
import time
import yaml
-import logging
+from config.logger import setup_logging
from typing import Dict, Any, Optional
from copy import deepcopy
from core.utils.util import get_project_dir
@@ -9,12 +9,14 @@ from core.utils import asr, vad, llm, tts
from manager.api.user_manager import UserManager
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 = logging.getLogger(__name__)
+ self.logger = setup_logging()
self.private_config = {}
self.auth_code_gen = auth_code_gen
self.user_manager = UserManager()
@@ -74,7 +76,7 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error handling private config: {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:
@@ -129,7 +131,7 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error updating config: {e}")
+ self.logger.bind(tag=TAG).error(f"Error updating config: {e}")
return False
async def delete_config(self) -> bool:
@@ -163,13 +165,13 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error deleting config: {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:
- logging.error("Private config not found for device_id: %s", self.device_id)
+ self.logger.bind(tag=TAG).error("Private config not found for device_id: {}", self.device_id)
return None, None, None, None
"""创建私有处理模块实例"""
@@ -208,7 +210,7 @@ class PrivateConfig:
timestamp: 指定的时间戳,不传则使用当前时间
"""
if not self.private_config:
- self.logger.error("Private config not found")
+ self.logger.bind(tag=TAG).error("Private config not found")
return False
try:
@@ -235,7 +237,7 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error updating last chat time: {e}")
+ self.logger.bind(tag=TAG).error(f"Error updating last chat time: {e}")
return False
def get_auth_code(self) -> str:
@@ -252,7 +254,7 @@ class PrivateConfig:
try:
# 检查用户是否存在
if not self.user_manager.get_user(username):
- self.logger.error(f"User {username} not found")
+ self.logger.bind(tag=TAG).error(f"User {username} not found")
return False
# 读取所有配置
@@ -260,12 +262,12 @@ class PrivateConfig:
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")
+ self.logger.bind(tag=TAG).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}")
+ self.logger.bind(tag=TAG).info(f"Binding user {username} to device {self.device_id}")
if auth_code:
del all_configs[self.device_id]['auth_code']
@@ -293,7 +295,7 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error binding user: {e}")
+ self.logger.bind(tag=TAG).error(f"Error binding user: {e}")
return False
async def unbind_user(self) -> bool:
@@ -330,7 +332,7 @@ class PrivateConfig:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
- self.logger.error(f"Error unbinding user: {e}")
+ self.logger.bind(tag=TAG).error(f"Error unbinding user: {e}")
return False
def get_owner(self) -> Optional[str]:
diff --git a/core/auth.py b/core/auth.py
index dcf994eb..8aaa4f00 100755
--- a/core/auth.py
+++ b/core/auth.py
@@ -1,6 +1,6 @@
-import logging
+from config.logger import setup_logging
-logger = logging.getLogger(__name__)
+TAG = __name__
class AuthenticationError(Exception):
@@ -37,15 +37,15 @@ class AuthMiddleware:
# 验证Authorization header
auth_header = headers.get("authorization", "")
if not auth_header.startswith("Bearer "):
- logger.error("Missing or invalid Authorization header")
+ logger.bind(tag=TAG).error("Missing or invalid Authorization header")
raise AuthenticationError("Missing or invalid Authorization header")
token = auth_header.split(" ")[1]
if token not in self.tokens:
- logger.error(f"Invalid token: {token}")
+ logger.bind(tag=TAG).error(f"Invalid token: {token}")
raise AuthenticationError("Invalid token")
- logger.info(f"Authentication successful - Device: {device_id}, Token: {self.tokens[token]}")
+ logger.bind(tag=TAG).info(f"Authentication successful - Device: {device_id}, Token: {self.tokens[token]}")
return True
def get_token_name(self, token):
diff --git a/core/connection.py b/core/connection.py
index e8b42340..ab5d3957 100644
--- a/core/connection.py
+++ b/core/connection.py
@@ -4,7 +4,7 @@ import uuid
import time
import queue
import asyncio
-import logging
+from config.logger import setup_logging
import threading
import websockets
from typing import Dict, Any
@@ -19,10 +19,12 @@ from config.private_config import PrivateConfig
from core.auth import AuthMiddleware, AuthenticationError
from core.utils.auth_code_gen import AuthCodeGenerator # 添加导入
+TAG = __name__
+
class ConnectionHandler:
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
self.config = config
- self.logger = logging.getLogger(__name__)
+ self.logger = setup_logging()
self.auth = AuthMiddleware(config)
self.websocket = None
@@ -85,7 +87,7 @@ class ConnectionHandler:
try:
# 获取并验证headers
self.headers = dict(ws.request.headers)
- self.logger.info(f"New connection request - Headers: {self.headers}")
+ self.logger.bind(tag=TAG).info(f"New connection request - Headers: {self.headers}")
# 进行认证
await self.auth.authenticate(self.headers)
@@ -94,7 +96,7 @@ class ConnectionHandler:
# Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False)
- logging.info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
+ self.info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
if bUsePrivateConfig and device_id:
try:
self.private_config = PrivateConfig(device_id, self.config, self.auth_code_gen)
@@ -112,12 +114,12 @@ class ConnectionHandler:
self.asr = asr
self.llm = llm
self.tts = tts
- self.logger.info(f"Loaded private config and instances for device {device_id}")
+ self.logger.bind(tag=TAG).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.logger.bind(tag=TAG).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.logger.bind(tag=TAG).error(f"Error initializing private config: {e}")
self.private_config = None
raise
@@ -138,15 +140,15 @@ class ConnectionHandler:
async for message in self.websocket:
await self._route_message(message)
except websockets.exceptions.ConnectionClosed:
- self.logger.info("客户端断开连接")
+ self.logger.bind(tag=TAG).info("客户端断开连接")
await self.close()
except AuthenticationError as e:
- self.logger.error(f"Authentication failed: {str(e)}")
+ self.logger.bind(tag=TAG).error(f"Authentication failed: {str(e)}")
await ws.close()
return
except Exception as e:
- self.logger.error(f"Connection error: {str(e)}")
+ self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}")
await ws.close()
return
@@ -208,7 +210,7 @@ class ConnectionHandler:
start_time = time.time() # 记录开始时间
llm_responses = self.llm.response(self.session_id, self.dialogue.get_llm_dialogue())
except Exception as e:
- self.logger.error(f"LLM 处理出错 {query}: {e}")
+ self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
return None
# 提交 TTS 任务到线程池
self.llm_finish_task = False
@@ -220,7 +222,7 @@ class ConnectionHandler:
break
end_time = time.time() # 记录结束时间
- self.logger.debug(f"大模型返回时间时间: {end_time - start_time} 秒, 生成token={content}")
+ self.logger.bind(tag=TAG).debug(f"大模型返回时间时间: {end_time - start_time} 秒, 生成token={content}")
if is_segment(response_message):
segment_text = "".join(response_message[start:])
segment_text = get_string_no_punctuation_or_emoji(segment_text)
@@ -241,7 +243,7 @@ class ConnectionHandler:
self.llm_finish_task = True
# 更新对话
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
- self.logger.debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
+ self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
def _priority_thread(self):
@@ -253,25 +255,25 @@ class ConnectionHandler:
continue
text = None
try:
- self.logger.debug("正在处理TTS任务...")
+ self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
tts_file, text = future.result(timeout=10)
if text is None or len(text) <= 0:
continue
if tts_file is None:
- self.logger.error(f"TTS文件生成失败: {text}")
+ self.logger.bind(tag=TAG).error(f"TTS文件生成失败: {text}")
continue
- self.logger.debug(f"TTS文件生成完毕,文件路径: {tts_file}")
+ self.logger.bind(tag=TAG).debug(f"TTS文件生成完毕,文件路径: {tts_file}")
if os.path.exists(tts_file):
opus_datas, duration = self.tts.wav_to_opus_data(tts_file)
else:
- self.logger.error(f"TTS文件不存在: {tts_file}")
+ self.logger.bind(tag=TAG).error(f"TTS文件不存在: {tts_file}")
opus_datas = []
duration = 0
except TimeoutError:
- self.logger.error("TTS 任务超时")
+ self.logger.bind(tag=TAG).error("TTS 任务超时")
continue
except Exception as e:
- self.logger.error(f"TTS 任务出错: {e}")
+ self.logger.bind(tag=TAG).error(f"TTS 任务出错: {e}")
continue
if not self.client_abort:
# 如果没有中途打断就发送语音
@@ -281,27 +283,27 @@ class ConnectionHandler:
if self.tts.delete_audio_file and os.path.exists(tts_file):
os.remove(tts_file)
except Exception as e:
- self.logger.error(f"TTS任务处理错误: {e}")
+ self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
self.clearSpeakStatus()
asyncio.run_coroutine_threadsafe(
self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})),
self.loop
)
- self.logger.error(f"tts_priority priority_thread: {text}{e}")
+ self.logger.bind(tag=TAG).error(f"tts_priority priority_thread: {text}{e}")
def speak_and_play(self, text):
if text is None or len(text) <= 0:
- self.logger.info(f"无需tts转换,query为空,{text}")
+ self.logger.bind(tag=TAG).info(f"无需tts转换,query为空,{text}")
return None, text
tts_file = self.tts.to_tts(text)
if tts_file is None:
- self.logger.error(f"tts转换失败,{text}")
+ self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
return None, text
- self.logger.debug(f"TTS 文件生成完毕: {tts_file}")
+ self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
return tts_file, text
def clearSpeakStatus(self):
- self.logger.debug(f"清除服务端讲话状态")
+ self.logger.bind(tag=TAG).debug(f"清除服务端讲话状态")
self.asr_server_receive = True
self.tts_last_text = None
self.tts_first_text = None
@@ -310,7 +312,7 @@ class ConnectionHandler:
def recode_first_last_text(self, text):
if not self.tts_first_text:
- self.logger.info(f"大模型说出第一句话: {text}")
+ self.logger.bind(tag=TAG).info(f"大模型说出第一句话: {text}")
self.tts_first_text = text
self.tts_last_text = text
@@ -320,14 +322,14 @@ class ConnectionHandler:
self.executor.shutdown(wait=False)
if self.websocket:
await self.websocket.close()
- self.logger.info("连接资源已释放")
+ self.logger.bind(tag=TAG).info("连接资源已释放")
def reset_vad_states(self):
self.client_audio_buffer = bytes()
self.client_have_voice = False
self.client_have_voice_last_time = 0
self.client_voice_stop = False
- self.logger.debug("VAD states reset.")
+ self.logger.bind(tag=TAG).debug("VAD states reset.")
def stop_all_tasks(self):
while self.scheduled_tasks:
diff --git a/core/handle/abortHandle.py b/core/handle/abortHandle.py
index 78549797..12d10ce4 100644
--- a/core/handle/abortHandle.py
+++ b/core/handle/abortHandle.py
@@ -1,11 +1,12 @@
import json
-import logging
+from config.logger import setup_logging
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
async def handleAbortMessage(conn):
- logger.info("Abort message received")
+ logger.bind(tag=TAG).info("Abort message received")
# 设置成打断状态,会自动打断llm、tts任务
conn.client_abort = True
# 打断屏显任务
@@ -13,4 +14,4 @@ async def handleAbortMessage(conn):
# 打断客户端说话状态
await conn.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": conn.session_id}))
conn.clearSpeakStatus()
- logger.info("Abort message received-end")
+ logger.bind(tag=TAG).info("Abort message received-end")
diff --git a/core/handle/audioHandle.py b/core/handle/audioHandle.py
index c301e698..8d8d37ad 100644
--- a/core/handle/audioHandle.py
+++ b/core/handle/audioHandle.py
@@ -1,15 +1,17 @@
-import logging
+from config.logger import setup_logging
import json
import asyncio
import time
from core.utils.util import remove_punctuation_and_length, get_string_no_punctuation_or_emoji
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
async def handleAudioMessage(conn, audio):
if not conn.asr_server_receive:
- logger.debug(f"前期数据处理中,暂停接收")
+ logger = setup_logging()
+ logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
return
if conn.client_listen_mode == "auto":
have_voice = conn.vad.is_vad(conn, audio)
@@ -28,7 +30,7 @@ async def handleAudioMessage(conn, audio):
conn.client_abort = False
conn.asr_server_receive = False
text, file_path = conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
- logger.info(f"识别文本: {text}")
+ logger.bind(tag=TAG).info(f"识别文本: {text}")
text_len, text_without_punctuation = remove_punctuation_and_length(text)
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
return
@@ -44,7 +46,7 @@ async def handleCMDMessage(conn, text):
cmd_exit = conn.cmd_exit
for cmd in cmd_exit:
if text == cmd:
- logger.info("识别到明确的退出命令".format(text))
+ logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
await finishToChat(conn)
return True
return False
@@ -80,7 +82,7 @@ async def sendAudioMessage(conn, audios, duration, text):
# 发送 tts.start
if text == conn.tts_first_text:
- logger.info(f"发送第一段语音: {text}")
+ logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
conn.tts_start_speak_time = time.time()
# 发送 sentence_start(每个音频文件之前发送一次)
diff --git a/core/handle/helloHandle.py b/core/handle/helloHandle.py
index 1cd46987..bafcf09d 100644
--- a/core/handle/helloHandle.py
+++ b/core/handle/helloHandle.py
@@ -1,7 +1,7 @@
import json
-import logging
+from config.logger import setup_logging
-logger = logging.getLogger(__name__)
+logger = setup_logging()
async def handleHelloMessage(conn):
diff --git a/core/handle/textHandle.py b/core/handle/textHandle.py
index 23da0fbd..9a6faaf0 100644
--- a/core/handle/textHandle.py
+++ b/core/handle/textHandle.py
@@ -1,15 +1,16 @@
-import logging
+from config.logger import setup_logging
import json
from core.handle.abortHandle import handleAbortMessage
from core.handle.helloHandle import handleHelloMessage
from core.handle.audioHandle import startToChat
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
async def handleTextMessage(conn, message):
"""处理文本消息"""
- logger.info(f"收到文本消息:{message}")
+ logger.bind(tag=TAG).info(f"收到文本消息:{message}")
try:
msg_json = json.loads(message)
if isinstance(msg_json, int):
@@ -22,7 +23,7 @@ async def handleTextMessage(conn, message):
elif msg_json["type"] == "listen":
if "mode" in msg_json:
conn.client_listen_mode = msg_json["mode"]
- logger.debug(f"客户端拾音模式:{conn.client_listen_mode}")
+ logger.bind(tag=TAG).debug(f"客户端拾音模式:{conn.client_listen_mode}")
if msg_json["state"] == "start":
conn.client_have_voice = True
conn.client_voice_stop = False
diff --git a/core/providers/llm/coze/coze.py b/core/providers/llm/coze/coze.py
index 4b236572..8abdfbc5 100644
--- a/core/providers/llm/coze/coze.py
+++ b/core/providers/llm/coze/coze.py
@@ -1,10 +1,11 @@
-import logging
+from config.logger import setup_logging
import requests
import json
import re
from core.providers.llm.base import LLMProviderBase
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
# 定义用于匹配中文标点符号的正则表达式(包括句号、感叹号、问号、分号)
punctuation_pattern = re.compile(r'([。!?;])')
@@ -27,7 +28,7 @@ class LLMProvider(LLMProviderBase):
"query": last_msg["content"],
"stream": True
}
- logger.info(f"发送到 Coze API 的请求数据: {json.dumps(data, ensure_ascii=False)}")
+ logger.bind(tag=TAG).info(f"发送到 Coze API 的请求数据: {json.dumps(data, ensure_ascii=False)}")
headers = {
'Authorization': f'Bearer {self.personal_access_token}',
@@ -43,7 +44,7 @@ class LLMProvider(LLMProviderBase):
json=data,
stream=True
)
- logger.info(f"请求状态: {response.status_code}")
+ logger.bind(tag=TAG).info(f"请求状态: {response.status_code}")
if response.status_code == 200:
# 对每一行流数据进行处理,不做跨块累积
@@ -54,7 +55,7 @@ class LLMProvider(LLMProviderBase):
# 使用 utf-8 解码,错误部分用替换符
line = line_bytes.decode('utf-8', errors='replace')
except Exception as e:
- logger.error(f"解码失败: {e}")
+ logger.bind(tag=TAG).error(f"解码失败: {e}")
continue
if line.startswith("data:"):
data_str = line[len("data:"):].strip()
@@ -63,7 +64,7 @@ class LLMProvider(LLMProviderBase):
try:
data_chunk = json.loads(data_str)
except json.JSONDecodeError as e:
- logger.error(f"JSON解析失败: {e} 数据: {line}")
+ logger.bind(tag=TAG).error(f"JSON解析失败: {e} 数据: {line}")
continue
msg = data_chunk.get("message", {})
if msg.get("role") == "assistant" and msg.get("type") == "answer":
@@ -88,8 +89,8 @@ class LLMProvider(LLMProviderBase):
if content.strip():
yield content.strip()
else:
- logger.error(f"请求失败,状态码: {response.status_code}")
+ logger.bind(tag=TAG).error(f"请求失败,状态码: {response.status_code}")
yield f"【Coze服务响应异常:请求失败,状态码 {response.status_code}】"
except Exception as e:
- logger.error(f"Error in Coze response generation: {e}")
+ logger.bind(tag=TAG).error(f"Error in Coze response generation: {e}")
yield "【Coze服务响应异常】"
diff --git a/core/providers/llm/dify/dify.py b/core/providers/llm/dify/dify.py
index 48f069ee..0e9c821a 100644
--- a/core/providers/llm/dify/dify.py
+++ b/core/providers/llm/dify/dify.py
@@ -1,10 +1,10 @@
import json
-import logging
+from config.logger import setup_logging
import requests
from core.providers.llm.base import LLMProviderBase
-logger = logging.getLogger(__name__)
-
+TAG = __name__
+logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
@@ -35,5 +35,5 @@ class LLMProvider(LLMProviderBase):
yield event['answer']
except Exception as e:
- logger.error(f"Error in response generation: {e}")
+ logger.bind(tag=TAG).error(f"Error in response generation: {e}")
yield "【服务响应异常】"
diff --git a/core/providers/llm/gemini/gemini.py b/core/providers/llm/gemini/gemini.py
index ef1351cd..7ddf84fa 100644
--- a/core/providers/llm/gemini/gemini.py
+++ b/core/providers/llm/gemini/gemini.py
@@ -1,8 +1,9 @@
-import logging
+from config.logger import setup_logging
import google.generativeai as genai
from core.providers.llm.base import LLMProviderBase
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
@@ -11,7 +12,7 @@ class LLMProvider(LLMProviderBase):
self.api_key = config.get("api_key")
if not self.api_key or "你" in self.api_key:
- logger.error("你还没配置Gemini LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
+ logger.bind(tag=TAG).error("你还没配置Gemini LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
return
try:
@@ -28,7 +29,7 @@ class LLMProvider(LLMProviderBase):
}
self.chat = None
except Exception as e:
- logger.error(f"Gemini初始化失败: {e}")
+ logger.bind(tag=TAG).error(f"Gemini初始化失败: {e}")
self.model = None
def response(self, session_id, dialogue):
@@ -69,7 +70,7 @@ class LLMProvider(LLMProviderBase):
except Exception as e:
error_msg = str(e)
- logger.error(f"Gemini响应生成错误: {error_msg}")
+ logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
# 针对不同错误返回友好提示
if "Rate limit" in error_msg:
diff --git a/core/providers/llm/ollama/ollama.py b/core/providers/llm/ollama/ollama.py
index 28c377d3..d0250255 100644
--- a/core/providers/llm/ollama/ollama.py
+++ b/core/providers/llm/ollama/ollama.py
@@ -1,8 +1,9 @@
-import logging
+from config.logger import setup_logging
import requests, json
from core.providers.llm.base import LLMProviderBase
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class LLMProvider(LLMProviderBase):
@@ -41,5 +42,5 @@ class LLMProvider(LLMProviderBase):
yield json_response["response"]
except Exception as e:
- logger.error(f"Error in Ollama response generation: {e}")
+ logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
yield "【Ollama服务响应异常】"
diff --git a/core/providers/llm/openai/openai.py b/core/providers/llm/openai/openai.py
index a0e0f33f..e574d50d 100644
--- a/core/providers/llm/openai/openai.py
+++ b/core/providers/llm/openai/openai.py
@@ -1,8 +1,9 @@
-import logging
+from config.logger import setup_logging
import openai
from core.providers.llm.base import LLMProviderBase
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class LLMProvider(LLMProviderBase):
@@ -14,7 +15,7 @@ class LLMProvider(LLMProviderBase):
else:
self.base_url = config.get("url")
if "你" in self.api_key:
- logger.error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
+ logger.bind(tag=TAG).error("你还没配置LLM的密钥,请在配置文件中配置密钥,否则无法正常工作")
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, session_id, dialogue):
@@ -32,4 +33,4 @@ class LLMProvider(LLMProviderBase):
if content: # 仅在content非空时生成
yield content
except Exception as e:
- logger.error(f"Error in response generation: {e}")
+ logger.bind(tag=TAG).error(f"Error in response generation: {e}")
diff --git a/core/providers/tts/base.py b/core/providers/tts/base.py
index ba5cdbdb..54438f31 100644
--- a/core/providers/tts/base.py
+++ b/core/providers/tts/base.py
@@ -1,12 +1,13 @@
import asyncio
-import logging
+from config.logger import setup_logging
import os
import numpy as np
import opuslib
from pydub import AudioSegment
from abc import ABC, abstractmethod
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class TTSProviderBase(ABC):
@@ -26,14 +27,14 @@ class TTSProviderBase(ABC):
asyncio.run(self.text_to_speak(text, tmp_file))
if not os.path.exists(tmp_file):
max_repeat_time = max_repeat_time - 1
- logger.error(f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次")
+ logger.bind(tag=TAG).error(f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次")
if max_repeat_time > 0:
- logger.info(f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次")
+ logger.bind(tag=TAG).info(f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次")
return tmp_file
except Exception as e:
- logger.info(f"Failed to generate TTS file: {e}")
+ logger.bind(tag=TAG).info(f"Failed to generate TTS file: {e}")
return None
@abstractmethod
diff --git a/core/providers/tts/gpt_sovits_v2.py b/core/providers/tts/gpt_sovits_v2.py
index 709e5338..ae20beb6 100644
--- a/core/providers/tts/gpt_sovits_v2.py
+++ b/core/providers/tts/gpt_sovits_v2.py
@@ -3,10 +3,12 @@ import uuid
import json
import base64
import requests
-import logging
+from config.logger import setup_logging
from datetime import datetime
from core.providers.tts.base import TTSProviderBase
-logger = logging.getLogger(__name__)
+
+TAG = __name__
+logger = setup_logging()
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
@@ -62,4 +64,4 @@ class TTSProvider(TTSProviderBase):
with open(output_file, "wb") as file:
file.write(resp.content)
else:
- logger.error(f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}")
+ logger.bind(tag=TAG).error(f"GPT_SoVITS_V2 TTS请求失败: {resp.status_code} - {resp.text}")
diff --git a/core/utils/asr.py b/core/utils/asr.py
index 091df283..1c23fe55 100644
--- a/core/utils/asr.py
+++ b/core/utils/asr.py
@@ -2,7 +2,7 @@ import time
import wave
import os
from abc import ABC, abstractmethod
-import logging
+from config.logger import setup_logging
from typing import Optional, Tuple, List
import uuid
@@ -10,8 +10,8 @@ import opuslib
from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess
-logger = logging.getLogger(__name__)
-
+TAG = __name__
+logger = setup_logging()
class ASR(ABC):
@abstractmethod
@@ -55,7 +55,7 @@ class FunASR(ASR):
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
pcm_data.append(pcm_frame)
except opuslib.OpusError as e:
- logger.error(f"Opus解码错误: {e}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
with wave.open(file_path, "wb") as wf:
wf.setnchannels(1)
@@ -72,7 +72,7 @@ class FunASR(ASR):
# 保存音频文件
start_time = time.time()
file_path = self.save_audio_to_file(opus_data, session_id)
- logger.debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}")
+ logger.bind(tag=TAG).debug(f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}")
# 语音识别
start_time = time.time()
@@ -84,12 +84,12 @@ class FunASR(ASR):
batch_size_s=60,
)
text = rich_transcription_postprocess(result[0]["text"])
- logger.debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}")
+ logger.bind(tag=TAG).debug(f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}")
return text, file_path
except Exception as e:
- logger.error(f"语音识别失败: {e}", exc_info=True)
+ logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True)
return None, None
finally:
@@ -97,9 +97,9 @@ class FunASR(ASR):
if self.delete_audio_file and file_path and os.path.exists(file_path):
try:
os.remove(file_path)
- logger.debug(f"已删除临时音频文件: {file_path}")
+ logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}")
except Exception as e:
- logger.error(f"文件删除失败: {file_path} | 错误: {e}")
+ logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")
def create_instance(class_name: str, *args, **kwargs) -> ASR:
diff --git a/core/utils/llm.py b/core/utils/llm.py
index 23c7ebae..f48c103d 100644
--- a/core/utils/llm.py
+++ b/core/utils/llm.py
@@ -1,13 +1,13 @@
import os
import sys
-import logging
+from config.logger import setup_logging
import importlib
from datetime import datetime
from core.utils.util import is_segment
from core.utils.util import get_string_no_punctuation_or_emoji
from core.utils.util import read_config, get_project_dir
-logger = logging.getLogger(__name__)
+logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
diff --git a/core/utils/lock_manager.py b/core/utils/lock_manager.py
index 049a33f5..f1db6fc7 100644
--- a/core/utils/lock_manager.py
+++ b/core/utils/lock_manager.py
@@ -1,8 +1,9 @@
import asyncio
from typing import Dict
-import logging
+from config.logger import setup_logging
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class FileLockManager:
_instance = None
@@ -25,7 +26,7 @@ class FileLockManager:
"""获取锁"""
lock = cls.get_lock(file_path)
await lock.acquire()
- logger.debug(f"Acquired lock for {file_path}")
+ logger.bind(tag=TAG).debug(f"Acquired lock for {file_path}")
@classmethod
def release_lock(cls, file_path: str):
@@ -33,6 +34,6 @@ class FileLockManager:
if file_path in cls._locks:
try:
cls._locks[file_path].release()
- logger.debug(f"Released lock for {file_path}")
+ logger.bind(tag=TAG).debug(f"Released lock for {file_path}")
except RuntimeError as e:
- logger.warning(f"Failed to release lock for {file_path}: {e}")
+ logger.bind(tag=TAG).warning(f"Failed to release lock for {file_path}: {e}")
diff --git a/core/utils/tts.py b/core/utils/tts.py
index 1ffc7e23..2fc733f7 100644
--- a/core/utils/tts.py
+++ b/core/utils/tts.py
@@ -1,11 +1,11 @@
import os
import sys
-import logging
+from config.logger import setup_logging
import importlib
from datetime import datetime
from core.utils.util import read_config, get_project_dir
-logger = logging.getLogger(__name__)
+logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
diff --git a/core/utils/vad.py b/core/utils/vad.py
index 8a05dc6a..05e11eb5 100644
--- a/core/utils/vad.py
+++ b/core/utils/vad.py
@@ -1,12 +1,12 @@
from abc import ABC, abstractmethod
-import logging
+from config.logger import setup_logging
import opuslib
import time
import numpy as np
import torch
-logger = logging.getLogger(__name__)
-
+TAG = __name__
+logger = setup_logging()
class VAD(ABC):
@abstractmethod
@@ -17,7 +17,7 @@ class VAD(ABC):
class SileroVAD(VAD):
def __init__(self, config):
- logger.info("SileroVAD", config)
+ logger.bind(tag=TAG).info("SileroVAD", config)
self.model, self.utils = torch.hub.load(repo_or_dir=config["model_dir"],
source='local',
model='silero_vad',
@@ -60,9 +60,9 @@ class SileroVAD(VAD):
return client_have_voice
except opuslib.OpusError as e:
- logger.info(f"解码错误: {e}")
+ logger.bind(tag=TAG).info(f"解码错误: {e}")
except Exception as e:
- logger.error(f"Error processing audio packet: {e}")
+ logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
def create_instance(class_name, *args, **kwargs) -> VAD:
diff --git a/core/websocket_server.py b/core/websocket_server.py
index 0eb76e83..d8dce04d 100644
--- a/core/websocket_server.py
+++ b/core/websocket_server.py
@@ -1,15 +1,16 @@
import asyncio
import websockets
-import logging
+from config.logger import setup_logging
from core.connection import ConnectionHandler
from core.utils.util import get_local_ip
from core.utils import asr, vad, llm, tts
+TAG = __name__
class WebSocketServer:
def __init__(self, config: dict):
self.config = config
- self.logger = logging.getLogger(__name__)
+ self.logger = setup_logging()
self._vad, self._asr, self._llm, self._tts = self._create_processing_instances()
def _create_processing_instances(self):
@@ -46,9 +47,8 @@ class WebSocketServer:
host = server_config["ip"]
port = server_config["port"]
- self.logger.info("=======下面的地址是websocket协议地址,请勿用浏览器访问=======")
- self.logger.info("Server is running at ws://%s:%s", get_local_ip(), port)
- self.logger.info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
+ self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
+ self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
async with websockets.serve(
self._handle_connection,
host,
diff --git a/manager/api/config.py b/manager/api/config.py
index cd1bc577..13588c9b 100644
--- a/manager/api/config.py
+++ b/manager/api/config.py
@@ -1,13 +1,14 @@
import os
import yaml
-import logging
+from config.logger import setup_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__)
+TAG = __name__
+logger = setup_logging()
class ConfigHandler:
def __init__(self, session_manager):
@@ -42,7 +43,7 @@ class ConfigHandler:
})
except Exception as e:
- logger.error(f"Error getting module options: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Error getting module options: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': '获取配置选项失败'
@@ -52,11 +53,11 @@ class ConfigHandler:
"""只返回用户绑定的设备配置"""
try:
username = request['username']
- logger.info(f"Getting devices for user: {username}")
+ logger.bind(tag=TAG).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}")
+ logger.bind(tag=TAG).info(f"User {username} has devices: {user_devices}")
# 读取所有配置
all_configs = {}
@@ -71,7 +72,7 @@ class ConfigHandler:
if device_id in user_devices
}
- logger.info(f"Returning {len(user_configs)} device configs for user {username}")
+ logger.bind(tag=TAG).info(f"Returning {len(user_configs)} device configs for user {username}")
return web.json_response({
'success': True,
'data': user_configs,
@@ -79,7 +80,7 @@ class ConfigHandler:
})
except Exception as e:
- logger.error(f"Error getting devices for user {request.get('username')}: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Error getting devices for user {request.get('username')}: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': f'获取设备列表失败: {str(e)}'
@@ -101,7 +102,7 @@ class ConfigHandler:
'message': '无权操作此设备'
}, status=403)
- logger.info(f"Device config updated: {device_id} :\n{config}")
+ logger.bind(tag=TAG).info(f"Device config updated: {device_id} :\n{config}")
if not device_id or not config:
return web.json_response({
'success': False,
@@ -128,7 +129,7 @@ class ConfigHandler:
})
except Exception as e:
- logger.error(f"Error saving device config: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Error saving device config: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': f'保存配置失败: {str(e)}'
@@ -163,7 +164,7 @@ class ConfigHandler:
})
except Exception as e:
- logger.error(f"Error deleting device config: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Error deleting device config: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': f'删除配置失败: {str(e)}'
@@ -219,7 +220,7 @@ class ConfigHandler:
}, status=500)
except Exception as e:
- logger.error(f"Error binding device: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Error binding device: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': f'绑定设备失败: {str(e)}'
diff --git a/manager/api/login.py b/manager/api/login.py
index bc0ddd7a..22070357 100644
--- a/manager/api/login.py
+++ b/manager/api/login.py
@@ -1,8 +1,9 @@
-import logging
+from config.logger import setup_logging
from aiohttp import web
import datetime
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class LoginHandler:
def __init__(self, user_manager, session_manager):
@@ -17,7 +18,7 @@ class LoginHandler:
password = data.get('password')
if not username or not password:
- logger.warning(f"Login attempt with empty credentials from {request.remote}")
+ logger.bind(tag=TAG).warning(f"Login attempt with empty credentials from {request.remote}")
return web.json_response({
'success': False,
'message': '用户名和密码不能为空'
@@ -25,7 +26,7 @@ class LoginHandler:
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}")
+ logger.bind(tag=TAG).warning(f"Failed login attempt for user {username} from {request.remote}")
return web.json_response({
'success': False,
'message': '用户名或密码错误'
@@ -45,7 +46,7 @@ class LoginHandler:
})
except Exception as e:
- logger.error(f"Login error: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Login error: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': '登录失败,请稍后重试'
diff --git a/manager/api/prompt.py b/manager/api/prompt.py
index 1fe5aa08..2a4232fa 100644
--- a/manager/api/prompt.py
+++ b/manager/api/prompt.py
@@ -1,12 +1,12 @@
-import logging
+from config.logger import setup_logging
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.response import response_unauthorized, response_success, response_error
-logger = logging.getLogger(__name__)
-
+TAG = __name__
+logger = setup_logging()
class PromptApi:
def __init__(self, config):
@@ -38,5 +38,5 @@ class PromptApi:
return response_success()
except Exception as e:
- logger.error(f"Failed to update prompt: {e}")
+ logger.bind(tag=TAG).error(f"Failed to update prompt: {e}")
return response_error(str(e))
diff --git a/manager/api/register.py b/manager/api/register.py
index f963e5eb..27879afc 100644
--- a/manager/api/register.py
+++ b/manager/api/register.py
@@ -1,9 +1,9 @@
-import logging
+from config.logger import setup_logging
from aiohttp import web
import datetime
-logger = logging.getLogger(__name__)
-
+TAG = __name__
+logger = setup_logging()
class RegisterHandler:
def __init__(self, user_manager):
@@ -17,7 +17,7 @@ class RegisterHandler:
password = data.get('password')
if not username or not password:
- logger.warning(f"Registration attempt with empty credentials from {request.remote}")
+ logger.bind(tag=TAG).warning(f"Registration attempt with empty credentials from {request.remote}")
return web.json_response({
'success': False,
'message': '用户名和密码不能为空'
@@ -25,7 +25,7 @@ class RegisterHandler:
# 检查用户是否已存在
if await self.user_manager.get_user(username):
- logger.warning(f"Registration attempt with existing username {username} from {request.remote}")
+ logger.bind(tag=TAG).warning(f"Registration attempt with existing username {username} from {request.remote}")
return web.json_response({
'success': False,
'message': '用户名已存在'
@@ -41,14 +41,14 @@ class RegisterHandler:
}
await self.user_manager.add_user(username, user_data)
- logger.info(f"Successfully registered new user {username} from {request.remote}")
+ logger.bind(tag=TAG).info(f"Successfully registered new user {username} from {request.remote}")
return web.json_response({
'success': True,
'message': '注册成功'
})
except Exception as e:
- logger.error(f"Register error: {str(e)}", exc_info=True)
+ logger.bind(tag=TAG).error(f"Register error: {str(e)}", exc_info=True)
return web.json_response({
'success': False,
'message': '注册失败,请稍后重试'
diff --git a/manager/api/user_manager.py b/manager/api/user_manager.py
index aa48ac25..abc7e3e5 100644
--- a/manager/api/user_manager.py
+++ b/manager/api/user_manager.py
@@ -1,11 +1,12 @@
import os
import yaml
import hashlib
-import logging
+from config.logger import setup_logging
from core.utils.util import get_project_dir
from core.utils.lock_manager import FileLockManager
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
class UserManager:
def __init__(self):
@@ -23,9 +24,9 @@ class UserManager:
with open(self.secrets_path, 'w', encoding='utf-8') as f:
yaml.dump(default_config, f)
os.chmod(self.secrets_path, 0o600)
- logger.info("Created new .secrets.yaml file")
+ logger.bind(tag=TAG).info("Created new .secrets.yaml file")
except Exception as e:
- logger.error(f"Failed to create .secrets.yaml: {e}")
+ logger.bind(tag=TAG).error(f"Failed to create .secrets.yaml: {e}")
raise
async def _load_user_data_internal(self):
@@ -34,9 +35,9 @@ class UserManager:
with open(self.secrets_path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f) or {'users': {}}
users = data['users']
- logger.debug("Successfully loaded user data")
+ logger.bind(tag=TAG).debug("Successfully loaded user data")
except Exception as e:
- logger.error(f"Failed to load user data: {e}")
+ logger.bind(tag=TAG).error(f"Failed to load user data: {e}")
users = {}
return users
@@ -50,7 +51,7 @@ class UserManager:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Failed to load user data: {e}")
+ logger.bind(tag=TAG).error(f"Failed to load user data: {e}")
users = {}
return users
@@ -59,9 +60,9 @@ class UserManager:
try:
with open(self.secrets_path, 'w', encoding='utf-8') as f:
yaml.dump({'users': users}, f)
- logger.debug("Successfully saved user data")
+ logger.bind(tag=TAG).debug("Successfully saved user data")
except Exception as e:
- logger.error(f"Failed to save user data: {e}")
+ logger.bind(tag=TAG).error(f"Failed to save user data: {e}")
raise
async def save_user_data(self, users):
@@ -73,7 +74,7 @@ class UserManager:
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Failed to save user data: {e}")
+ logger.bind(tag=TAG).error(f"Failed to save user data: {e}")
raise
def hash_password(self, password):
@@ -103,7 +104,7 @@ class UserManager:
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Error adding user: {e}")
+ logger.bind(tag=TAG).error(f"Error adding user: {e}")
raise
async def update_user(self, username, data):
@@ -120,7 +121,7 @@ class UserManager:
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Error updating user: {e}")
+ logger.bind(tag=TAG).error(f"Error updating user: {e}")
return False
async def get_user_devices(self, username: str) -> list:
@@ -151,7 +152,7 @@ class UserManager:
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Error adding device: {e}")
+ logger.bind(tag=TAG).error(f"Error adding device: {e}")
return False
async def remove_device(self, username: str, device_id: str) -> bool:
@@ -170,5 +171,5 @@ class UserManager:
finally:
self.lock_manager.release_lock(self.secrets_path)
except Exception as e:
- logger.error(f"Error removing device: {e}")
+ logger.bind(tag=TAG).error(f"Error removing device: {e}")
return False
diff --git a/manager/http_server.py b/manager/http_server.py
index 202e4a2d..841ea876 100644
--- a/manager/http_server.py
+++ b/manager/http_server.py
@@ -5,10 +5,9 @@ current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(current_dir)
sys.path.append(root_dir)
-import logging
+from config.logger import setup_logging
from aiohttp import web
from aiohttp_cors import setup as cors_setup, ResourceOptions
-from core.utils.util import get_local_ip
from manager.api.login import LoginHandler
from manager.api.register import RegisterHandler
from manager.api.user_manager import UserManager
@@ -16,7 +15,8 @@ from manager.api.config import ConfigHandler
from manager.session import SessionManager
from functools import wraps
-logger = logging.getLogger(__name__)
+TAG = __name__
+logger = setup_logging()
def auth_required(handler):
"""鉴权装饰器"""
@@ -94,23 +94,21 @@ class WebUI:
# 从请求头获取session_id
session_id = request.headers.get('Authorization')
if not session_id:
- logger.warning("No session_id in Authorization header")
+ logger.bind(tag=TAG).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}")
+ logger.bind(tag=TAG).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}")
+ logger.bind(tag=TAG).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()
- logger.info(f"WebUI server is running at http://{local_ip}:{port}")
web.run_app(self.app, host=host, port=port)
if __name__ == '__main__':
diff --git a/poetry.lock b/poetry.lock
index 7a2b337e..470620b6 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -539,7 +539,7 @@ description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
-markers = "platform_system == \"Windows\""
+markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -1813,6 +1813,30 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
+[[package]]
+name = "loguru"
+version = "0.7.3"
+description = "Python logging made (stupidly) simple"
+optional = false
+python-versions = ">=3.5,<4.0"
+groups = ["main"]
+files = [
+ {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"},
+ {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
+win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""]
+
+[package.source]
+type = "legacy"
+url = "https://pypi.tuna.tsinghua.edu.cn/simple"
+reference = "mirrors"
+
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -4241,6 +4265,27 @@ type = "legacy"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
reference = "mirrors"
+[[package]]
+name = "win32-setctime"
+version = "1.2.0"
+description = "A small Python utility to set file creation time on Windows"
+optional = false
+python-versions = ">=3.5"
+groups = ["main"]
+markers = "sys_platform == \"win32\""
+files = [
+ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"},
+ {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"},
+]
+
+[package.extras]
+dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"]
+
+[package.source]
+type = "legacy"
+url = "https://pypi.tuna.tsinghua.edu.cn/simple"
+reference = "mirrors"
+
[[package]]
name = "yarl"
version = "1.18.3"
@@ -4346,4 +4391,4 @@ reference = "mirrors"
[metadata]
lock-version = "2.1"
python-versions = "^3.10.16"
-content-hash = "9e62f0425b6f332e766fb61ceb2b0d8e49a9ba4dc439e181ba709fa8436ad482"
+content-hash = "8645b79840ee30a5262cc87a54aff45a21756f37d6e15fec3215e342ebfe864d"
diff --git a/pyproject.toml b/pyproject.toml
index 59d89bd4..096e32cb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,7 @@ aiohttp-cors = "0.7.0"
ormsgpack = "1.7.0"
ruamel-yaml = "0.18.10"
setuptools = "^75.8.0"
+loguru = "^0.7.3"
[[tool.poetry.source]]