Python 后端提交
Python 后端(FastAPI + FastMCP + ...)的初始版本号设定为 0.1.0,这是 uv 在 pypriject.toml 里给我自动设置的,我觉得有道理。
This commit is contained in:
14
njupt_api/baselib/__init__.py
Normal file
14
njupt_api/baselib/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .config import config
|
||||
from .logger import LogRecord, log_buffer, log_record_serialize, logger
|
||||
from .mcploggingmiddleware import LoggingMiddleware
|
||||
from .playcontextmanager import PlayContextManager
|
||||
|
||||
__all__ = [
|
||||
config,
|
||||
LogRecord,
|
||||
log_buffer,
|
||||
log_record_serialize,
|
||||
logger,
|
||||
LoggingMiddleware,
|
||||
PlayContextManager,
|
||||
]
|
||||
115
njupt_api/baselib/config.py
Normal file
115
njupt_api/baselib/config.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from json import dumps, loads
|
||||
from pathlib import Path
|
||||
from typing import TypeVar
|
||||
|
||||
import aiofiles
|
||||
|
||||
from .logger import logger
|
||||
|
||||
CONFIG_PATH = Path.cwd() / "data" / "config.json"
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self) -> None:
|
||||
self._doc = {}
|
||||
|
||||
async def load_json(self) -> None:
|
||||
"""
|
||||
从 Toml 配置文件中读取配置。
|
||||
"""
|
||||
logger.debug("异步读取配置文件。")
|
||||
async with aiofiles.open(file=CONFIG_PATH, mode="r") as f:
|
||||
self._doc = loads(await f.read())
|
||||
|
||||
def sync_load_json(self) -> None:
|
||||
"""
|
||||
同步读取配置文件,仅限于 main.py 中启动时。
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 配置文件不存在。
|
||||
"""
|
||||
logger.debug("同步读取配置文件。")
|
||||
try:
|
||||
with open(file=CONFIG_PATH, mode="r") as f:
|
||||
self._doc = loads(f.read())
|
||||
except FileNotFoundError:
|
||||
logger.warning("FileNotFoundError - 配置文件不存在。")
|
||||
raise
|
||||
|
||||
def sync_create_json(self) -> None:
|
||||
"""
|
||||
同步创建配置文件。
|
||||
"""
|
||||
logger.debug("同步创建配置文件。")
|
||||
with open(file=CONFIG_PATH, mode="w") as f:
|
||||
f.write(dumps(self._doc))
|
||||
|
||||
def init_config(self) -> None:
|
||||
"""
|
||||
重新初始化 Toml 配置文件。这会重置所有配置。
|
||||
"""
|
||||
logger.warning("初始化配置文件,这会重置所有配置。")
|
||||
self._doc.clear()
|
||||
doc_system = {}
|
||||
doc_schedule = {}
|
||||
doc_log = {}
|
||||
|
||||
doc_system["host"] = "0.0.0.0"
|
||||
doc_system["port"] = 8000
|
||||
doc_system["reload"] = True
|
||||
|
||||
doc_schedule["jwxt_login_method"] = "sso"
|
||||
doc_schedule["semester_start_date"] = "2026-03-02"
|
||||
doc_schedule["schedule_title_template"] = "芒果酸的第 {title} 周课程表"
|
||||
doc_schedule["schedule_subtitle_template"] = "我也要上吗?"
|
||||
|
||||
doc_log["log_api_request_details"] = False
|
||||
doc_log["log_mcp_request_details"] = False
|
||||
doc_log["log_assets_request"] = False
|
||||
|
||||
self._doc["system"] = doc_system
|
||||
self._doc["schedule"] = doc_schedule
|
||||
self._doc["log"] = doc_log
|
||||
|
||||
async def save_json(self) -> None:
|
||||
"""
|
||||
异步保存 Toml 配置文件。
|
||||
"""
|
||||
logger.debug("异步保存配置文件。")
|
||||
async with aiofiles.open(file=CONFIG_PATH, mode="w") as f:
|
||||
await f.write(dumps(self._doc, indent=4))
|
||||
|
||||
def get(self, group: str, option: str, default: T) -> T:
|
||||
"""
|
||||
获取配置项的值。
|
||||
|
||||
Args:
|
||||
group: Table
|
||||
option: Key
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
Any,与 default 参数类型相同。
|
||||
"""
|
||||
try:
|
||||
return self._doc.get(group).get(option)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return self._doc
|
||||
|
||||
def from_dict(self, data: dict) -> None:
|
||||
self._doc.clear()
|
||||
for key, value in data.items():
|
||||
if isinstance(value, dict):
|
||||
t_table = {}
|
||||
for k, v in value.items():
|
||||
t_table[k] = v
|
||||
self._doc[key] = t_table
|
||||
|
||||
|
||||
config = Config()
|
||||
42
njupt_api/baselib/logger.py
Normal file
42
njupt_api/baselib/logger.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import sys
|
||||
from collections import deque
|
||||
|
||||
from loguru import logger
|
||||
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level="DEBUG",
|
||||
colorize=True,
|
||||
)
|
||||
logger.add("data/app.log", rotation="10 MB", retention="7 days") # 文件日志
|
||||
|
||||
log_buffer = deque(maxlen=1000)
|
||||
|
||||
|
||||
class LogRecord:
|
||||
log_counter = 0
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
self.id = LogRecord.log_counter
|
||||
self.message = message
|
||||
|
||||
LogRecord.log_counter += 1
|
||||
|
||||
|
||||
def log_record_serialize(record: LogRecord) -> dict:
|
||||
return {
|
||||
"id": record.id,
|
||||
"message": record.message,
|
||||
}
|
||||
|
||||
|
||||
def memory_sink(message: str) -> None:
|
||||
"""向自定义缓冲区写入日志,供 WebUI 获取
|
||||
:param message: 'loguru._handler.Message'
|
||||
"""
|
||||
log_entry = LogRecord(message=message)
|
||||
log_buffer.append(log_entry)
|
||||
|
||||
|
||||
logger.add(sink=memory_sink, level="DEBUG", colorize=True)
|
||||
36
njupt_api/baselib/mcploggingmiddleware.py
Normal file
36
njupt_api/baselib/mcploggingmiddleware.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import time
|
||||
|
||||
from fastmcp.server.middleware import CallNext, MiddlewareContext
|
||||
from fastmcp.server.middleware.middleware import Middleware
|
||||
from fastmcp.tools import ToolResult
|
||||
from mcp import types as mt
|
||||
|
||||
from . import config
|
||||
from .logger import logger
|
||||
|
||||
|
||||
class LoggingMiddleware(Middleware):
|
||||
async def on_call_tool(
|
||||
self,
|
||||
context: MiddlewareContext[mt.CallToolRequestParams],
|
||||
call_next: CallNext[mt.CallToolRequestParams, ToolResult],
|
||||
) -> ToolResult:
|
||||
tool_name = context.message.name
|
||||
args = context.message.arguments
|
||||
|
||||
start_time = time.time()
|
||||
logger.debug(f"MCP → 调用工具: {tool_name}")
|
||||
if config.get("log", "log_mcp_request_details", False):
|
||||
logger.debug(f"调用参数 - {args=}")
|
||||
|
||||
try:
|
||||
result = await call_next(context)
|
||||
elapsed = time.time() - start_time
|
||||
logger.info(f"MCP ← 工具 {tool_name} 完成, 耗时: {elapsed:.3f}s")
|
||||
return result
|
||||
except Exception as e:
|
||||
elapsed = time.time() - start_time
|
||||
logger.error(
|
||||
f"MCP ✗ 工具 {tool_name} 失败, 耗时: {elapsed:.3f}s, 错误: {e}",
|
||||
)
|
||||
raise
|
||||
56
njupt_api/baselib/playcontextmanager.py
Normal file
56
njupt_api/baselib/playcontextmanager.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from playwright.async_api import (
|
||||
Browser,
|
||||
BrowserContext,
|
||||
Page,
|
||||
Playwright,
|
||||
async_playwright,
|
||||
)
|
||||
|
||||
|
||||
class PlayContextManager:
|
||||
def __init__(
|
||||
self,
|
||||
playwright: Playwright = None,
|
||||
browser: Browser = None,
|
||||
context: BrowserContext = None,
|
||||
page: Page = None,
|
||||
) -> None:
|
||||
self.playwright = playwright
|
||||
self.browser = browser
|
||||
self.context = context
|
||||
self.page = page
|
||||
|
||||
self.isLogin = False
|
||||
|
||||
async def start(self) -> None:
|
||||
"""手动启动"""
|
||||
self.playwright = await async_playwright().start() # 不是 __enter__
|
||||
self.browser = await self.playwright.chromium.launch(
|
||||
headless=False,
|
||||
args=[
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--no-proxy-server",
|
||||
],
|
||||
)
|
||||
self.context = await self.browser.new_context()
|
||||
self.page = await self.context.new_page()
|
||||
|
||||
async def __aenter__(self) -> "PlayContextManager":
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def close(self) -> None:
|
||||
"""手动关闭"""
|
||||
if self.context:
|
||||
await self.context.close()
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
if self.playwright:
|
||||
await self.playwright.stop() # 不是 __exit__
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ANN001
|
||||
await self.close()
|
||||
Reference in New Issue
Block a user