Compare commits
6 Commits
v1_interfa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ebd4ea09e | |||
| 43b6018a3e | |||
| e1ab76a9a1 | |||
| a3a19630b7 | |||
| bda8f13446 | |||
| 45276a7787 |
@@ -33,7 +33,47 @@
|
||||
"Bash(./test_upload.sh:*)",
|
||||
"Bash(./test_all.sh)",
|
||||
"Bash(/data/code/FT_Platform/YG_FT_Platform/test_data_dir.sh:*)",
|
||||
"Bash(grep:*)"
|
||||
"Bash(ls:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(pip install:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(wmic:*)",
|
||||
"Bash(for:*)",
|
||||
"Bash(do echo \"Testing $url:\")",
|
||||
"Bash(done)",
|
||||
"Bash(./stop.sh:*)",
|
||||
"Bash(./xrequest/Scripts/python.exe -m pip:*)",
|
||||
"Bash(echo \"1. 访问: http://localhost:9999/web/pages/main.html\")",
|
||||
"Bash(echo:*)",
|
||||
"Bash(git rm --cached -r:*)",
|
||||
"Bash(git reset HEAD X-Request/)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git check-ignore:*)",
|
||||
"Bash(./setup.sh:*)",
|
||||
"Bash(ss:*)",
|
||||
"Bash(bash:*)",
|
||||
"Bash(/d/Softwares/npm/npm install)",
|
||||
"Bash(npm config:*)",
|
||||
"Bash(cmd:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(./xrequest/Scripts/python.exe:*)",
|
||||
"Bash(xrequest/Scripts/python.exe:*)",
|
||||
"Bash(start:*)",
|
||||
"Bash(do:*)",
|
||||
"Bash(powershell:*)",
|
||||
"Bash(\"D:\\\\Softwares\\\\Anaconda\\\\python\":*)",
|
||||
"Bash(export PYTHONPATH=\"d:\\\\Code\\\\Project\\\\FT-Platform\\\\request:$PYTHONPATH\":*)",
|
||||
"Bash(export PYTHONPATH=\"/d/Code/Project/FT-Platform/request:$PYTHONPATH\":*)",
|
||||
"Bash(PYTHONPATH=src python -m uvicorn:*)",
|
||||
"Bash(ip route get 1.1.1.1)",
|
||||
"Bash(/d/Softwares/Anaconda/python:*)",
|
||||
"Bash(set PYTHONPATH=src)",
|
||||
"Bash(fuser:*)",
|
||||
"Read(//d/Code/Project/FT-Platform/**)",
|
||||
"Bash(xargs:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -174,3 +174,17 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Virtual Environments (项目特定的虚拟环境)
|
||||
# 排除 X-Request 目录下的虚拟环境
|
||||
X-Request/xrequest/
|
||||
X-Request/venv/
|
||||
X-Request/env/
|
||||
X-Request/.venv/
|
||||
X-Request/ENV/
|
||||
X-Request/ENV.bak/
|
||||
X-Request/venv.bak/
|
||||
|
||||
# X-Request 项目 (如果它有自己的 .git 仓库,则整个排除)
|
||||
# 如果 X-Request 是子模块,请删除此行
|
||||
X-Request/
|
||||
|
||||
|
||||
25
clear_cache.bat
Normal file
25
clear_cache.bat
Normal file
@@ -0,0 +1,25 @@
|
||||
@echo off
|
||||
echo 正在清除所有缓存文件...
|
||||
echo.
|
||||
|
||||
REM 切换到项目根目录
|
||||
cd /d "%~dp0"
|
||||
cd request
|
||||
|
||||
echo 1. 清除Python缓存文件...
|
||||
REM 删除所有__pycache__目录
|
||||
for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" 2>nul
|
||||
REM 删除所有.pyc文件
|
||||
for /r . %%f in (*.pyc) do @if exist "%%f" del /q "%%f" 2>nul
|
||||
echo Python缓存清除完成
|
||||
|
||||
echo.
|
||||
echo 2. 清除浏览器缓存(请手动清除或按Ctrl+Shift+Delete)
|
||||
echo 请在浏览器中清除"缓存的图片和文件"
|
||||
echo.
|
||||
|
||||
echo 3. 重启服务提示
|
||||
echo 请按任意键打开启动命令窗口,然后运行启动命令
|
||||
echo 例如: python -m uvicorn src.core.app:app --host 0.0.0.0 --port 8000 --reload
|
||||
echo.
|
||||
pause
|
||||
17
config.yaml
17
config.yaml
@@ -1,17 +0,0 @@
|
||||
# 数据库配置
|
||||
database:
|
||||
host: "10.10.10.189"
|
||||
port: 3306
|
||||
username: "root"
|
||||
password: "88116142"
|
||||
name: "ft"
|
||||
charset: "utf8mb4"
|
||||
|
||||
# 应用配置
|
||||
app:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
debug: true
|
||||
|
||||
# 密钥配置
|
||||
secret_key: "yg-ft-platform-secret-key-2024"
|
||||
3
data/.gitignore
vendored
Normal file
3
data/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# 忽略data目录下的所有文件
|
||||
*
|
||||
!.gitignore
|
||||
42
models/models.json
Normal file
42
models/models.json
Normal file
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"id": "e81c21e1-a4ce-4237-ba22-0922b741b9be",
|
||||
"name": "qwen3-flash",
|
||||
"type": "custom",
|
||||
"provider": "openai",
|
||||
"version": "qwen-flash",
|
||||
"apiUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"apiKey": "sk-5706307e3e3a4eb09452dbf0bb87fe31",
|
||||
"timeout": 60,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.7,
|
||||
"topP": 1,
|
||||
"topK": 50,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": "3e61f8de-831b-4439-9f0a-8b8ad753c5ca",
|
||||
"name": "Nova",
|
||||
"type": "custom",
|
||||
"provider": "custom",
|
||||
"version": "nova",
|
||||
"apiUrl": "http://10.10.10.122:1234/v1",
|
||||
"apiKey": "123",
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.7,
|
||||
"topP": 1,
|
||||
"topK": 50,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": 1
|
||||
}
|
||||
]
|
||||
39
request/_backend.bat
Normal file
39
request/_backend.bat
Normal file
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 读取配置
|
||||
cd /d "%~dp0"
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.host; } catch { Write-Host 'ERROR'; exit 1; }" > temp_host.txt
|
||||
set /p backend_host=<temp_host.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.port; } catch { Write-Host 'ERROR'; exit 1; }" > temp_port.txt
|
||||
set /p backend_port=<temp_port.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.python_path; } catch { Write-Host 'ERROR'; exit 1; }" > temp_python.txt
|
||||
set /p python_path=<temp_python.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.main_module; } catch { Write-Host 'ERROR'; exit 1; }" > temp_module.txt
|
||||
set /p main_module=<temp_module.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.log_level; } catch { Write-Host 'INFO'; exit 1; }" > temp_loglevel.txt
|
||||
set /p log_level=<temp_loglevel.txt
|
||||
|
||||
del temp_host.txt temp_port.txt temp_python.txt temp_module.txt temp_loglevel.txt 2>nul
|
||||
|
||||
echo [后端] 启动配置:
|
||||
echo [后端] 主机: !backend_host!
|
||||
echo [后端] 端口: !backend_port!
|
||||
echo [后端] 日志级别: !log_level!
|
||||
echo [后端] 主模块: !main_module!
|
||||
echo.
|
||||
|
||||
:: 设置环境变量
|
||||
set PYTHONPATH=src
|
||||
set LOGLEVEL=!log_level!
|
||||
|
||||
:: 启动后端服务
|
||||
echo [后端] 正在启动服务...
|
||||
!python_path! -m uvicorn !main_module! --host !backend_host! --port !backend_port!
|
||||
|
||||
pause
|
||||
39
request/fix_newlines.py
Normal file
39
request/fix_newlines.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
修复文件的换行符,将 Windows 风格的 \r\n 转换为 Unix 风格的 \n
|
||||
用法:python fix_newlines.py <file1> <file2> ...
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def fix_newlines(file_path):
|
||||
"""修复文件的换行符"""
|
||||
print(f"修复文件: {file_path}")
|
||||
|
||||
try:
|
||||
# 读取文件内容
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 写入文件,使用 Unix 风格的换行符
|
||||
with open(file_path, 'w', encoding='utf-8', newline='\n') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"✅ 修复成功: {file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 修复失败: {file_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python fix_newlines.py <file1> <file2> ...")
|
||||
sys.exit(1)
|
||||
|
||||
# 修复所有指定的文件
|
||||
for file_path in sys.argv[1:]:
|
||||
if os.path.exists(file_path):
|
||||
fix_newlines(file_path)
|
||||
else:
|
||||
print(f"❌ 文件不存在: {file_path}")
|
||||
1
request/logs/xrequest.pid
Normal file
1
request/logs/xrequest.pid
Normal file
@@ -0,0 +1 @@
|
||||
2916
|
||||
56
request/main.py
Normal file
56
request/main.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
X-Request 高性能FastAPI框架
|
||||
主程序入口
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 将src目录添加到Python路径
|
||||
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
||||
|
||||
from src.core import get_app
|
||||
from src.config import settings
|
||||
from src.utils import logger_manager
|
||||
|
||||
|
||||
# 配置日志系统
|
||||
logger_manager.configure(
|
||||
log_level=settings.log_level,
|
||||
log_format=settings.log_format,
|
||||
log_file=settings.log_file,
|
||||
log_to_console=settings.log_to_console
|
||||
)
|
||||
|
||||
# 创建FastAPI应用(在模块级别,方便uvicorn导入)
|
||||
app = get_app()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
# 打印启动信息
|
||||
print(f"[X-Request] 高性能FastAPI框架正在启动...")
|
||||
print(f"[地址] 监听地址: {settings.host}:{settings.port}")
|
||||
print(f"[文档] API文档: http://{settings.host}:{settings.port}/docs")
|
||||
print(f"[健康] 健康检查: http://{settings.host}:{settings.port}/health")
|
||||
print(f"[信息] 应用信息: http://{settings.host}:{settings.port}/info")
|
||||
print(f"\n[成功] 应用已成功启动!按 Ctrl+C 停止服务器\n")
|
||||
|
||||
# 启动服务器
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
workers=1, # 简化配置,使用单进程
|
||||
reload=False, # 禁用热重载
|
||||
log_level="info", # 显示信息级别的日志
|
||||
access_log=False, # 关闭访问日志
|
||||
use_colors=True, # 启用颜色输出
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
300
request/setup.sh
Normal file
300
request/setup.sh
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 永远从脚本所在目录运行(避免在别的目录执行导致 requirements/.env/venv 路径错误)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# 函数:加载 .env 文件中的变量
|
||||
load_env_file() {
|
||||
local env_file=".env"
|
||||
if [ -f "$env_file" ]; then
|
||||
while IFS='=' read -r key value; do
|
||||
# 跳过注释和空行
|
||||
[[ "$key" =~ ^#.*$ ]] && continue
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
# 移除值两端的引号(如果有)
|
||||
value=$(echo "$value" | sed 's/^["'\'']//' | sed 's/["'\'']$//')
|
||||
|
||||
# 导出变量
|
||||
export "$key=$value"
|
||||
done < "$env_file"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "🔧 X-Request 框架环境设置"
|
||||
echo "=========================="
|
||||
|
||||
# 检查当前操作系统
|
||||
# 首先检查是否是Windows环境
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
OS_TYPE="Windows"
|
||||
PYTHON_CMD="python"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
OS_TYPE="Linux"
|
||||
PYTHON_CMD="python3"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
OS_TYPE="macOS"
|
||||
PYTHON_CMD="python3"
|
||||
else
|
||||
# 使用uname作为后备检测
|
||||
OS_TYPE="$(uname -s)"
|
||||
if [[ "$OS_TYPE" == *"Windows"* || "$OS_TYPE" == "MSYS"* || "$OS_TYPE" == "MINGW"* ]]; then
|
||||
OS_TYPE="Windows"
|
||||
PYTHON_CMD="python"
|
||||
else
|
||||
PYTHON_CMD="python3"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 额外检查是否在Windows PowerShell或CMD中运行
|
||||
if [[ "$SHELL" == *"powershell"* || "$SHELL" == *"cmd.exe"* ]]; then
|
||||
OS_TYPE="Windows"
|
||||
PYTHON_CMD="python"
|
||||
fi
|
||||
|
||||
# 检查是否存在python.exe文件(Windows特有)
|
||||
if [[ -f "$(which python.exe 2>/dev/null)" ]]; then
|
||||
OS_TYPE="Windows"
|
||||
PYTHON_CMD="python.exe"
|
||||
fi
|
||||
|
||||
echo "📋 检测到操作系统: $OS_TYPE"
|
||||
echo "📋 使用Python命令: $PYTHON_CMD"
|
||||
|
||||
# 检查Python版本
|
||||
echo "📋 检查Python版本..."
|
||||
python_version=$($PYTHON_CMD --version 2>&1 | grep -Po '(?<=Python )\d+\.\d+')
|
||||
if [ -z "$python_version" ]; then
|
||||
python_version=$($PYTHON_CMD --version 2>&1 | awk '{print $2}' | cut -d'.' -f1,2)
|
||||
fi
|
||||
|
||||
echo "✅ 发现Python: $python_version"
|
||||
|
||||
# 简单的版本比较(不需要bc命令)
|
||||
major_version=$(echo $python_version | cut -d'.' -f1)
|
||||
minor_version=$(echo $python_version | cut -d'.' -f2)
|
||||
|
||||
if [ "$major_version" -lt 3 ] || ([ "$major_version" -eq 3 ] && [ "$minor_version" -lt 8 ]); then
|
||||
echo "❌ 需要 Python 3.8 或更高版本,当前版本: $python_version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Python版本检查通过"
|
||||
|
||||
# 检查虚拟环境是否存在且完整
|
||||
VENV_COMPLETE=false
|
||||
if [ -d "xrequest" ]; then
|
||||
# 检查激活脚本是否存在
|
||||
if [ -f "xrequest/Scripts/activate" ] || [ -f "xrequest/bin/activate" ]; then
|
||||
echo "✅ 虚拟环境已存在且完整"
|
||||
VENV_COMPLETE=true
|
||||
else
|
||||
echo "⚠️ 虚拟环境存在但不完整,删除并重新创建..."
|
||||
rm -rf xrequest
|
||||
fi
|
||||
fi
|
||||
|
||||
# 如果虚拟环境不存在或不完整,创建新的虚拟环境
|
||||
if [ "$VENV_COMPLETE" = false ]; then
|
||||
echo "📦 创建虚拟环境..."
|
||||
|
||||
# 根据操作系统类型选择不同的虚拟环境创建方式
|
||||
if [ "$OS_TYPE" = "Windows" ]; then
|
||||
# 在Windows上使用python -m venv
|
||||
$PYTHON_CMD -m venv xrequest
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# 检查虚拟环境是否创建成功
|
||||
if [ -f "xrequest/Scripts/activate" ]; then
|
||||
echo "✅ 虚拟环境创建完成"
|
||||
else
|
||||
echo "⚠️ 虚拟环境创建可能失败,尝试使用pip创建..."
|
||||
# 如果venv创建失败,尝试使用pip安装virtualenv并创建虚拟环境
|
||||
$PYTHON_CMD -m pip install --user virtualenv
|
||||
$PYTHON_CMD -m virtualenv xrequest
|
||||
if [ $? -eq 0 ]; then
|
||||
if [ -f "xrequest/Scripts/activate" ]; then
|
||||
echo "✅ 虚拟环境创建完成(使用virtualenv)"
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 手动创建方法:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:xrequest/Scripts/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 手动创建方法:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:xrequest/Scripts/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 手动创建方法:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:xrequest/Scripts/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
else
|
||||
# 在Linux/Mac上使用python3 -m venv
|
||||
$PYTHON_CMD -m venv xrequest
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# 检查虚拟环境是否创建成功
|
||||
if [ -f "xrequest/bin/activate" ]; then
|
||||
echo "✅ 虚拟环境创建完成"
|
||||
else
|
||||
echo "⚠️ 虚拟环境创建可能失败,尝试使用pip创建..."
|
||||
# 如果venv创建失败,尝试使用pip安装virtualenv并创建虚拟环境
|
||||
$PYTHON_CMD -m pip install --user virtualenv
|
||||
$PYTHON_CMD -m virtualenv xrequest
|
||||
if [ $? -eq 0 ]; then
|
||||
if [ -f "xrequest/bin/activate" ]; then
|
||||
echo "✅ 虚拟环境创建完成(使用virtualenv)"
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 手动创建方法:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:source xrequest/bin/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 手动创建方法:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:source xrequest/bin/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "❌ 虚拟环境创建失败,请手动创建虚拟环境"
|
||||
echo " 在Linux上,您可能需要先安装python3-venv包:"
|
||||
echo " sudo apt install python3.12-venv" # 针对Ubuntu/Debian系统
|
||||
echo " 或者:"
|
||||
echo " 1. 安装virtualenv:pip install virtualenv"
|
||||
echo " 2. 创建虚拟环境:virtualenv xrequest"
|
||||
echo " 3. 激活虚拟环境:source xrequest/bin/activate"
|
||||
echo " 4. 安装依赖:pip install -r requirements.txt"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 确定激活脚本路径
|
||||
# 检查是否存在 Windows 风格的激活脚本
|
||||
if [ -f "xrequest/Scripts/activate" ]; then
|
||||
ACTIVATE_SCRIPT="xrequest/Scripts/activate"
|
||||
echo "📋 使用 Windows 风格的激活脚本: $ACTIVATE_SCRIPT"
|
||||
elif [ -f "xrequest/bin/activate" ]; then
|
||||
ACTIVATE_SCRIPT="xrequest/bin/activate"
|
||||
echo "📋 使用 Linux/Mac 风格的激活脚本: $ACTIVATE_SCRIPT"
|
||||
else
|
||||
ACTIVATE_SCRIPT=""
|
||||
echo "⚠️ 未找到激活脚本"
|
||||
fi
|
||||
|
||||
# 安装依赖(强制使用虚拟环境里的 python -m pip;不要因为 curl 检测失败而跳过安装)
|
||||
echo "📦 安装依赖包..."
|
||||
|
||||
VENV_PY=""
|
||||
if [ -f "xrequest/Scripts/python.exe" ]; then
|
||||
VENV_PY="xrequest/Scripts/python.exe"
|
||||
elif [ -f "xrequest/bin/python" ]; then
|
||||
VENV_PY="xrequest/bin/python"
|
||||
fi
|
||||
|
||||
if [ -z "$VENV_PY" ]; then
|
||||
echo "❌ 未找到虚拟环境 Python,无法安装依赖。请先确保虚拟环境创建成功。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📋 使用虚拟环境 Python: $VENV_PY"
|
||||
|
||||
echo " 📦 升级 pip/setuptools/wheel..."
|
||||
$VENV_PY -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
echo " 📚 使用 requirements.txt 安装所有依赖..."
|
||||
$VENV_PY -m pip install -r requirements.txt
|
||||
|
||||
echo " 🔎 校验关键依赖..."
|
||||
$VENV_PY -c "import uvicorn, fastapi; print('OK: uvicorn/fastapi installed')"
|
||||
|
||||
echo "✅ 依赖安装完成!"
|
||||
|
||||
# 加载 .env 文件中的变量
|
||||
echo "📄 加载环境配置..."
|
||||
load_env_file
|
||||
|
||||
# 创建日志目录(使用 .env 中的 LOGS_DIR 配置)
|
||||
echo "📁 创建日志目录..."
|
||||
mkdir -p "${LOGS_DIR:-logs}"
|
||||
|
||||
# 检查是否存在.env文件,如果不存在则创建
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "📄 创建环境配置文件..."
|
||||
if [ -f ".env.example" ]; then
|
||||
cp .env.example .env
|
||||
echo "✅ 已从 .env.example 创建 .env 文件,可根据需要修改配置"
|
||||
else
|
||||
# 如果没有示例文件,创建一个基本的.env文件
|
||||
cat > .env << EOF
|
||||
# 应用配置
|
||||
APP_NAME="X-Request API Framework"
|
||||
APP_VERSION="1.0.0"
|
||||
DEBUG=false
|
||||
|
||||
# 服务器配置
|
||||
HOST="0.0.0.0"
|
||||
PORT=1111
|
||||
WORKERS=1
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL="INFO"
|
||||
LOG_FILE="logs/app.log"
|
||||
LOG_FORMAT="json"
|
||||
LOG_TO_CONSOLE=false
|
||||
|
||||
# 高级日志配置
|
||||
ADVANCED_LOGGING=true
|
||||
LOGS_DIR="logs"
|
||||
MAX_LOG_DAYS=30
|
||||
ENABLE_LOG_CLEANUP=true
|
||||
ROUTE_BASED_LOGGING=true
|
||||
|
||||
# 性能配置
|
||||
MAX_REQUESTS=1000
|
||||
MAX_CONNECTIONS=1000
|
||||
REQUEST_TIMEOUT=30
|
||||
|
||||
# CORS配置
|
||||
CORS_ORIGINS=["*"]
|
||||
CORS_METHODS=["*"]
|
||||
CORS_HEADERS=["*"]
|
||||
EOF
|
||||
echo "✅ 已创建基本的 .env 文件,可根据需要修改配置"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 环境设置完成!"
|
||||
echo ""
|
||||
echo "🚀 启动方法:"
|
||||
echo " ./start.sh"
|
||||
echo " 或者:"
|
||||
echo " source xrequest/bin/activate && python main.py"
|
||||
echo ""
|
||||
echo "📚 API文档地址:"
|
||||
echo " http://localhost:${PORT:-3000}/docs"
|
||||
echo "🏥 健康检查:"
|
||||
echo " http://localhost:${PORT:-3000}/health"
|
||||
echo ""
|
||||
echo ""
|
||||
7
request/src/__init__.py
Normal file
7
request/src/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
X-Request 高性能FastAPI框架
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "X-Request Team"
|
||||
__description__ = "高性能、高并发的请求框架,具有全面的日志系统"
|
||||
11
request/src/api/__init__.py
Normal file
11
request/src/api/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
API 模块
|
||||
|
||||
目录结构:
|
||||
- internal/: 框架核心代码 (base, discovery, monitoring)
|
||||
- modules/: 用户业务代码 (hello, user, ...)
|
||||
"""
|
||||
|
||||
from .internal import BaseAPI, route, auto_register_routes, get_registered_modules_info
|
||||
|
||||
__all__ = ["BaseAPI", "route", "auto_register_routes", "get_registered_modules_info"]
|
||||
13
request/src/api/internal/__init__.py
Normal file
13
request/src/api/internal/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
框架内部模块
|
||||
|
||||
此目录包含框架的核心组件,不建议用户修改:
|
||||
- base.py: API基类
|
||||
- discovery.py: 自动发现和注册系统
|
||||
- monitoring.py: 系统监控API
|
||||
"""
|
||||
|
||||
from .base import BaseAPI, route, get, post, put, delete
|
||||
from .discovery import auto_register_routes, get_registered_modules_info
|
||||
|
||||
__all__ = ['BaseAPI', 'route', 'get', 'post', 'put', 'delete', 'auto_register_routes', 'get_registered_modules_info']
|
||||
271
request/src/api/internal/base.py
Normal file
271
request/src/api/internal/base.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
BaseAPI 基类 - 提供自动路由注册和通用功能
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Any, Dict, List
|
||||
from abc import ABC, abstractmethod
|
||||
import inspect
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 修复导入错误
|
||||
from src.utils.logger import log_info, log_warning, log_error, get_logger
|
||||
from src.utils.exceptions import (
|
||||
ValidationException, NotFoundException, BusinessException
|
||||
)
|
||||
|
||||
|
||||
class BaseAPI(ABC):
|
||||
"""
|
||||
API 基类
|
||||
|
||||
使用方法:
|
||||
1. 继承 BaseAPI
|
||||
2. 定义路由方法 (自动装饰)
|
||||
3. 文件名自动成为路由前缀
|
||||
4. 自动获得日志、错误处理等功能
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化基类"""
|
||||
# 获取模块名作为路由前缀(允许子类覆盖)
|
||||
self.module_name = getattr(self, '_override_module_name', None) or self.__class__.__module__.split('.')[-1]
|
||||
self.router_prefix = f"/{self.module_name}"
|
||||
|
||||
# 创建路由器
|
||||
self.router = APIRouter(
|
||||
prefix=self.router_prefix,
|
||||
tags=[self.module_name.capitalize()]
|
||||
)
|
||||
|
||||
# 获取日志器
|
||||
self.logger = get_logger(self.__class__.__module__)
|
||||
|
||||
# 自动注册路由
|
||||
self._auto_register_routes()
|
||||
|
||||
# 记录初始化
|
||||
self.logger.info(
|
||||
f"API模块初始化完成",
|
||||
module=self.module_name,
|
||||
prefix=self.router_prefix,
|
||||
routes=len(self.router.routes)
|
||||
)
|
||||
|
||||
def _auto_register_routes(self):
|
||||
"""自动注册路由方法"""
|
||||
# 获取所有公共方法
|
||||
methods = inspect.getmembers(self, predicate=inspect.ismethod)
|
||||
|
||||
for name, method in methods:
|
||||
# 跳过私有方法和特殊方法
|
||||
if name.startswith('_'):
|
||||
continue
|
||||
|
||||
# 跳过基类方法
|
||||
if method.__self__.__class__ == BaseAPI:
|
||||
continue
|
||||
|
||||
# 检查方法是否有路径装饰器(HTTP路由)
|
||||
if hasattr(method, '__route_config__'):
|
||||
route_config = method.__route_config__
|
||||
self._register_route(method, route_config)
|
||||
|
||||
# 检查方法是否有WebSocket装饰器
|
||||
elif hasattr(method, '__websocket_config__'):
|
||||
websocket_config = method.__websocket_config__
|
||||
self._register_websocket(method, websocket_config)
|
||||
|
||||
def _register_route(self, method, route_config: Dict[str, Any]):
|
||||
"""注册单个路由"""
|
||||
http_method = route_config['method']
|
||||
path = route_config['path']
|
||||
response_model = route_config.get('response_model')
|
||||
summary = route_config.get('summary', method.__name__)
|
||||
tags = route_config.get('tags', [self.module_name])
|
||||
|
||||
# 创建路由处理器(包装原始方法以添加日志)
|
||||
async def wrapped_handler(*args, **kwargs):
|
||||
return await self._handle_request(method, *args, **kwargs)
|
||||
|
||||
# 复制原始方法的签名和文档
|
||||
wrapped_handler.__signature__ = inspect.signature(method)
|
||||
wrapped_handler.__doc__ = method.__doc__
|
||||
wrapped_handler.__name__ = method.__name__
|
||||
|
||||
# 注册路由
|
||||
self.router.add_api_route(
|
||||
path=path,
|
||||
endpoint=wrapped_handler,
|
||||
methods=[http_method],
|
||||
response_model=response_model,
|
||||
summary=summary,
|
||||
tags=tags
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"路由注册成功",
|
||||
method=http_method,
|
||||
path=f"{self.router_prefix}{path}",
|
||||
handler=method.__name__
|
||||
)
|
||||
|
||||
async def _handle_request(self, method, *args, **kwargs):
|
||||
"""请求处理器包装器"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# 记录请求开始
|
||||
self.logger.info(
|
||||
f"请求开始",
|
||||
method=method.__name__,
|
||||
args=args,
|
||||
kwargs={k: v for k, v in kwargs.items() if k != 'request'}
|
||||
)
|
||||
|
||||
# 执行原始方法
|
||||
result = await method(*args, **kwargs)
|
||||
|
||||
# 记录成功
|
||||
execution_time = time.time() - start_time
|
||||
self.logger.info(
|
||||
f"请求成功",
|
||||
method=method.__name__,
|
||||
execution_time=f"{execution_time:.4f}s"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# 记录错误
|
||||
execution_time = time.time() - start_time
|
||||
self.logger.error(
|
||||
f"请求失败",
|
||||
method=method.__name__,
|
||||
error=str(e),
|
||||
error_type=type(e).__name__,
|
||||
execution_time=f"{execution_time:.4f}s"
|
||||
)
|
||||
raise
|
||||
|
||||
# 快速响应方法
|
||||
def success(self, data: Any = None, message: str = "操作成功") -> Dict[str, Any]:
|
||||
"""成功响应"""
|
||||
return {
|
||||
"success": True,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
def error(self, message: str = "操作失败", code: str = "UNKNOWN_ERROR") -> Dict[str, Any]:
|
||||
"""错误响应"""
|
||||
return {
|
||||
"success": False,
|
||||
"message": message,
|
||||
"code": code,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
def paginated_response(self, items: List[Any], page: int, size: int, total: int) -> Dict[str, Any]:
|
||||
"""分页响应"""
|
||||
return self.success({
|
||||
"items": items,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"size": size,
|
||||
"total": total,
|
||||
"pages": (total + size - 1) // size
|
||||
}
|
||||
}, "数据获取成功")
|
||||
|
||||
def _register_websocket(self, method, websocket_config: Dict[str, Any]):
|
||||
"""注册WebSocket端点"""
|
||||
path = websocket_config['path']
|
||||
|
||||
# 注册WebSocket路由
|
||||
self.router.add_websocket_route(
|
||||
path=path,
|
||||
endpoint=method,
|
||||
name=method.__name__
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"WebSocket路由注册成功",
|
||||
path=f"{self.router_prefix}{path}",
|
||||
handler=method.__name__
|
||||
)
|
||||
|
||||
|
||||
# 路由装饰器
|
||||
def route(method: str, path: str = "/", response_model: Optional[type] = None,
|
||||
summary: Optional[str] = None, tags: Optional[List[str]] = None):
|
||||
"""
|
||||
路由装饰器
|
||||
|
||||
Args:
|
||||
method: HTTP方法 (GET, POST, PUT, DELETE 等)
|
||||
path: 路径,相对于模块前缀
|
||||
response_model: 响应模型
|
||||
summary: API摘要
|
||||
tags: 标签列表
|
||||
"""
|
||||
def decorator(func):
|
||||
func.__route_config__ = {
|
||||
'method': method.upper(),
|
||||
'path': path,
|
||||
'response_model': response_model,
|
||||
'summary': summary or func.__name__,
|
||||
'tags': tags
|
||||
}
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
# 便捷装饰器
|
||||
def get(path: str = "/", response_model: Optional[type] = None, **kwargs):
|
||||
"""GET 路由装饰器"""
|
||||
return route("GET", path, response_model, **kwargs)
|
||||
|
||||
|
||||
def post(path: str = "/", response_model: Optional[type] = None, **kwargs):
|
||||
"""POST 路由装饰器"""
|
||||
return route("POST", path, response_model, **kwargs)
|
||||
|
||||
|
||||
def put(path: str = "/", response_model: Optional[type] = None, **kwargs):
|
||||
"""PUT 路由装饰器"""
|
||||
return route("PUT", path, response_model, **kwargs)
|
||||
|
||||
|
||||
def delete(path: str = "/", response_model: Optional[type] = None, **kwargs):
|
||||
"""DELETE 路由装饰器"""
|
||||
return route("DELETE", path, response_model, **kwargs)
|
||||
|
||||
|
||||
# 通用请求/响应模型
|
||||
class BaseResponse(BaseModel):
|
||||
"""基础响应模型"""
|
||||
success: bool = True
|
||||
message: str
|
||||
timestamp: float
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""错误响应模型"""
|
||||
success: bool = False
|
||||
message: str
|
||||
code: str
|
||||
timestamp: float
|
||||
|
||||
|
||||
class DataResponse(BaseResponse):
|
||||
"""数据响应模型"""
|
||||
data: Any
|
||||
|
||||
|
||||
class PaginatedResponse(BaseResponse):
|
||||
"""分页响应模型"""
|
||||
data: Dict[str, Any] # 包含 items 和 pagination
|
||||
251
request/src/api/internal/discovery.py
Normal file
251
request/src/api/internal/discovery.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
API 模块自动发现和注册系统
|
||||
"""
|
||||
|
||||
import os
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Type
|
||||
|
||||
from .base import BaseAPI
|
||||
from ...utils import log_info, log_warning, log_error, get_logger
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class APIDiscovery:
|
||||
"""API 模块自动发现和注册器"""
|
||||
|
||||
def __init__(self, api_package: str = "src.api"):
|
||||
"""
|
||||
初始化发现器
|
||||
|
||||
Args:
|
||||
api_package: API 包路径
|
||||
"""
|
||||
self.api_package = api_package
|
||||
self.api_modules: Dict[str, BaseAPI] = {}
|
||||
self.discovered_modules: List[str] = []
|
||||
|
||||
def discover_and_register(self, app) -> Dict[str, Any]:
|
||||
"""
|
||||
发现并注册所有 API 模块
|
||||
|
||||
Returns:
|
||||
注册结果统计
|
||||
"""
|
||||
log_info("开始自动发现 API 模块")
|
||||
|
||||
# 获取 API 目录
|
||||
api_dir = Path(self.api_package.replace('.', '/'))
|
||||
if not api_dir.exists():
|
||||
log_error(f"API 目录不存在: {api_dir}")
|
||||
return {"success": False, "error": "API directory not found"}
|
||||
|
||||
# 扫描 Python 文件
|
||||
python_files = self._scan_python_files(api_dir)
|
||||
log_info(f"发现 {len(python_files)} 个 Python 文件")
|
||||
|
||||
# 导入和注册模块
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for file_path in python_files:
|
||||
try:
|
||||
module_name = self._get_module_name(file_path, api_dir)
|
||||
|
||||
# 跳过特殊模块
|
||||
if self._should_skip_module(module_name):
|
||||
log_info(f"跳过模块: {module_name}")
|
||||
continue
|
||||
|
||||
# 导入模块
|
||||
module = self._import_module(module_name)
|
||||
|
||||
# 查找 BaseAPI 子类
|
||||
api_instances = self._find_api_instances(module)
|
||||
|
||||
if api_instances:
|
||||
for instance in api_instances:
|
||||
# 注册路由
|
||||
app.include_router(instance.router)
|
||||
self.api_modules[module_name] = instance
|
||||
|
||||
log_info(
|
||||
f"API 模块注册成功",
|
||||
module=module_name,
|
||||
prefix=instance.router_prefix,
|
||||
routes=len(instance.router.routes)
|
||||
)
|
||||
|
||||
success_count += 1
|
||||
self.discovered_modules.append(module_name)
|
||||
else:
|
||||
log_warning(f"模块中未找到 BaseAPI 子类: {module_name}")
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
log_error(f"模块注册失败: {file_path.name}", error=str(e))
|
||||
|
||||
# 返回统计结果
|
||||
result = {
|
||||
"success": True,
|
||||
"total_files": len(python_files),
|
||||
"registered_modules": success_count,
|
||||
"failed_modules": error_count,
|
||||
"discovered_modules": self.discovered_modules,
|
||||
"api_modules": list(self.api_modules.keys())
|
||||
}
|
||||
|
||||
log_info(
|
||||
f"API 模块发现完成",
|
||||
**{k: v for k, v in result.items() if k != "api_modules"}
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _scan_python_files(self, api_dir: Path) -> List[Path]:
|
||||
"""扫描 API 目录中的 Python 文件"""
|
||||
python_files = []
|
||||
|
||||
# 扫描 internal 子目录(框架内置API,如 monitoring.py)
|
||||
internal_dir = api_dir / "internal"
|
||||
if internal_dir.exists():
|
||||
for file_path in internal_dir.glob("*.py"):
|
||||
if file_path.name.startswith('_') or file_path.name == '__pycache__':
|
||||
continue
|
||||
python_files.append(file_path)
|
||||
|
||||
# 扫描 modules 子目录(用户自定义业务API)
|
||||
modules_dir = api_dir / "modules"
|
||||
if modules_dir.exists():
|
||||
for file_path in modules_dir.glob("*.py"):
|
||||
if file_path.name.startswith('_') or file_path.name == '__pycache__':
|
||||
continue
|
||||
python_files.append(file_path)
|
||||
|
||||
return sorted(python_files)
|
||||
|
||||
def _get_module_name(self, file_path: Path, api_dir: Path) -> str:
|
||||
"""从文件路径获取模块名"""
|
||||
relative_path = file_path.relative_to(api_dir)
|
||||
module_name = str(relative_path.with_suffix('')).replace(os.sep, '.')
|
||||
return f"{self.api_package}.{module_name}"
|
||||
|
||||
def _should_skip_module(self, module_name: str) -> bool:
|
||||
"""判断是否应该跳过模块"""
|
||||
skip_patterns = [
|
||||
'__init__',
|
||||
'base',
|
||||
'discovery',
|
||||
'example' # 跳过旧的 example.py,使用新的基类系统
|
||||
]
|
||||
|
||||
return any(pattern in module_name for pattern in skip_patterns)
|
||||
|
||||
def _import_module(self, module_name: str):
|
||||
"""动态导入模块"""
|
||||
try:
|
||||
return importlib.import_module(module_name)
|
||||
except ImportError as e:
|
||||
log_error(f"模块导入失败: {module_name}", error=str(e))
|
||||
raise
|
||||
|
||||
def _find_api_instances(self, module) -> List[BaseAPI]:
|
||||
"""在模块中查找 BaseAPI 实例"""
|
||||
api_instances = []
|
||||
|
||||
# 检查模块级别的属性
|
||||
for name, obj in inspect.getmembers(module):
|
||||
if isinstance(obj, BaseAPI):
|
||||
api_instances.append(obj)
|
||||
|
||||
# 如果没有找到实例,检查是否有类定义
|
||||
if not api_instances:
|
||||
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||
if (issubclass(obj, BaseAPI) and
|
||||
obj != BaseAPI and
|
||||
obj.__module__ == module.__name__):
|
||||
# 尝试实例化
|
||||
try:
|
||||
instance = obj()
|
||||
api_instances.append(instance)
|
||||
except Exception as e:
|
||||
log_warning(f"API 类实例化失败: {name}", error=str(e))
|
||||
|
||||
return api_instances
|
||||
|
||||
def get_module_info(self) -> Dict[str, Any]:
|
||||
"""获取已注册模块的信息"""
|
||||
info = {}
|
||||
|
||||
for module_name, api_instance in self.api_modules.items():
|
||||
info[module_name] = {
|
||||
"prefix": api_instance.router_prefix,
|
||||
"routes_count": len(api_instance.router.routes),
|
||||
"routes": [
|
||||
{
|
||||
"path": route.path,
|
||||
"methods": list(route.methods),
|
||||
"summary": getattr(route.endpoint, '__doc__', 'No summary')
|
||||
}
|
||||
for route in api_instance.router.routes
|
||||
]
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
def reload_module(self, module_name: str, app):
|
||||
"""重新加载指定模块"""
|
||||
if module_name not in self.api_modules:
|
||||
log_error(f"模块未找到: {module_name}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 重新导入模块
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
# 查找新的 API 实例
|
||||
new_instances = self._find_api_instances(module)
|
||||
|
||||
if new_instances:
|
||||
# 移除旧路由(FastAPI 不支持直接移除,需要重新创建应用)
|
||||
# 这里只是更新实例,实际的路由更新需要重启应用
|
||||
old_instance = self.api_modules[module_name]
|
||||
self.api_modules[module_name] = new_instances[0]
|
||||
|
||||
log_info(f"模块重新加载成功: {module_name}")
|
||||
return True
|
||||
else:
|
||||
log_error(f"重新加载后未找到 API 实例: {module_name}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
log_error(f"模块重新加载失败: {module_name}", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
# 全局发现器实例
|
||||
_discovery_instance = None
|
||||
|
||||
|
||||
def get_discovery() -> APIDiscovery:
|
||||
"""获取全局发现器实例"""
|
||||
global _discovery_instance
|
||||
if _discovery_instance is None:
|
||||
_discovery_instance = APIDiscovery()
|
||||
return _discovery_instance
|
||||
|
||||
|
||||
def auto_register_routes(app):
|
||||
"""自动注册路由的便捷函数"""
|
||||
discovery = get_discovery()
|
||||
return discovery.discover_and_register(app)
|
||||
|
||||
|
||||
def get_registered_modules_info():
|
||||
"""获取已注册模块信息的便捷函数"""
|
||||
discovery = get_discovery()
|
||||
return discovery.get_module_info()
|
||||
499
request/src/api/internal/monitoring.py
Normal file
499
request/src/api/internal/monitoring.py
Normal file
@@ -0,0 +1,499 @@
|
||||
from typing import Dict, Any, List, Set
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from fastapi import BackgroundTasks
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.api.internal.base import BaseAPI, get, post, delete
|
||||
from src.config import settings
|
||||
|
||||
|
||||
# Pydantic模型
|
||||
class BatchDownloadRequest(BaseModel):
|
||||
"""批量下载请求模型"""
|
||||
files: List[str] # 日志文件路径列表,相对路径
|
||||
|
||||
class BatchDeleteRequest(BaseModel):
|
||||
"""批量删除请求模型"""
|
||||
files: List[str] # 日志文件路径列表,相对路径
|
||||
|
||||
|
||||
class MonitoringAPI(BaseAPI):
|
||||
"""监控相关API"""
|
||||
|
||||
def _get_business_modules(self) -> Set[str]:
|
||||
"""获取业务模块列表(从 src/api/modules 目录)"""
|
||||
try:
|
||||
# 获取业务模块目录
|
||||
modules_dir = Path(__file__).parent.parent / "modules"
|
||||
if not modules_dir.exists():
|
||||
return set()
|
||||
|
||||
# 获取所有 Python 文件(排除 __init__.py 和 __pycache__)
|
||||
business_modules = set()
|
||||
for file_path in modules_dir.glob("*.py"):
|
||||
if file_path.name.startswith("__"):
|
||||
continue
|
||||
# 文件名(不含扩展名)就是模块名
|
||||
module_name = file_path.stem
|
||||
business_modules.add(module_name)
|
||||
|
||||
return business_modules
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to get business modules: {str(e)}")
|
||||
return set()
|
||||
|
||||
def _is_business_log(self, log_name: str) -> bool:
|
||||
"""判断是否为业务日志文件"""
|
||||
# 日志文件名格式:{route_name}_success.log 或 {route_name}_error.log
|
||||
# 去掉 _success 或 _error 后缀,获取路由名
|
||||
if log_name.endswith("_success.log"):
|
||||
route_name = log_name[:-12] # 去掉 "_success.log"
|
||||
elif log_name.endswith("_error.log"):
|
||||
route_name = log_name[:-10] # 去掉 "_error.log"
|
||||
else:
|
||||
# 其他格式的日志文件,可能是旧格式或特殊格式
|
||||
# 尝试去掉 .log 后缀
|
||||
if log_name.endswith(".log"):
|
||||
route_name = log_name[:-4]
|
||||
else:
|
||||
return False
|
||||
|
||||
# 获取业务模块列表
|
||||
business_modules = self._get_business_modules()
|
||||
|
||||
# 检查路由名是否在业务模块列表中
|
||||
return route_name in business_modules
|
||||
|
||||
@get("/status", summary="获取服务器状态")
|
||||
async def get_server_status(self):
|
||||
"""
|
||||
获取服务器基本状态信息
|
||||
"""
|
||||
try:
|
||||
# 获取系统信息
|
||||
hostname = os.environ.get('COMPUTERNAME', 'Unknown')
|
||||
system_type = "Windows" if os.name == 'nt' else "Linux"
|
||||
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
|
||||
system_info = {
|
||||
"hostname": hostname,
|
||||
"system": system_type,
|
||||
"python_version": python_version,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
return self.success({
|
||||
"system": system_info
|
||||
}, "Get server status success")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.logger.error(f"Failed to get server status: {str(e)}")
|
||||
self.logger.error(f"Error traceback: {traceback.format_exc()}")
|
||||
return self.error(f"Failed to get server status: {str(e)}")
|
||||
|
||||
@get("/logs", summary="获取日志列表")
|
||||
async def get_logs_list(self, date: str = None):
|
||||
"""
|
||||
获取日志列表,支持按时间分类
|
||||
|
||||
Args:
|
||||
date: 日期,格式为YYYY-MM-DD,如不提供则返回所有日期文件夹
|
||||
"""
|
||||
try:
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
if date:
|
||||
# 获取指定日期的日志文件
|
||||
date_log_dir = log_dir / date
|
||||
if not date_log_dir.exists():
|
||||
return self.success({
|
||||
"logs": [],
|
||||
"total": 0,
|
||||
"date": date
|
||||
}, f"No logs found for date {date}")
|
||||
|
||||
logs = []
|
||||
for file in date_log_dir.glob("*.log"):
|
||||
# 只包含业务模块的日志
|
||||
if not self._is_business_log(file.name):
|
||||
continue
|
||||
|
||||
stats = file.stat()
|
||||
logs.append({
|
||||
"name": file.name,
|
||||
"path": str(file),
|
||||
"relative_path": str(file.relative_to(log_dir)),
|
||||
"size": stats.st_size,
|
||||
"created_at": stats.st_ctime,
|
||||
"modified_at": stats.st_mtime
|
||||
})
|
||||
|
||||
# 按修改时间降序排序
|
||||
logs.sort(key=lambda x: x["modified_at"], reverse=True)
|
||||
|
||||
return self.success({
|
||||
"logs": logs,
|
||||
"total": len(logs),
|
||||
"date": date
|
||||
}, f"Get logs list for date {date} success")
|
||||
else:
|
||||
# 获取所有日期文件夹
|
||||
date_folders = []
|
||||
for folder in log_dir.iterdir():
|
||||
if folder.is_dir():
|
||||
# 检查文件夹名是否为日期格式YYYY-MM-DD
|
||||
try:
|
||||
time.strptime(folder.name, "%Y-%m-%d")
|
||||
# 获取文件夹下的业务日志文件数量(只统计业务模块的日志)
|
||||
business_logs = [f for f in folder.glob("*.log") if f.is_file() and self._is_business_log(f.name)]
|
||||
log_count = len(business_logs)
|
||||
# 获取最后修改时间(只考虑业务日志)
|
||||
max_mtime = max(
|
||||
(f.stat().st_mtime for f in business_logs),
|
||||
default=folder.stat().st_mtime
|
||||
)
|
||||
date_folders.append({
|
||||
"date": folder.name,
|
||||
"path": str(folder),
|
||||
"log_count": log_count,
|
||||
"last_modified": max_mtime
|
||||
})
|
||||
except ValueError:
|
||||
# 不是日期格式,跳过
|
||||
continue
|
||||
|
||||
# 按日期降序排序
|
||||
date_folders.sort(key=lambda x: x["date"], reverse=True)
|
||||
|
||||
return self.success({
|
||||
"dates": date_folders,
|
||||
"total": len(date_folders)
|
||||
}, "Get all log dates success")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to get logs list", error=str(e))
|
||||
return self.error(f"Failed to get logs list: {str(e)}")
|
||||
|
||||
@get("/logs/{date}/{log_name}", summary="获取指定日期的日志内容")
|
||||
async def get_log_content_by_date(self, date: str, log_name: str, entries: int = 10, mode: str = "latest"):
|
||||
"""
|
||||
获取指定日期的日志内容
|
||||
|
||||
Args:
|
||||
date: 日期,格式为YYYY-MM-DD
|
||||
log_name: 日志文件名
|
||||
entries: 返回的日志条目数量,默认10个
|
||||
mode: 显示模式,latest表示最新的N个条目显示在顶部,oldest表示最早的N个条目显示在顶部,默认latest
|
||||
"""
|
||||
try:
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / date / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
# 读取日志文件内容
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 解析多行JSON日志条目
|
||||
log_entries = []
|
||||
current_entry = []
|
||||
brace_count = 0
|
||||
in_entry = False
|
||||
|
||||
for line in content.splitlines():
|
||||
stripped_line = line.strip()
|
||||
if not stripped_line:
|
||||
continue
|
||||
|
||||
# 开始新的日志条目
|
||||
if stripped_line.startswith('{'):
|
||||
# 如果有未完成的条目,先保存它
|
||||
if in_entry and current_entry:
|
||||
# 检查当前条目是否有内容,如果有就保存(即使不完整)
|
||||
if current_entry and any(current_entry):
|
||||
log_entries.append('\n'.join(current_entry))
|
||||
current_entry = [line]
|
||||
brace_count = 1
|
||||
in_entry = True
|
||||
elif in_entry:
|
||||
current_entry.append(line)
|
||||
brace_count += stripped_line.count('{')
|
||||
brace_count -= stripped_line.count('}')
|
||||
|
||||
# 完整的日志条目
|
||||
if brace_count == 0:
|
||||
log_entries.append('\n'.join(current_entry))
|
||||
in_entry = False
|
||||
|
||||
# 如果还有未完成的条目,也要保存(可能是正在写入的最后一条日志)
|
||||
if in_entry and current_entry and any(current_entry):
|
||||
log_entries.append('\n'.join(current_entry))
|
||||
|
||||
result_entries = []
|
||||
|
||||
# 根据模式处理日志条目
|
||||
if mode == "oldest":
|
||||
# 最早的N个条目,顺序不变
|
||||
result_entries = log_entries[:entries]
|
||||
else: # latest
|
||||
# 最新的N个条目,反转顺序,最新的在顶部
|
||||
latest_entries = log_entries[-entries:]
|
||||
latest_entries.reverse()
|
||||
result_entries = latest_entries
|
||||
|
||||
# 拼接成完整的日志内容,添加分隔线(独占一行)
|
||||
separator = "\n" + "=" * 80 + "\n\n"
|
||||
result_content = separator.join(result_entries) + "\n"
|
||||
|
||||
return self.success({
|
||||
"log_name": log_name,
|
||||
"date": date,
|
||||
"entries": len(result_entries),
|
||||
"content": result_content,
|
||||
"mode": mode
|
||||
}, "Get log content success")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to get log content", error=str(e))
|
||||
return self.error(f"Failed to get log content: {str(e)}")
|
||||
|
||||
@get("/logs/{date}/{log_name}/download", summary="下载指定日期的日志文件")
|
||||
async def download_log_by_date(self, date: str, log_name: str):
|
||||
"""
|
||||
下载指定日期的日志文件
|
||||
|
||||
Args:
|
||||
date: 日期,格式为YYYY-MM-DD
|
||||
log_name: 日志文件名
|
||||
"""
|
||||
try:
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / date / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
return FileResponse(
|
||||
log_file,
|
||||
media_type="application/octet-stream",
|
||||
filename=log_file.name
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to download log file", error=str(e))
|
||||
return self.error(f"Failed to download log file: {str(e)}")
|
||||
|
||||
@get("/logs/{log_name}", summary="获取日志内容")
|
||||
async def get_log_content(self, log_name: str, lines: int = 100):
|
||||
"""
|
||||
获取日志文件内容
|
||||
|
||||
Args:
|
||||
log_name: 日志文件名
|
||||
lines: 返回的行数,默认100行
|
||||
"""
|
||||
try:
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
# 读取日志文件的最后N行
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
content = f.readlines()
|
||||
|
||||
# 返回最后lines行
|
||||
content = content[-lines:]
|
||||
|
||||
return self.success({
|
||||
"log_name": log_name,
|
||||
"lines": len(content),
|
||||
"content": "".join(content)
|
||||
}, "Get log content success")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to get log content", error=str(e))
|
||||
return self.error(f"Failed to get log content: {str(e)}")
|
||||
|
||||
@get("/logs/{log_name}/download", summary="下载日志文件")
|
||||
async def download_log(self, log_name: str):
|
||||
"""
|
||||
下载日志文件
|
||||
|
||||
Args:
|
||||
log_name: 日志文件名,支持带日期路径,如YYYY-MM-DD/filename.log
|
||||
"""
|
||||
try:
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
return FileResponse(
|
||||
log_file,
|
||||
media_type="application/octet-stream",
|
||||
filename=log_file.name
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to download log file", error=str(e))
|
||||
return self.error(f"Failed to download log file: {str(e)}")
|
||||
|
||||
@post("/logs/download", summary="批量下载日志文件")
|
||||
async def batch_download_logs(self, request: BatchDownloadRequest):
|
||||
"""
|
||||
批量下载日志文件
|
||||
|
||||
Args:
|
||||
request: 批量下载请求,包含日志文件路径列表
|
||||
"""
|
||||
try:
|
||||
import zipfile
|
||||
import tempfile
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
files = request.files
|
||||
if not files:
|
||||
return self.error("No files selected", code="BAD_REQUEST")
|
||||
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
|
||||
# 创建临时ZIP文件
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_zip:
|
||||
temp_zip_path = temp_zip.name
|
||||
|
||||
# 写入日志文件到ZIP
|
||||
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file_path in files:
|
||||
abs_file_path = log_dir / file_path
|
||||
if abs_file_path.exists():
|
||||
# 保持原始目录结构
|
||||
zipf.write(abs_file_path, arcname=file_path)
|
||||
|
||||
# 返回ZIP文件
|
||||
return FileResponse(
|
||||
temp_zip_path,
|
||||
media_type="application/zip",
|
||||
filename=f"logs_{time.strftime('%Y%m%d_%H%M%S')}.zip",
|
||||
background=BackgroundTasks([lambda: Path(temp_zip_path).unlink(missing_ok=True)])
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to batch download logs", error=str(e))
|
||||
return self.error(f"Failed to batch download logs: {str(e)}")
|
||||
|
||||
@delete("/logs/{log_name}", summary="删除单个日志文件")
|
||||
async def delete_log(self, log_name: str):
|
||||
"""
|
||||
删除单个日志文件
|
||||
|
||||
Args:
|
||||
log_name: 日志文件名,支持带日期路径,如YYYY-MM-DD/filename.log
|
||||
"""
|
||||
try:
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
# 删除日志文件
|
||||
log_file.unlink()
|
||||
|
||||
return self.success({
|
||||
"log_name": log_name,
|
||||
"deleted": True
|
||||
}, "Log file deleted successfully")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to delete log file {log_name}", error=str(e))
|
||||
return self.error(f"Failed to delete log file: {str(e)}")
|
||||
|
||||
@delete("/logs/{date}/{log_name}", summary="删除指定日期的单个日志文件")
|
||||
async def delete_log_by_date(self, date: str, log_name: str):
|
||||
"""
|
||||
删除指定日期的单个日志文件
|
||||
|
||||
Args:
|
||||
date: 日期,格式为YYYY-MM-DD
|
||||
log_name: 日志文件名
|
||||
"""
|
||||
try:
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
log_file = log_dir / date / log_name
|
||||
|
||||
if not log_file.exists():
|
||||
return self.error("Log file not found", code="NOT_FOUND")
|
||||
|
||||
# 删除日志文件
|
||||
log_file.unlink()
|
||||
|
||||
return self.success({
|
||||
"log_name": log_name,
|
||||
"date": date,
|
||||
"deleted": True
|
||||
}, "Log file deleted successfully")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to delete log file {date}/{log_name}", error=str(e))
|
||||
return self.error(f"Failed to delete log file: {str(e)}")
|
||||
|
||||
@post("/logs/batch/delete", summary="批量删除日志文件")
|
||||
async def batch_delete_logs(self, request: BatchDeleteRequest):
|
||||
"""
|
||||
批量删除日志文件
|
||||
|
||||
Args:
|
||||
request: 批量删除请求,包含日志文件路径列表
|
||||
"""
|
||||
try:
|
||||
files = request.files
|
||||
if not files:
|
||||
return self.error("No files selected", code="BAD_REQUEST")
|
||||
|
||||
log_dir = Path(settings.log_dir) if hasattr(settings, 'log_dir') else Path("./logs")
|
||||
|
||||
# 统计删除结果
|
||||
deleted_count = 0
|
||||
failed_count = 0
|
||||
failed_files = []
|
||||
|
||||
# 批量删除日志文件
|
||||
for file_path in files:
|
||||
abs_file_path = log_dir / file_path
|
||||
if abs_file_path.exists():
|
||||
try:
|
||||
abs_file_path.unlink()
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to delete log file {file_path}", error=str(e))
|
||||
failed_count += 1
|
||||
failed_files.append({
|
||||
"file": file_path,
|
||||
"error": str(e)
|
||||
})
|
||||
else:
|
||||
failed_count += 1
|
||||
failed_files.append({
|
||||
"file": file_path,
|
||||
"error": "File not found"
|
||||
})
|
||||
|
||||
return self.success({
|
||||
"total": len(files),
|
||||
"deleted": deleted_count,
|
||||
"failed": failed_count,
|
||||
"failed_files": failed_files
|
||||
}, f"Batch delete completed. Deleted: {deleted_count}, Failed: {failed_count}")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to batch delete logs", error=str(e))
|
||||
return self.error(f"Failed to batch delete logs: {str(e)}")
|
||||
|
||||
|
||||
# 创建API实例
|
||||
monitoring_api = MonitoringAPI()
|
||||
9
request/src/api/log/__init__.py
Normal file
9
request/src/api/log/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
日志管理API模块
|
||||
|
||||
提供日志文件的管理、搜索、下载等功能
|
||||
"""
|
||||
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
289
request/src/api/log/management.py
Normal file
289
request/src/api/log/management.py
Normal file
@@ -0,0 +1,289 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Query
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
import os
|
||||
import json
|
||||
from ..utils.advanced_logger import advanced_logger_manager
|
||||
from ..utils import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_log_stats():
|
||||
"""获取日志统计信息"""
|
||||
try:
|
||||
stats = advanced_logger_manager.get_log_stats()
|
||||
return {
|
||||
"success": True,
|
||||
"data": stats
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get log stats: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve log statistics")
|
||||
|
||||
|
||||
@router.get("/cleanup")
|
||||
async def cleanup_logs(days: Optional[int] = Query(None, description="删除多少天前的日志,默认使用配置值")):
|
||||
"""清理过期日志文件"""
|
||||
try:
|
||||
result = advanced_logger_manager.cleanup_old_logs(days)
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Log cleanup completed successfully",
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup logs: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to cleanup logs")
|
||||
|
||||
|
||||
@router.get("/directories")
|
||||
async def get_log_directories():
|
||||
"""获取所有日志日期目录"""
|
||||
try:
|
||||
logs_dir = Path(advanced_logger_manager.logs_dir)
|
||||
directories = []
|
||||
|
||||
for date_dir in logs_dir.iterdir():
|
||||
if not date_dir.is_dir():
|
||||
continue
|
||||
|
||||
try:
|
||||
# 验证日期格式
|
||||
datetime.strptime(date_dir.name, "%Y-%m-%d")
|
||||
|
||||
# 获取目录信息
|
||||
dir_stats = {
|
||||
"name": date_dir.name,
|
||||
"path": str(date_dir),
|
||||
"size_bytes": 0,
|
||||
"file_count": 0,
|
||||
"files": []
|
||||
}
|
||||
|
||||
# 统计文件信息
|
||||
for log_file in date_dir.glob("*.log"):
|
||||
file_stats = log_file.stat()
|
||||
dir_stats["file_count"] += 1
|
||||
dir_stats["size_bytes"] += file_stats.st_size
|
||||
dir_stats["files"].append({
|
||||
"name": log_file.name,
|
||||
"size_bytes": file_stats.st_size,
|
||||
"modified": datetime.fromtimestamp(file_stats.st_mtime).isoformat()
|
||||
})
|
||||
|
||||
dir_stats["size_mb"] = round(dir_stats["size_bytes"] / (1024 * 1024), 2)
|
||||
directories.append(dir_stats)
|
||||
|
||||
except ValueError:
|
||||
# 跳过非日期格式的目录
|
||||
continue
|
||||
|
||||
# 按日期排序(最新的在前)
|
||||
directories.sort(key=lambda x: x["name"], reverse=True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"directories": directories,
|
||||
"total_directories": len(directories),
|
||||
"base_path": str(logs_dir)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get log directories: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to retrieve log directories")
|
||||
|
||||
|
||||
@router.get("/files/{date}/{route_name}")
|
||||
async def get_log_file(date: str, route_name: str, lines: Optional[int] = Query(100, ge=1, le=10000, description="返回的行数")):
|
||||
"""获取指定日期和路由的日志文件内容"""
|
||||
try:
|
||||
# 验证日期格式
|
||||
datetime.strptime(date, "%Y-%m-%d")
|
||||
|
||||
# 验证路由名
|
||||
if not route_name.replace('_', '').replace('-', '').isalnum():
|
||||
raise ValueError("Invalid route name")
|
||||
|
||||
log_path = advanced_logger_manager.get_route_log_path(route_name, datetime.strptime(date, "%Y-%m-%d"))
|
||||
|
||||
if not log_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Log file not found: {route_name}.log for date {date}")
|
||||
|
||||
# 读取文件内容
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8') as f:
|
||||
file_lines = f.readlines()
|
||||
|
||||
# 取最后N行
|
||||
if len(file_lines) > lines:
|
||||
file_lines = file_lines[-lines:]
|
||||
|
||||
# 解析JSON日志行
|
||||
parsed_lines = []
|
||||
for line in file_lines:
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
parsed_line = json.loads(line)
|
||||
parsed_lines.append(parsed_line)
|
||||
except json.JSONDecodeError:
|
||||
# 如果不是JSON格式,保持原样
|
||||
parsed_lines.append({"raw": line})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"file": str(log_path),
|
||||
"date": date,
|
||||
"route_name": route_name,
|
||||
"total_lines": len(file_lines),
|
||||
"showed_lines": len(parsed_lines),
|
||||
"lines": parsed_lines
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to read log file {log_path}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to read log file: {str(e)}")
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid date format: {str(e)}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error getting log file: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/download/{date}/{route_name}")
|
||||
async def download_log_file(date: str, route_name: str):
|
||||
"""下载指定的日志文件"""
|
||||
try:
|
||||
# 验证日期格式
|
||||
datetime.strptime(date, "%Y-%m-%d")
|
||||
|
||||
# 验证路由名
|
||||
if not route_name.replace('_', '').replace('-', '').isalnum():
|
||||
raise ValueError("Invalid route name")
|
||||
|
||||
log_path = advanced_logger_manager.get_route_log_path(route_name, datetime.strptime(date, "%Y-%m-%d"))
|
||||
|
||||
if not log_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Log file not found: {route_name}.log for date {date}")
|
||||
|
||||
return FileResponse(
|
||||
path=str(log_path),
|
||||
filename=f"{date}_{route_name}.log",
|
||||
media_type="text/plain"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid date format: {str(e)}")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error downloading log file: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
async def search_logs(
|
||||
date: str = Query(..., description="搜索日期 (YYYY-MM-DD)"),
|
||||
route_name: Optional[str] = Query(None, description="路由名称"),
|
||||
keyword: Optional[str] = Query(None, description="搜索关键词"),
|
||||
level: Optional[str] = Query(None, description="日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)"),
|
||||
limit: int = Query(100, ge=1, le=1000, description="返回结果数量限制")
|
||||
):
|
||||
"""搜索日志内容"""
|
||||
try:
|
||||
# 验证日期格式
|
||||
search_date = datetime.strptime(date, "%Y-%m-%d")
|
||||
|
||||
# 确定搜索范围
|
||||
if route_name:
|
||||
# 搜索特定路由的日志
|
||||
log_files = [advanced_logger_manager.get_route_log_path(route_name, search_date)]
|
||||
else:
|
||||
# 搜索所有路由的日志
|
||||
date_dir = advanced_logger_manager.get_date_log_dir(search_date)
|
||||
log_files = list(date_dir.glob("*.log")) if date_dir.exists() else []
|
||||
|
||||
results = []
|
||||
|
||||
for log_file in log_files:
|
||||
if not log_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(log_file, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
log_entry = json.loads(line)
|
||||
|
||||
# 应用过滤条件
|
||||
if level and log_entry.get("level", "").upper() != level.upper():
|
||||
continue
|
||||
|
||||
if keyword:
|
||||
# 在消息中搜索关键词
|
||||
message = str(log_entry.get("message", "")) + str(log_entry.get("event", ""))
|
||||
if keyword.lower() not in message.lower():
|
||||
continue
|
||||
|
||||
# 添加文件信息
|
||||
log_entry["file_info"] = {
|
||||
"file": log_file.name,
|
||||
"line_number": line_num
|
||||
}
|
||||
|
||||
results.append(log_entry)
|
||||
|
||||
# 检查是否达到限制
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
except json.JSONDecodeError:
|
||||
# 非JSON格式的行,跳过
|
||||
continue
|
||||
|
||||
if len(results) >= limit:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to search in file {log_file}: {str(e)}")
|
||||
continue
|
||||
|
||||
# 按时间戳排序(最新的在前)
|
||||
results.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"search_criteria": {
|
||||
"date": date,
|
||||
"route_name": route_name,
|
||||
"keyword": keyword,
|
||||
"level": level,
|
||||
"limit": limit
|
||||
},
|
||||
"total_found": len(results),
|
||||
"results": results
|
||||
}
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid date format: {str(e)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to search logs: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to search logs")
|
||||
11
request/src/api/log/routes.py
Normal file
11
request/src/api/log/routes.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
from .management import router as management_router
|
||||
|
||||
# 创建主路由器
|
||||
router = APIRouter(prefix="/logs", tags=["日志管理"])
|
||||
|
||||
# 包含所有子路由
|
||||
router.include_router(management_router)
|
||||
|
||||
# 导出路由器
|
||||
__all__ = ["router"]
|
||||
20
request/src/api/modules/__init__.py
Normal file
20
request/src/api/modules/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
业务模块目录
|
||||
|
||||
此目录用于存放用户自定义的业务API模块。
|
||||
框架会自动扫描此目录并注册所有继承 BaseAPI 的模块。
|
||||
|
||||
示例:
|
||||
from src.api.base import BaseAPI, route
|
||||
|
||||
class MyAPI(BaseAPI):
|
||||
router_prefix = "/myapi"
|
||||
router_tags = ["MyAPI"]
|
||||
|
||||
@route.get("/hello")
|
||||
async def hello(self):
|
||||
return {"message": "Hello!"}
|
||||
|
||||
# 创建实例供自动发现
|
||||
my_api = MyAPI()
|
||||
"""
|
||||
381
request/src/api/modules/dataset.py
Normal file
381
request/src/api/modules/dataset.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
数据集管理 API 模块
|
||||
提供数据集上传、列表、删除等功能
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from fastapi import UploadFile, File, Form
|
||||
|
||||
from src.api.internal.base import BaseAPI, post, get, delete
|
||||
from src.models.response import StandardResponse
|
||||
from src.services.file_upload import file_upload_service
|
||||
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""
|
||||
格式化文件大小显示
|
||||
|
||||
Args:
|
||||
size_bytes: 文件大小(字节)
|
||||
|
||||
Returns:
|
||||
str: 格式化后的文件大小字符串
|
||||
"""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
size_kb = round(size_bytes / 1024, 2)
|
||||
return f"{size_kb} KB"
|
||||
else:
|
||||
size_mb = round(size_bytes / 1024 / 1024, 2)
|
||||
return f"{size_mb} MB"
|
||||
|
||||
|
||||
class DatasetAPI(BaseAPI):
|
||||
"""数据集管理 API"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化"""
|
||||
# 在调用super().__init__()之前设置module_name
|
||||
self._override_module_name = "datasets"
|
||||
super().__init__()
|
||||
self.logger.info("DatasetAPI 初始化完成")
|
||||
|
||||
@post("/upload", response_model=StandardResponse)
|
||||
async def upload_dataset(
|
||||
self,
|
||||
file: UploadFile = File(...),
|
||||
description: Optional[str] = Form(None)
|
||||
):
|
||||
"""
|
||||
上传数据集文件
|
||||
|
||||
Args:
|
||||
file: 上传的文件(支持 .json, .jsonl 格式)
|
||||
description: 文件描述(可选)
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含上传结果的标准响应
|
||||
"""
|
||||
try:
|
||||
# 验证文件类型
|
||||
filename = file.filename or "unknown"
|
||||
file_ext = filename.lower().split('.')[-1] if '.' in filename else ''
|
||||
|
||||
if file_ext not in ['json', 'jsonl']:
|
||||
return StandardResponse.error("只支持 .json 和 .jsonl 格式的文件")
|
||||
|
||||
# 读取文件内容
|
||||
file_content = await file.read()
|
||||
|
||||
# 如果未提供描述,使用默认描述
|
||||
if not description:
|
||||
description = f"用户上传的数据集文件: {filename}"
|
||||
|
||||
# 使用文件上传服务上传文件
|
||||
uploaded_file = await file_upload_service.upload_file(
|
||||
file_content=file_content,
|
||||
original_filename=filename,
|
||||
content_type=file.content_type,
|
||||
description=description
|
||||
)
|
||||
|
||||
# 转换为前端期望的格式
|
||||
# 显示真实文件名(从映射文件中获取)
|
||||
mapping = file_upload_service.get_filename_mapping(uploaded_file.file_id)
|
||||
display_name = mapping["original_filename"] if mapping else uploaded_file.original_filename
|
||||
|
||||
# 格式化文件大小
|
||||
size_mb = round(uploaded_file.file_size / 1024 / 1024, 2)
|
||||
size_display = format_file_size(uploaded_file.file_size)
|
||||
|
||||
dataset_info = {
|
||||
"file_id": uploaded_file.file_id,
|
||||
"name": display_name,
|
||||
"size": uploaded_file.file_size,
|
||||
"size_mb": size_mb,
|
||||
"size_display": size_display,
|
||||
"status": "已处理", # 默认状态
|
||||
"uploaded_at": uploaded_file.uploaded_at,
|
||||
"description": uploaded_file.description
|
||||
}
|
||||
|
||||
return StandardResponse.success({
|
||||
"message": "数据集上传成功",
|
||||
"dataset": dataset_info
|
||||
})
|
||||
|
||||
except ValueError as e:
|
||||
return StandardResponse.error(str(e))
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"上传失败: {str(e)}")
|
||||
|
||||
@get("", response_model=StandardResponse)
|
||||
async def list_datasets(self):
|
||||
"""
|
||||
获取数据集列表(只返回filename_mapping.json中记录的文件)
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含数据集列表的标准响应
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
|
||||
data_dir = file_upload_service.upload_dir
|
||||
mapping_file = data_dir / "filename_mapping.json"
|
||||
|
||||
# 读取文件名映射
|
||||
mappings = {}
|
||||
if mapping_file.exists():
|
||||
with open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
mapping_data = json.load(f)
|
||||
mappings = mapping_data.get("mappings", {})
|
||||
|
||||
# 根据映射文件构建数据集列表
|
||||
datasets = []
|
||||
for file_id, mapping_info in mappings.items():
|
||||
# 获取物理文件信息
|
||||
storage_filename = mapping_info.get("storage_filename", f"{file_id}.json")
|
||||
file_path = data_dir / storage_filename
|
||||
|
||||
if file_path.exists():
|
||||
# 获取文件大小
|
||||
file_size = file_path.stat().st_size
|
||||
size_mb = round(file_size / 1024 / 1024, 2)
|
||||
size_display = format_file_size(file_size)
|
||||
|
||||
datasets.append({
|
||||
"file_id": file_id,
|
||||
"name": mapping_info.get("original_filename", ""),
|
||||
"size": file_size,
|
||||
"size_mb": size_mb,
|
||||
"size_display": size_display,
|
||||
"status": "已处理",
|
||||
"description": mapping_info.get("original_filename", ""),
|
||||
"uploaded_at": mapping_info.get("uploaded_at", ""),
|
||||
"download_count": 0,
|
||||
"is_physical_file": True
|
||||
})
|
||||
|
||||
# 按上传时间排序
|
||||
datasets.sort(key=lambda x: x["uploaded_at"], reverse=True)
|
||||
|
||||
return StandardResponse.success({
|
||||
"datasets": datasets,
|
||||
"total": len(datasets),
|
||||
"source": "filename_mapping"
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"获取数据集列表失败: {str(e)}")
|
||||
|
||||
@get("/{file_id}", response_model=StandardResponse)
|
||||
async def get_dataset(self, file_id: str):
|
||||
"""
|
||||
获取特定数据集的详细信息
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含数据集详情的标准响应
|
||||
"""
|
||||
try:
|
||||
file_info = file_upload_service.get_file(file_id)
|
||||
|
||||
if not file_info:
|
||||
return StandardResponse.error(f"数据集 {file_id} 不存在")
|
||||
|
||||
# 转换为前端期望的格式
|
||||
# 显示真实文件名(从映射文件中获取)
|
||||
mapping = file_upload_service.get_filename_mapping(file_info.file_id)
|
||||
display_name = mapping["original_filename"] if mapping else file_info.original_filename
|
||||
|
||||
# 格式化文件大小
|
||||
size_mb = round(file_info.file_size / 1024 / 1024, 2)
|
||||
size_display = format_file_size(file_info.file_size)
|
||||
|
||||
dataset_info = {
|
||||
"file_id": file_info.file_id,
|
||||
"name": display_name,
|
||||
"size": file_info.file_size,
|
||||
"size_mb": size_mb,
|
||||
"size_display": size_display,
|
||||
"status": "已处理",
|
||||
"description": file_info.description or "",
|
||||
"uploaded_at": file_info.uploaded_at,
|
||||
"updated_at": file_info.updated_at,
|
||||
"download_count": file_info.download_count,
|
||||
"content_type": file_info.content_type,
|
||||
"file_hash": file_info.file_hash
|
||||
}
|
||||
|
||||
return StandardResponse.success(dataset_info)
|
||||
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"获取数据集详情失败: {str(e)}")
|
||||
|
||||
@get("/{file_id}/content", response_model=StandardResponse)
|
||||
async def get_dataset_content(self, file_id: str, limit: int = 5):
|
||||
"""
|
||||
获取数据集文件内容(前N条记录)
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
limit: 返回的记录数量,默认5条
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含数据集内容的标准响应
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
import jsonlines
|
||||
|
||||
# 获取文件信息
|
||||
file_info = file_upload_service.get_file(file_id)
|
||||
if not file_info:
|
||||
return StandardResponse.error(f"数据集 {file_id} 不存在")
|
||||
|
||||
# 获取文件路径
|
||||
file_path = file_upload_service.get_file_path(file_id)
|
||||
if not file_path or not file_path.exists():
|
||||
return StandardResponse.error(f"文件 {file_id} 不存在")
|
||||
|
||||
# 读取文件内容
|
||||
content_preview = []
|
||||
filename = file_info.original_filename.lower()
|
||||
|
||||
try:
|
||||
if filename.endswith('.jsonl'):
|
||||
# 处理JSONL格式
|
||||
with jsonlines.open(file_path) as reader:
|
||||
count = 0
|
||||
for item in reader:
|
||||
if count >= limit:
|
||||
break
|
||||
content_preview.append(item)
|
||||
count += 1
|
||||
else:
|
||||
# 处理JSON格式
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, list):
|
||||
# 如果是数组,取前N条
|
||||
content_preview = data[:limit]
|
||||
else:
|
||||
# 如果是对象,直接返回
|
||||
content_preview = data
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return StandardResponse.error(f"JSON文件格式错误: {str(e)}")
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"读取文件内容失败: {str(e)}")
|
||||
|
||||
# 获取真实文件名(从映射文件中获取)
|
||||
mapping = file_upload_service.get_filename_mapping(file_id)
|
||||
display_filename = mapping["original_filename"] if mapping else file_info.original_filename
|
||||
|
||||
return StandardResponse.success({
|
||||
"file_id": file_id,
|
||||
"filename": display_filename,
|
||||
"total_records": len(content_preview),
|
||||
"preview": content_preview
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"获取数据集内容失败: {str(e)}")
|
||||
|
||||
@delete("/{file_id}", response_model=StandardResponse)
|
||||
async def delete_dataset(self, file_id: str):
|
||||
"""
|
||||
删除数据集
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含删除结果的标准响应
|
||||
"""
|
||||
try:
|
||||
if not file_upload_service.file_exists(file_id):
|
||||
return StandardResponse.error(f"数据集 {file_id} 不存在")
|
||||
|
||||
success = file_upload_service.delete_file(file_id)
|
||||
|
||||
if success:
|
||||
return StandardResponse.success({
|
||||
"message": f"数据集 {file_id} 已删除"
|
||||
})
|
||||
else:
|
||||
return StandardResponse.error(f"删除数据集 {file_id} 失败")
|
||||
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"删除数据集失败: {str(e)}")
|
||||
|
||||
@get("/list-files", response_model=StandardResponse)
|
||||
async def list_data_files(self):
|
||||
"""
|
||||
查询data目录下的文件列表
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含文件列表的标准响应
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
data_dir = file_upload_service.upload_dir
|
||||
mapping_file = data_dir / "filename_mapping.json"
|
||||
|
||||
# 读取文件名映射
|
||||
mappings = {}
|
||||
if mapping_file.exists():
|
||||
try:
|
||||
with open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
mapping_data = json.load(f)
|
||||
mappings = mapping_data.get("mappings", {})
|
||||
except Exception:
|
||||
mappings = {}
|
||||
|
||||
# 获取data目录下的所有JSON文件
|
||||
files_info = []
|
||||
if data_dir.exists():
|
||||
for file_path in data_dir.iterdir():
|
||||
# 跳过目录和映射文件本身
|
||||
if file_path.is_file() and file_path.name != "filename_mapping.json":
|
||||
file_id = file_path.stem # 去掉.json后缀得到file_id
|
||||
|
||||
# 从映射文件获取真实文件名
|
||||
mapping_info = mappings.get(file_id, {})
|
||||
original_filename = mapping_info.get("original_filename", file_path.name)
|
||||
uploaded_at = mapping_info.get("uploaded_at", "")
|
||||
|
||||
# 获取文件大小
|
||||
file_size = file_path.stat().st_size
|
||||
|
||||
files_info.append({
|
||||
"file_id": file_id,
|
||||
"original_filename": original_filename,
|
||||
"storage_filename": file_path.name,
|
||||
"file_path": str(file_path),
|
||||
"file_size": file_size,
|
||||
"file_size_mb": round(file_size / 1024 / 1024, 2),
|
||||
"uploaded_at": uploaded_at,
|
||||
"exists_in_mapping": file_id in mappings
|
||||
})
|
||||
|
||||
# 按文件名排序
|
||||
files_info.sort(key=lambda x: x["original_filename"])
|
||||
|
||||
return StandardResponse.success({
|
||||
"total": len(files_info),
|
||||
"files": files_info
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return StandardResponse.error(f"查询文件列表失败: {str(e)}")
|
||||
|
||||
|
||||
# 创建实例(自动发现系统会找到这个实例)
|
||||
dataset_api = DatasetAPI()
|
||||
567
request/src/api/modules/models.py
Normal file
567
request/src/api/modules/models.py
Normal file
@@ -0,0 +1,567 @@
|
||||
"""
|
||||
模型配置管理模块
|
||||
支持模型的增删改查操作
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import HTTPException, Body, Response
|
||||
import logging
|
||||
|
||||
# 导入基类
|
||||
from src.api.internal.base import BaseAPI, get, post, put, delete
|
||||
|
||||
# 配置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 模型配置文件路径
|
||||
import os
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
MODELS_CONFIG_PATH = os.path.join(BASE_DIR, "..", "..", "models", "models.json")
|
||||
# 规范化路径
|
||||
MODELS_CONFIG_PATH = os.path.normpath(MODELS_CONFIG_PATH)
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""模型配置管理器"""
|
||||
|
||||
@staticmethod
|
||||
def load_models() -> List[Dict[str, Any]]:
|
||||
"""从JSON文件加载所有模型配置"""
|
||||
try:
|
||||
if not os.path.exists(MODELS_CONFIG_PATH):
|
||||
logger.warning(f"模型配置文件不存在: {MODELS_CONFIG_PATH}")
|
||||
return []
|
||||
|
||||
with open(MODELS_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
models = json.load(f)
|
||||
logger.info(f"成功加载 {len(models)} 个模型配置")
|
||||
return models
|
||||
except Exception as e:
|
||||
logger.error(f"加载模型配置失败: {str(e)}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def save_models(models: List[Dict[str, Any]]) -> bool:
|
||||
"""保存模型配置到JSON文件"""
|
||||
try:
|
||||
# 确保目录存在
|
||||
os.makedirs(os.path.dirname(MODELS_CONFIG_PATH), exist_ok=True)
|
||||
|
||||
with open(MODELS_CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(models, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"成功保存 {len(models)} 个模型配置")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存模型配置失败: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_model_by_id(model_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取单个模型配置"""
|
||||
models = ModelManager.load_models()
|
||||
for model in models:
|
||||
if model.get('id') == model_id:
|
||||
return model
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def create_model(model_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""创建新模型配置"""
|
||||
models = ModelManager.load_models()
|
||||
|
||||
# 生成唯一ID
|
||||
model_id = str(uuid.uuid4())
|
||||
|
||||
# 创建新模型
|
||||
new_model = {
|
||||
'id': model_id,
|
||||
'name': model_data.get('name', ''),
|
||||
'type': model_data.get('type', ''),
|
||||
'provider': model_data.get('provider', ''),
|
||||
'version': model_data.get('version', ''),
|
||||
'apiUrl': model_data.get('apiUrl', ''),
|
||||
'apiKey': model_data.get('apiKey', ''),
|
||||
'timeout': model_data.get('timeout', 30),
|
||||
'maxRetries': model_data.get('maxRetries', 3),
|
||||
'temperature': model_data.get('temperature', 0.7),
|
||||
'topP': model_data.get('topP', 1.0),
|
||||
'topK': model_data.get('topK', 50),
|
||||
'maxTokens': model_data.get('maxTokens', 2048),
|
||||
'systemPrompt': model_data.get('systemPrompt', '你是一个有用的AI助手。'),
|
||||
'streaming': model_data.get('streaming', True),
|
||||
'functions': model_data.get('functions', False),
|
||||
'logRequests': model_data.get('logRequests', True),
|
||||
'status': model_data.get('status', '未测试')
|
||||
}
|
||||
|
||||
# 添加到列表
|
||||
models.append(new_model)
|
||||
|
||||
# 保存
|
||||
if ModelManager.save_models(models):
|
||||
return new_model
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="保存模型配置失败")
|
||||
|
||||
@staticmethod
|
||||
def update_model(model_id: str, model_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""更新模型配置"""
|
||||
models = ModelManager.load_models()
|
||||
|
||||
# 查找并更新模型
|
||||
for i, model in enumerate(models):
|
||||
if model.get('id') == model_id:
|
||||
# 更新模型数据
|
||||
updated_model = models[i].copy()
|
||||
updated_model.update(model_data)
|
||||
updated_model['id'] = model_id # 确保ID不变
|
||||
|
||||
models[i] = updated_model
|
||||
|
||||
# 保存
|
||||
if ModelManager.save_models(models):
|
||||
return updated_model
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="保存模型配置失败")
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def delete_model(model_id: str) -> bool:
|
||||
"""删除模型配置"""
|
||||
models = ModelManager.load_models()
|
||||
|
||||
# 查找并删除
|
||||
for i, model in enumerate(models):
|
||||
if model.get('id') == model_id:
|
||||
del models[i]
|
||||
|
||||
# 保存
|
||||
if ModelManager.save_models(models):
|
||||
return True
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="保存模型配置失败")
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def call_model(model_id: str, prompt: str, system_prompt: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""真实调用模型API - 简化版本"""
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if not model:
|
||||
return {'success': False, 'error': '模型不存在'}
|
||||
|
||||
# 检查API配置
|
||||
api_url = model.get('apiUrl', '').strip()
|
||||
api_key = model.get('apiKey', '').strip()
|
||||
version = model.get('version', '').strip()
|
||||
|
||||
# 记录调试信息
|
||||
model_name = str(model.get('name', 'unknown')) if model.get('name') is not None else 'unknown'
|
||||
logger.info(f"测试模型: {model_name}")
|
||||
logger.info(f"API地址: {api_url}")
|
||||
logger.info(f"模型版本: {version}")
|
||||
logger.info(f"API密钥长度: {len(api_key) if api_key else 0}")
|
||||
|
||||
if not api_url:
|
||||
return {'success': False, 'error': 'API地址未配置'}
|
||||
|
||||
if not version:
|
||||
return {'success': False, 'error': '模型版本未配置'}
|
||||
|
||||
# 准备请求数据 - 标准的OpenAI格式
|
||||
messages = []
|
||||
|
||||
# 添加系统提示词
|
||||
if system_prompt:
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
elif model.get('systemPrompt'):
|
||||
system_prompt_content = model.get('systemPrompt')
|
||||
if system_prompt_content:
|
||||
messages.append({"role": "system", "content": system_prompt_content})
|
||||
|
||||
# 添加用户提示词
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
logger.info(f"准备发送的消息: {messages}")
|
||||
|
||||
request_data = {
|
||||
"model": version,
|
||||
"messages": messages,
|
||||
"temperature": model.get('temperature', 0.7),
|
||||
"top_p": model.get('topP', 1.0),
|
||||
"max_tokens": model.get('maxTokens', 2048),
|
||||
"stream": False # 强制使用非流式响应
|
||||
}
|
||||
|
||||
# 设置请求头 - 根据是否有API密钥决定认证方式
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 只有在有API密钥时才添加认证头
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
try:
|
||||
# 发送API请求 - 只支持OpenAI兼容格式
|
||||
timeout = httpx.Timeout(30)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
# 记录请求信息(隐藏API密钥)
|
||||
masked_headers = {k: (v[:10] + '...' if k == 'Authorization' and len(v) > 10 else v) for k, v in headers.items()}
|
||||
logger.info(f"发送请求到: {api_url.rstrip('/')}/chat/completions")
|
||||
logger.info(f"请求头: {masked_headers}")
|
||||
logger.info(f"请求体: {request_data}")
|
||||
|
||||
response = await client.post(
|
||||
f"{api_url.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json=request_data
|
||||
)
|
||||
|
||||
# 检查响应
|
||||
logger.info(f"响应状态码: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info(f"响应内容: {result}")
|
||||
|
||||
# 解析响应
|
||||
if "choices" in result and len(result["choices"]) > 0:
|
||||
message = result["choices"][0]["message"]
|
||||
content = message.get("content", "") or ""
|
||||
logger.info(f"原始响应内容: {message}")
|
||||
logger.info(f"解析成功,收到回复长度: {len(content)}")
|
||||
|
||||
# 如果内容为空,记录警告但仍然返回成功
|
||||
if not content:
|
||||
logger.warning("API返回的内容为空")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'model': str(model.get('name', 'unknown')),
|
||||
'content': content,
|
||||
'usage': result.get('usage', {}),
|
||||
'raw_response': result
|
||||
}
|
||||
else:
|
||||
logger.error(f"API响应格式异常: {result}")
|
||||
return {'success': False, 'error': 'API响应格式异常'}
|
||||
else:
|
||||
# 获取错误信息
|
||||
try:
|
||||
error_detail = response.json()
|
||||
error_detail_str = str(error_detail) if error_detail is not None else '无错误详情'
|
||||
logger.error(f"API错误响应: {error_detail}")
|
||||
except:
|
||||
error_detail_str = response.text or '未知错误'
|
||||
logger.error(f"API错误响应文本: {error_detail_str}")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'API调用失败 ({response.status_code}): {error_detail_str}'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e) if e is not None else '未知异常'
|
||||
return {'success': False, 'error': f'调用异常: {error_str}'}
|
||||
|
||||
@staticmethod
|
||||
def test_model(model_id: str) -> Dict[str, Any]:
|
||||
"""测试模型连接 - 使用真实的API调用"""
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
# 使用新线程中的事件循环
|
||||
result_holder = {}
|
||||
|
||||
def run_test():
|
||||
"""在新线程中运行测试"""
|
||||
try:
|
||||
# 直接使用asyncio.run(),不手动设置事件循环
|
||||
test_result = asyncio.run(
|
||||
ModelManager.call_model(
|
||||
model_id=model_id,
|
||||
prompt="你好,请回复'测试成功'"
|
||||
)
|
||||
)
|
||||
|
||||
result_holder['success'] = True
|
||||
result_holder['result'] = test_result
|
||||
|
||||
except Exception as e:
|
||||
result_holder['success'] = False
|
||||
result_holder['error'] = str(e)
|
||||
|
||||
# 在新线程中运行
|
||||
thread = threading.Thread(target=run_test)
|
||||
thread.start()
|
||||
thread.join()
|
||||
|
||||
# 检查结果
|
||||
if not result_holder.get('success'):
|
||||
error = result_holder.get('error', '未知错误')
|
||||
logger.error(f"测试模型连接时发生错误: {error}")
|
||||
|
||||
# 更新模型状态为连接失败
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if model:
|
||||
ModelManager.update_model(model_id, {'status': '连接失败'})
|
||||
|
||||
return {
|
||||
'model_id': model_id,
|
||||
'success': False,
|
||||
'status': '连接失败',
|
||||
'message': f'测试过程出错: {error}',
|
||||
'error': error
|
||||
}
|
||||
|
||||
test_result = result_holder.get('result', {})
|
||||
|
||||
# 检查测试结果
|
||||
success = test_result.get('success', False)
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
|
||||
# 更新模型状态
|
||||
if model:
|
||||
if success:
|
||||
model['status'] = '已测试'
|
||||
message = '连接测试成功'
|
||||
else:
|
||||
model['status'] = '连接失败'
|
||||
error = test_result.get("error")
|
||||
message = f'连接测试失败: {str(error) if error else "未知错误"}'
|
||||
|
||||
# 保存更新
|
||||
ModelManager.update_model(model_id, {'status': model['status']})
|
||||
|
||||
return {
|
||||
'model_id': model_id,
|
||||
'success': success,
|
||||
'status': model['status'] if model else '未知',
|
||||
'message': message,
|
||||
'test_result': test_result
|
||||
}
|
||||
|
||||
|
||||
class ModelsAPI(BaseAPI):
|
||||
"""模型配置管理API"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化"""
|
||||
super().__init__()
|
||||
self.logger.info("ModelsAPI 初始化完成")
|
||||
|
||||
# API路由定义
|
||||
|
||||
@get("/")
|
||||
async def get_models(self):
|
||||
"""获取所有模型配置"""
|
||||
try:
|
||||
models = ModelManager.load_models()
|
||||
return self.success({
|
||||
'models': models,
|
||||
'total': len(models)
|
||||
}, "获取模型配置成功")
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取模型配置失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@get("/{model_id}")
|
||||
async def get_model(self, model_id: str):
|
||||
"""获取单个模型配置"""
|
||||
try:
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
return self.success(model, "获取模型配置成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取模型配置失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@post("/")
|
||||
async def create_model(self, model_data: Dict[str, Any] = Body(...)):
|
||||
"""创建新模型配置"""
|
||||
try:
|
||||
# 验证必填字段
|
||||
if not model_data.get('name'):
|
||||
raise HTTPException(status_code=400, detail="模型名称不能为空")
|
||||
|
||||
new_model = ModelManager.create_model(model_data)
|
||||
|
||||
return self.success(new_model, "创建模型配置成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"创建模型配置失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@put("/{model_id}")
|
||||
async def update_model(self, model_id: str, model_data: Dict[str, Any] = Body(...)):
|
||||
"""更新模型配置"""
|
||||
try:
|
||||
# 验证模型是否存在
|
||||
existing_model = ModelManager.get_model_by_id(model_id)
|
||||
if not existing_model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
updated_model = ModelManager.update_model(model_id, model_data)
|
||||
|
||||
return self.success(updated_model, "更新模型配置成功")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"更新模型配置失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@delete("/{model_id}")
|
||||
async def delete_model(self, model_id: str):
|
||||
"""删除模型配置"""
|
||||
try:
|
||||
# 验证模型是否存在
|
||||
existing_model = ModelManager.get_model_by_id(model_id)
|
||||
if not existing_model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
success = ModelManager.delete_model(model_id)
|
||||
|
||||
if success:
|
||||
return self.success(None, "删除模型配置成功")
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="删除模型配置失败")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"删除模型配置失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@post("/{model_id}/test")
|
||||
async def test_model_connection(self, model_id: str):
|
||||
"""测试模型连接"""
|
||||
try:
|
||||
# 直接调用异步的call_model方法
|
||||
result = await ModelManager.call_model(
|
||||
model_id=model_id,
|
||||
prompt="你好,请回复'测试成功'"
|
||||
)
|
||||
|
||||
# 更新模型状态
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if model:
|
||||
if result.get('success'):
|
||||
model['status'] = '已测试'
|
||||
message = '连接测试成功'
|
||||
else:
|
||||
model['status'] = '连接失败'
|
||||
error = result.get("error")
|
||||
message = f'连接测试失败: {str(error) if error else "未知错误"}'
|
||||
|
||||
# 保存更新
|
||||
ModelManager.update_model(model_id, {'status': model['status']})
|
||||
|
||||
test_result = {
|
||||
'model_id': model_id,
|
||||
'success': result.get('success', False),
|
||||
'status': model['status'] if model else '未知',
|
||||
'message': message,
|
||||
'test_result': result
|
||||
}
|
||||
|
||||
return self.success(test_result, message)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"测试模型连接失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@post("/{model_id}/call")
|
||||
async def call_model_api(self, model_id: str, request_data: Dict[str, Any] = Body(...)):
|
||||
"""调用模型进行对话(使用非流式响应)"""
|
||||
try:
|
||||
# 获取请求参数
|
||||
prompt = request_data.get('prompt', '')
|
||||
system_prompt = request_data.get('systemPrompt', None)
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=400, detail="提示词不能为空")
|
||||
|
||||
# 强制使用非流式调用
|
||||
result = await ModelManager.call_model(
|
||||
model_id=model_id,
|
||||
prompt=prompt,
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
return self.success(result, "模型调用成功" if result.get('success') else "模型调用失败")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"调用模型失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@post("/batch-delete")
|
||||
async def batch_delete_models(self, model_ids: List[str] = Body(...)):
|
||||
"""批量删除模型"""
|
||||
try:
|
||||
deleted_count = 0
|
||||
failed_count = 0
|
||||
|
||||
for model_id in model_ids:
|
||||
if ModelManager.delete_model(model_id):
|
||||
deleted_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
|
||||
return self.success({
|
||||
'deleted_count': deleted_count,
|
||||
'failed_count': failed_count
|
||||
}, f'批量删除完成: 成功 {deleted_count} 个, 失败 {failed_count} 个')
|
||||
except Exception as e:
|
||||
self.logger.error(f"批量删除模型失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@get("/providers/list")
|
||||
async def get_model_providers(self):
|
||||
"""获取模型提供方列表"""
|
||||
providers = [
|
||||
{'value': 'openai', 'label': 'OpenAI'},
|
||||
{'value': 'anthropic', 'label': 'Anthropic'},
|
||||
{'value': 'azure', 'label': 'Azure OpenAI'},
|
||||
{'value': 'local', 'label': '本地部署'},
|
||||
{'value': 'custom', 'label': '自定义'}
|
||||
]
|
||||
|
||||
return self.success(providers, "获取提供方列表成功")
|
||||
|
||||
@get("/types/list")
|
||||
async def get_model_types(self):
|
||||
"""获取模型类型列表"""
|
||||
types = [
|
||||
{'value': 'gpt', 'label': 'GPT 系列'},
|
||||
{'value': 'bert', 'label': 'BERT 系列'},
|
||||
{'value': 'llama', 'label': 'LLaMA 系列'},
|
||||
{'value': 'claude', 'label': 'Claude 系列'},
|
||||
{'value': 'custom', 'label': '自定义'}
|
||||
]
|
||||
|
||||
return self.success(types, "获取模型类型列表成功")
|
||||
|
||||
@get("/status/list")
|
||||
async def get_model_statuses(self):
|
||||
"""获取模型状态列表"""
|
||||
statuses = [
|
||||
{'value': '已测试', 'label': '已测试', 'color': 'green'},
|
||||
{'value': '连接失败', 'label': '连接失败', 'color': 'red'},
|
||||
{'value': '未测试', 'label': '未测试', 'color': 'gray'}
|
||||
]
|
||||
|
||||
return self.success(statuses, "获取模型状态列表成功")
|
||||
|
||||
|
||||
# 创建API实例
|
||||
models_api = ModelsAPI()
|
||||
3
request/src/config/__init__.py
Normal file
3
request/src/config/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .settings import settings
|
||||
|
||||
__all__ = ["settings"]
|
||||
73
request/src/config/settings.py
Normal file
73
request/src/config/settings.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 应用配置
|
||||
app_name: str = "X-Request API Framework"
|
||||
app_version: str = "1.0.0"
|
||||
debug: bool = False
|
||||
|
||||
# 服务器配置
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 3000 # 默认使用config.json中的端口
|
||||
workers: int = 1
|
||||
|
||||
# 日志配置
|
||||
log_level: str = "INFO"
|
||||
log_file: Optional[str] = "logs/app.log" # 默认写入日志文件
|
||||
log_format: str = "json" # json 或 console
|
||||
log_to_console: bool = False # 是否同时输出到控制台
|
||||
|
||||
# 高级日志配置
|
||||
advanced_logging: bool = True # 是否启用高级日志系统
|
||||
logs_dir: str = "logs" # 日志目录
|
||||
max_log_days: int = 30 # 日志文件保存天数
|
||||
enable_log_cleanup: bool = True # 是否启用自动日志清理
|
||||
route_based_logging: bool = True # 是否启用基于路由的日志分类
|
||||
|
||||
# 性能配置
|
||||
max_requests: int = 1000
|
||||
max_connections: int = 1000
|
||||
request_timeout: int = 30
|
||||
|
||||
# CORS配置
|
||||
cors_origins: list[str] = ["*"]
|
||||
cors_methods: list[str] = ["*"]
|
||||
cors_headers: list[str] = ["*"]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# 尝试读取根目录的config.json配置文件
|
||||
try:
|
||||
# 获取项目根目录(相对于当前文件的路径)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.abspath(os.path.join(current_dir, "..", ".."))
|
||||
config_path = os.path.join(root_dir, "config.json")
|
||||
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# 从config.json读取配置,覆盖默认配置
|
||||
if 'backend' in config:
|
||||
backend_config = config['backend']
|
||||
if 'port' in backend_config:
|
||||
self.port = int(backend_config['port'])
|
||||
if 'host' in backend_config:
|
||||
self.host = backend_config['host']
|
||||
if 'log_level' in backend_config:
|
||||
self.log_level = backend_config['log_level']
|
||||
except Exception as e:
|
||||
# 如果读取配置文件失败,使用默认配置
|
||||
print(f"警告: 无法读取配置文件 config.json,使用默认配置. 错误: {e}")
|
||||
|
||||
|
||||
# 全局设置实例
|
||||
settings = Settings()
|
||||
3
request/src/core/__init__.py
Normal file
3
request/src/core/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .app import create_app, get_app
|
||||
|
||||
__all__ = ["create_app", "get_app"]
|
||||
345
request/src/core/app.py
Normal file
345
request/src/core/app.py
Normal file
@@ -0,0 +1,345 @@
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from contextlib import asynccontextmanager
|
||||
from threading import Lock
|
||||
from typing import Dict, Any, Optional
|
||||
import time
|
||||
import uuid
|
||||
from ..config import settings
|
||||
from ..utils import get_logger
|
||||
from ..middleware.logging import LoggingMiddleware
|
||||
from ..middleware.error_handling import ErrorHandlingMiddleware
|
||||
from ..middleware.performance import PerformanceMiddleware
|
||||
from ..utils.advanced_logger import advanced_logger_manager
|
||||
from ..utils.doc_generator import generate_docs
|
||||
from ..models.response import StandardResponse
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时执行
|
||||
logger.info(
|
||||
"应用启动",
|
||||
app_name=settings.app_name,
|
||||
version=settings.app_version,
|
||||
debug=settings.debug,
|
||||
host=settings.host,
|
||||
port=settings.port
|
||||
)
|
||||
|
||||
# 这里可以添加初始化数据库连接、缓存等操作
|
||||
await initialize_app()
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时执行
|
||||
logger.info("应用关闭")
|
||||
await cleanup_app()
|
||||
|
||||
|
||||
async def initialize_app():
|
||||
"""初始化应用"""
|
||||
# 初始化高级日志管理器
|
||||
if settings.advanced_logging:
|
||||
logger.info("初始化高级日志系统")
|
||||
try:
|
||||
# 计划日志清理任务
|
||||
if settings.enable_log_cleanup:
|
||||
advanced_logger_manager.schedule_cleanup()
|
||||
logger.info(f"日志自动清理已启用,保留天数: {settings.max_log_days}")
|
||||
|
||||
# 获取初始日志统计
|
||||
stats = advanced_logger_manager.get_log_stats()
|
||||
logger.info("日志系统初始化完成", log_stats=stats)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("高级日志系统初始化失败", error=str(e))
|
||||
|
||||
# 初始化数据库连接
|
||||
# 初始化缓存
|
||||
# 初始化其他资源
|
||||
|
||||
|
||||
async def cleanup_app():
|
||||
"""清理应用资源"""
|
||||
# 清理高级日志管理器资源
|
||||
if settings.advanced_logging:
|
||||
logger.info("清理高级日志系统资源")
|
||||
try:
|
||||
# 清理日志管理器资源
|
||||
from ..utils.advanced_logger import advanced_logger_manager
|
||||
advanced_logger_manager.__del__()
|
||||
logger.info("高级日志系统资源清理完成")
|
||||
except Exception as e:
|
||||
logger.error("高级日志系统资源清理失败", error=str(e))
|
||||
|
||||
# 关闭数据库连接
|
||||
# 清理缓存
|
||||
# 清理其他资源
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""创建FastAPI应用实例"""
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
description="高性能、高并发的请求框架",
|
||||
debug=settings.debug,
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs", # Swagger UI 始终可用
|
||||
redoc_url="/redoc", # ReDoc 始终可用
|
||||
openapi_url="/openapi.json", # OpenAPI JSON 始终可用
|
||||
)
|
||||
|
||||
# 添加中间件(注意顺序很重要)
|
||||
setup_middleware(app)
|
||||
|
||||
# 添加异常处理器
|
||||
setup_exception_handlers(app)
|
||||
|
||||
# 添加路由
|
||||
setup_routes(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def setup_middleware(app: FastAPI):
|
||||
"""设置中间件"""
|
||||
|
||||
# CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=settings.cors_methods,
|
||||
allow_headers=settings.cors_headers,
|
||||
)
|
||||
|
||||
# Gzip压缩中间件
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
|
||||
# 重新启用所有中间件
|
||||
app.add_middleware(PerformanceMiddleware)
|
||||
app.add_middleware(LoggingMiddleware)
|
||||
app.add_middleware(ErrorHandlingMiddleware)
|
||||
|
||||
|
||||
def setup_exception_handlers(app: FastAPI):
|
||||
"""设置异常处理器"""
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""全局异常处理器"""
|
||||
request_id = getattr(request.state, "request_id", "unknown")
|
||||
|
||||
logger.error(
|
||||
"未处理的异常",
|
||||
request_id=request_id,
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
error=str(exc),
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"success": False,
|
||||
"message": "服务器内部错误",
|
||||
"request_id": request_id,
|
||||
"error": str(exc) if settings.debug else "Internal Server Error"
|
||||
}
|
||||
)
|
||||
|
||||
@app.exception_handler(ValueError)
|
||||
async def value_error_handler(request: Request, exc: ValueError):
|
||||
"""值错误处理器"""
|
||||
request_id = getattr(request.state, "request_id", "unknown")
|
||||
|
||||
logger.warning(
|
||||
"值错误",
|
||||
request_id=request_id,
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
error=str(exc)
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"success": False,
|
||||
"message": "请求参数错误",
|
||||
"request_id": request_id,
|
||||
"error": str(exc)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_routes(app: FastAPI):
|
||||
"""设置路由"""
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径 - 返回主界面"""
|
||||
return FileResponse("../web/main.html")
|
||||
|
||||
@app.get("/dashboard")
|
||||
async def dashboard():
|
||||
"""前端监控界面"""
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
response_data = {
|
||||
"status": "healthy",
|
||||
"service": settings.app_name
|
||||
}
|
||||
return StandardResponse.success(response_data)
|
||||
|
||||
@app.get("/info")
|
||||
async def app_info():
|
||||
"""应用信息"""
|
||||
response_data = {
|
||||
"app_name": settings.app_name,
|
||||
"version": settings.app_version,
|
||||
"debug": settings.debug,
|
||||
"host": settings.host,
|
||||
"port": settings.port
|
||||
}
|
||||
return StandardResponse.success(response_data)
|
||||
|
||||
# 添加路由信息接口
|
||||
@app.get("/routes")
|
||||
async def get_routes_info():
|
||||
"""获取所有路由信息"""
|
||||
from ..api.internal.discovery import get_registered_modules_info
|
||||
|
||||
response_data = {
|
||||
"app_routes": [
|
||||
{
|
||||
"path": route.path,
|
||||
"methods": list(route.methods),
|
||||
"name": route.name
|
||||
}
|
||||
for route in app.routes
|
||||
if hasattr(route, 'path')
|
||||
],
|
||||
"api_modules": get_registered_modules_info()
|
||||
}
|
||||
|
||||
return StandardResponse.success(response_data)
|
||||
|
||||
# 自动发现并注册 API 模块
|
||||
from ..api.internal.discovery import auto_register_routes
|
||||
|
||||
registration_result = auto_register_routes(app)
|
||||
|
||||
# SSE模拟大模型输出端点(在路由注册之后添加)
|
||||
@app.get("/sse")
|
||||
async def sse_demo():
|
||||
"""SSE模拟大模型流式输出演示"""
|
||||
from fastapi.responses import StreamingResponse
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
async def generate_ai_response():
|
||||
"""模拟大模型的流式输出"""
|
||||
# 模拟大模型回答关于人工智能的内容
|
||||
response_parts = [
|
||||
"人工智能(AI)是计算机科学的一个分支,",
|
||||
"它试图理解智能的实质,",
|
||||
"并生产出一种新的能以人类智能相似的方式做出反应的智能机器。",
|
||||
"包括机器人、语言识别、图像识别、自然语言处理和专家系统等。",
|
||||
"",
|
||||
"AI的发展可以追溯到1956年,",
|
||||
"当时在达特茅斯会议上,",
|
||||
"约翰·麦卡锡首次提出了'人工智能'这一术语。",
|
||||
"",
|
||||
"经过几十年的发展,",
|
||||
"AI已经取得了巨大的进步,",
|
||||
"特别是在深度学习、神经网络和大数据的推动下,",
|
||||
"AI技术已经广泛应用于各个领域,",
|
||||
"包括医疗、金融、交通、教育、娱乐等。",
|
||||
"",
|
||||
"未来,AI将继续发展,",
|
||||
"有望在更多领域发挥重要作用,",
|
||||
"但同时也需要关注其带来的挑战,",
|
||||
"如隐私保护、伦理问题、就业影响等。",
|
||||
"",
|
||||
"总的来说,",
|
||||
"人工智能是人类智慧的结晶,",
|
||||
"它将继续推动社会进步,",
|
||||
"但我们也需要谨慎地对待它的发展和应用。"
|
||||
]
|
||||
|
||||
for part in response_parts:
|
||||
# 每部分作为一条消息发送
|
||||
yield f"data: {json.dumps({'content': part, 'done': False})}\n\n"
|
||||
# 模拟生成延迟
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 发送完成信号
|
||||
yield f"data: {json.dumps({'content': '', 'done': True})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate_ai_response(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "*"
|
||||
}
|
||||
)
|
||||
|
||||
# 挂载静态文件目录
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
# 只挂载静态资源(CSS, JS, 图片等)- 如果目录存在
|
||||
import os
|
||||
if os.path.exists("static"):
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
# 生成离线API文档
|
||||
generate_docs(app)
|
||||
|
||||
if registration_result["success"]:
|
||||
logger.info(
|
||||
"API模块自动注册完成",
|
||||
registered_modules=registration_result["registered_modules"],
|
||||
total_files=registration_result["total_files"]
|
||||
)
|
||||
else:
|
||||
logger.error("API模块自动注册失败", error=registration_result.get("error"))
|
||||
|
||||
|
||||
# 应用实例缓存
|
||||
_app_instance: Optional[FastAPI] = None
|
||||
# 用于确保线程安全的锁
|
||||
_app_lock = Lock()
|
||||
|
||||
|
||||
# 应用工厂函数
|
||||
def get_app() -> FastAPI:
|
||||
"""获取应用实例(线程安全的单例模式)"""
|
||||
global _app_instance
|
||||
# 双重检查锁定模式,确保线程安全
|
||||
if _app_instance is None:
|
||||
with _app_lock:
|
||||
if _app_instance is None:
|
||||
_app_instance = create_app()
|
||||
return _app_instance
|
||||
|
||||
# 导出应用实例
|
||||
app = get_app()
|
||||
5
request/src/middleware/__init__.py
Normal file
5
request/src/middleware/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .logging import LoggingMiddleware
|
||||
from .error_handling import ErrorHandlingMiddleware
|
||||
from .performance import PerformanceMiddleware
|
||||
|
||||
__all__ = ["LoggingMiddleware", "ErrorHandlingMiddleware", "PerformanceMiddleware"]
|
||||
59
request/src/middleware/error_handling.py
Normal file
59
request/src/middleware/error_handling.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.types import ASGIApp
|
||||
import json
|
||||
from typing import Callable
|
||||
from ..utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ErrorHandlingMiddleware(BaseHTTPMiddleware):
|
||||
"""错误处理中间件"""
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
super().__init__(app)
|
||||
self.logger = get_logger("error_handling")
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""处理请求并捕获异常"""
|
||||
try:
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
request_id = getattr(request.state, "request_id", "unknown")
|
||||
|
||||
# 记录详细错误信息
|
||||
self.logger.error(
|
||||
"中间件捕获到未处理异常",
|
||||
request_id=request_id,
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
query_params=str(request.query_params),
|
||||
error_type=type(e).__name__,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
# 返回友好的错误响应
|
||||
return self._create_error_response(
|
||||
request_id=request_id,
|
||||
error_message="服务器内部错误",
|
||||
status_code=500
|
||||
)
|
||||
|
||||
def _create_error_response(self, request_id: str, error_message: str, status_code: int) -> Response:
|
||||
"""创建错误响应"""
|
||||
error_data = {
|
||||
"success": False,
|
||||
"message": error_message,
|
||||
"request_id": request_id,
|
||||
"timestamp": json.dumps({"timestamp": "now"}) # 简单的时间戳
|
||||
}
|
||||
|
||||
return Response(
|
||||
content=json.dumps(error_data, ensure_ascii=False),
|
||||
status_code=status_code,
|
||||
media_type="application/json"
|
||||
)
|
||||
388
request/src/middleware/logging.py
Normal file
388
request/src/middleware/logging.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.types import ASGIApp, Receive
|
||||
import time
|
||||
import uuid
|
||||
import json
|
||||
from typing import Callable, Optional
|
||||
from ..utils import get_logger
|
||||
from ..utils.advanced_logger import get_route_logger, advanced_logger_manager
|
||||
from ..config import settings
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
"""请求日志中间件"""
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
super().__init__(app)
|
||||
self.logger = get_logger("request")
|
||||
self.use_advanced_logging = settings.advanced_logging
|
||||
self.use_route_based_logging = settings.route_based_logging
|
||||
|
||||
# 不记录日志的路径模式
|
||||
SKIP_LOG_PATTERNS = (
|
||||
'.html', '.css', '.js', '.ico', '.png', '.jpg', '.jpeg',
|
||||
'.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.map'
|
||||
)
|
||||
|
||||
# 不记录日志的路径前缀
|
||||
SKIP_LOG_PREFIXES = (
|
||||
'/static/', '/favicon', '/assets/', '/vendor/',
|
||||
'/docs', '/redoc', '/openapi.json', '/.well-known/'
|
||||
)
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""处理请求并记录日志"""
|
||||
# 生成请求ID
|
||||
request_id = str(uuid.uuid4())
|
||||
request.state.request_id = request_id
|
||||
|
||||
# 检查是否需要跳过日志记录(静态资源)
|
||||
path = request.url.path.lower()
|
||||
skip_logging = (
|
||||
path.endswith(self.SKIP_LOG_PATTERNS) or
|
||||
any(path.startswith(prefix) for prefix in self.SKIP_LOG_PREFIXES)
|
||||
)
|
||||
|
||||
# 记录请求开始时间
|
||||
start_time = time.time()
|
||||
|
||||
# 获取客户端IP
|
||||
client_ip = self._get_client_ip(request)
|
||||
|
||||
# 获取用户代理
|
||||
user_agent = request.headers.get("user-agent", "Unknown")
|
||||
|
||||
# 获取路由名称
|
||||
route_name = self._extract_route_name(request)
|
||||
|
||||
# 对于POST/PUT/PATCH请求,需要缓存请求体以便后续使用
|
||||
body_bytes = None
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
body_bytes = await request.body()
|
||||
# 保存原始的 state 内容
|
||||
original_state = {}
|
||||
for key, value in request.state.__dict__.items():
|
||||
original_state[key] = value
|
||||
|
||||
# 重新创建请求对象,将缓存的请求体设置回去
|
||||
# 使用闭包来跟踪是否已经返回了body
|
||||
body_sent = False
|
||||
async def receive() -> dict:
|
||||
nonlocal body_sent
|
||||
if not body_sent:
|
||||
body_sent = True
|
||||
return {"type": "http.request", "body": body_bytes}
|
||||
else:
|
||||
return {"type": "http.request", "body": b""}
|
||||
|
||||
# 创建新的 scope,保留原始的 state
|
||||
# 使用 dict() 创建新的字典,避免修改原始 scope
|
||||
new_scope = dict(request.scope)
|
||||
# 确保 state 存在
|
||||
if "state" not in new_scope:
|
||||
new_scope["state"] = {}
|
||||
# 恢复原始的 state 内容
|
||||
for key, value in original_state.items():
|
||||
new_scope["state"][key] = value
|
||||
|
||||
new_request = Request(new_scope, receive)
|
||||
request = new_request
|
||||
|
||||
# 不再记录请求开始日志,只在响应时记录一次
|
||||
|
||||
try:
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# 读取响应数据(需要复制响应,因为响应流只能读取一次)
|
||||
response_body = b""
|
||||
async for chunk in response.body_iterator:
|
||||
response_body += chunk
|
||||
|
||||
# 创建新的响应对象,因为原始响应流已经被消耗
|
||||
new_response = Response(
|
||||
content=response_body,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
media_type=response.media_type
|
||||
)
|
||||
|
||||
# 尝试解析响应体
|
||||
response_data = None
|
||||
if response_body:
|
||||
try:
|
||||
# 只解析JSON响应
|
||||
if new_response.headers.get("content-type") and "application/json" in new_response.headers.get("content-type"):
|
||||
response_data = json.loads(response_body.decode("utf-8"))
|
||||
except Exception:
|
||||
# 如果无法解析,跳过
|
||||
pass
|
||||
|
||||
# 记录响应信息(现在会同时包含请求和响应的信息)
|
||||
if not skip_logging:
|
||||
if self.use_advanced_logging and self.use_route_based_logging:
|
||||
await self._log_response_advanced(
|
||||
request=request,
|
||||
response=new_response,
|
||||
response_data=response_data,
|
||||
route_name=route_name,
|
||||
request_id=request_id,
|
||||
process_time=process_time,
|
||||
client_ip=client_ip,
|
||||
body_bytes=body_bytes # 传递缓存的请求体
|
||||
)
|
||||
else:
|
||||
await self._log_response(
|
||||
request=request,
|
||||
response=new_response,
|
||||
response_data=response_data,
|
||||
request_id=request_id,
|
||||
process_time=process_time,
|
||||
client_ip=client_ip
|
||||
)
|
||||
|
||||
# 添加响应头
|
||||
new_response.headers["X-Request-ID"] = request_id
|
||||
new_response.headers["X-Process-Time"] = f"{process_time:.4f}"
|
||||
|
||||
return new_response
|
||||
|
||||
except Exception as e:
|
||||
# 计算处理时间
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# 静态资源异常也跳过日志记录
|
||||
if not skip_logging:
|
||||
if self.use_advanced_logging and self.use_route_based_logging:
|
||||
route_logger = get_route_logger(route_name)
|
||||
route_logger.log_request(
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status_code=500,
|
||||
process_time=process_time,
|
||||
client_ip=client_ip,
|
||||
request_id=request_id,
|
||||
user_agent=user_agent,
|
||||
query_params=dict(request.query_params),
|
||||
error=str(e)
|
||||
)
|
||||
else:
|
||||
self.logger.error(
|
||||
"请求处理异常",
|
||||
request_id=request_id,
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
query_params=str(request.query_params),
|
||||
client_ip=client_ip,
|
||||
process_time=f"{process_time:.4f}s",
|
||||
error=str(e),
|
||||
error_type=type(e).__name__
|
||||
)
|
||||
|
||||
# 重新抛出异常,让其他中间件或异常处理器处理
|
||||
raise
|
||||
|
||||
def _get_client_ip(self, request: Request) -> str:
|
||||
"""获取客户端IP地址"""
|
||||
# 检查代理头
|
||||
forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
# 取第一个IP(如果有多级代理)
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
|
||||
real_ip = request.headers.get("X-Real-IP")
|
||||
if real_ip:
|
||||
return real_ip
|
||||
|
||||
# 返回直连IP
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
async def _log_request(self, request: Request, request_id: str, client_ip: str, user_agent: str, body_bytes: Optional[bytes] = None):
|
||||
"""记录请求信息"""
|
||||
# 过滤敏感查询参数
|
||||
safe_query_params = self._filter_sensitive_params(dict(request.query_params))
|
||||
|
||||
log_data = {
|
||||
"request_id": request_id,
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"query_params": safe_query_params,
|
||||
"client_ip": client_ip,
|
||||
"user_agent": user_agent,
|
||||
"content_type": request.headers.get("content-type"),
|
||||
"content_length": request.headers.get("content-length"),
|
||||
}
|
||||
|
||||
# 如果是POST/PUT/PATCH请求,尝试记录请求体
|
||||
if request.method in ["POST", "PUT", "PATCH"] and body_bytes:
|
||||
try:
|
||||
body = self._parse_request_body(body_bytes, request.headers.get("content-type", ""), request_id)
|
||||
if body:
|
||||
log_data["request_body"] = body
|
||||
except Exception:
|
||||
# 如果无法解析请求体,跳过
|
||||
pass
|
||||
|
||||
self.logger.info(
|
||||
f"收到请求: {request.method} {request.url.path}",
|
||||
**log_data
|
||||
)
|
||||
|
||||
async def _log_response(self, request: Request, response: Response, response_data: Optional[dict], request_id: str, process_time: float, client_ip: str):
|
||||
"""记录响应信息"""
|
||||
log_data = {
|
||||
"request_id": request_id,
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status_code": response.status_code,
|
||||
"process_time": f"{process_time:.4f}s",
|
||||
"client_ip": client_ip,
|
||||
"content_type": response.headers.get("content-type"),
|
||||
"content_length": response.headers.get("content-length"),
|
||||
}
|
||||
|
||||
# 如果有响应数据,添加到日志中
|
||||
if response_data:
|
||||
log_data["response_body"] = response_data
|
||||
|
||||
# 根据状态码确定日志级别
|
||||
if response.status_code >= 500:
|
||||
log_level = "error"
|
||||
elif response.status_code >= 400:
|
||||
log_level = "warning"
|
||||
else:
|
||||
log_level = "info"
|
||||
|
||||
message = f"请求完成: {request.method} {request.url.path} - {response.status_code} ({process_time:.4f}s)"
|
||||
|
||||
getattr(self.logger, log_level)(message, **log_data)
|
||||
|
||||
def _parse_request_body(self, body_bytes: bytes, content_type: str, request_id: str) -> Optional[dict]:
|
||||
"""解析请求体(从字节数据)"""
|
||||
try:
|
||||
if not body_bytes:
|
||||
return None
|
||||
|
||||
if "application/json" in content_type:
|
||||
return json.loads(body_bytes.decode("utf-8"))
|
||||
|
||||
elif "application/x-www-form-urlencoded" in content_type:
|
||||
# 对于表单数据,返回原始字符串(因为已经读取了body,无法再使用request.form())
|
||||
return {"raw": body_bytes.decode("utf-8")}
|
||||
|
||||
# 对于其他内容类型,返回基本信息
|
||||
return {"type": content_type, "size": len(body_bytes)}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
"无法解析请求体",
|
||||
request_id=request_id,
|
||||
error=str(e)
|
||||
)
|
||||
return {"parse_error": str(e)}
|
||||
|
||||
def _filter_sensitive_params(self, params: dict) -> dict:
|
||||
"""过滤敏感查询参数"""
|
||||
sensitive_keys = {
|
||||
"password", "token", "secret", "key", "auth", "authorization",
|
||||
"api_key", "access_token", "refresh_token"
|
||||
}
|
||||
|
||||
filtered_params = {}
|
||||
for key, value in params.items():
|
||||
if any(sensitive in key.lower() for sensitive in sensitive_keys):
|
||||
filtered_params[key] = "***"
|
||||
else:
|
||||
filtered_params[key] = value
|
||||
|
||||
return filtered_params
|
||||
|
||||
def _extract_route_name(self, request: Request) -> str:
|
||||
"""从请求路径中提取路由名称"""
|
||||
path = request.url.path.strip('/')
|
||||
|
||||
if not path:
|
||||
return "root"
|
||||
|
||||
# 路径解析:/api/hello -> hello, /api/user/profile -> user
|
||||
path_parts = path.split('/')
|
||||
|
||||
# 如果有api前缀,跳过它
|
||||
if path_parts and path_parts[0] == 'api':
|
||||
path_parts = path_parts[1:]
|
||||
|
||||
# 取第一个主要部分作为路由名
|
||||
if path_parts:
|
||||
route_name = path_parts[0]
|
||||
# 清理路由名,只保留字母数字和下划线
|
||||
import re
|
||||
route_name = re.sub(r'[^a-zA-Z0-9_]', '', route_name)
|
||||
return route_name if route_name else "unknown"
|
||||
|
||||
return "unknown"
|
||||
|
||||
async def _log_request_advanced(self, request: Request, route_name: str, request_id: str, client_ip: str, user_agent: str, body_bytes: Optional[bytes] = None):
|
||||
"""使用高级日志系统记录请求信息"""
|
||||
route_logger = get_route_logger(route_name)
|
||||
|
||||
# 获取查询参数
|
||||
query_params = self._filter_sensitive_params(dict(request.query_params))
|
||||
|
||||
# 获取请求体(如果是POST/PUT/PATCH)
|
||||
request_body = None
|
||||
if request.method in ["POST", "PUT", "PATCH"] and body_bytes:
|
||||
try:
|
||||
request_body = self._parse_request_body(body_bytes, request.headers.get("content-type", ""), request_id)
|
||||
except Exception:
|
||||
request_body = {"parse_error": "Failed to parse request body"}
|
||||
|
||||
route_logger.log_info(
|
||||
f"收到请求: {request.method} {request.url.path}",
|
||||
request_id=request_id,
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
query_params=query_params,
|
||||
client_ip=client_ip,
|
||||
user_agent=user_agent,
|
||||
content_type=request.headers.get("content-type"),
|
||||
content_length=request.headers.get("content-length"),
|
||||
request_body=request_body
|
||||
)
|
||||
|
||||
async def _log_response_advanced(self, request: Request, response: Response, response_data: Optional[dict], route_name: str,
|
||||
request_id: str, process_time: float, client_ip: str, body_bytes: Optional[bytes] = None):
|
||||
"""使用高级日志系统记录完整的请求和响应信息"""
|
||||
route_logger = get_route_logger(route_name)
|
||||
|
||||
# 获取请求体
|
||||
request_body = None
|
||||
if request.method in ["POST", "PUT", "PATCH"] and body_bytes:
|
||||
try:
|
||||
request_body = self._parse_request_body(body_bytes, request.headers.get("content-type", ""), request_id)
|
||||
except Exception:
|
||||
request_body = {"parse_error": "Failed to parse request body"}
|
||||
|
||||
# 获取查询参数
|
||||
query_params = self._filter_sensitive_params(dict(request.query_params))
|
||||
|
||||
route_logger.log_request(
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status_code=response.status_code,
|
||||
process_time=process_time,
|
||||
client_ip=client_ip,
|
||||
request_id=request_id,
|
||||
user_agent=request.headers.get("user-agent", "Unknown"),
|
||||
query_params=query_params,
|
||||
request_body=request_body,
|
||||
response_body=response_data,
|
||||
content_type=request.headers.get("content-type"),
|
||||
content_length=request.headers.get("content-length")
|
||||
)
|
||||
65
request/src/middleware/performance.py
Normal file
65
request/src/middleware/performance.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from fastapi import Request, Response
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.types import ASGIApp
|
||||
import time
|
||||
from typing import Callable
|
||||
from ..utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PerformanceMiddleware(BaseHTTPMiddleware):
|
||||
"""性能监控中间件"""
|
||||
|
||||
# 类变量,用于存储实例引用
|
||||
_instance = None
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
super().__init__(app)
|
||||
self.logger = get_logger("performance")
|
||||
self.total_requests = 0
|
||||
self.requests_per_status = {}
|
||||
self.total_response_time = 0
|
||||
|
||||
# 保存实例引用
|
||||
PerformanceMiddleware._instance = self
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""处理请求并添加性能头"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
response_time = time.time() - start_time
|
||||
|
||||
# 添加性能头
|
||||
response.headers["X-Response-Time"] = f"{response_time:.4f}"
|
||||
|
||||
# 更新请求计数
|
||||
self.total_requests += 1
|
||||
status_code = response.status_code
|
||||
if status_code not in self.requests_per_status:
|
||||
self.requests_per_status[status_code] = 0
|
||||
self.requests_per_status[status_code] += 1
|
||||
|
||||
# 更新总响应时间
|
||||
self.total_response_time += response_time
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
raise
|
||||
|
||||
def get_stats(self):
|
||||
"""获取性能统计信息"""
|
||||
return {
|
||||
"total_requests": self.total_requests,
|
||||
"requests_per_status": self.requests_per_status,
|
||||
"total_response_time": self.total_response_time,
|
||||
"average_response_time": self.total_response_time / self.total_requests if self.total_requests > 0 else 0
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""获取中间件实例"""
|
||||
return cls._instance
|
||||
63
request/src/models/__init__.py
Normal file
63
request/src/models/__init__.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
统一数据模型模块
|
||||
"""
|
||||
|
||||
# 基础模型
|
||||
from .base.response import (
|
||||
BaseResponse,
|
||||
ErrorResponse,
|
||||
DataResponse,
|
||||
PaginatedResponse
|
||||
)
|
||||
|
||||
# 响应模型
|
||||
from .response import (
|
||||
StandardResponse,
|
||||
RequestInfo,
|
||||
ResponseInfo
|
||||
)
|
||||
|
||||
# 业务模块模型
|
||||
from .modules import (
|
||||
UserCreateRequest,
|
||||
UserUpdateRequest,
|
||||
UserResponse,
|
||||
HelloRequest,
|
||||
HelloResponse,
|
||||
GreetResponse,
|
||||
SceneRequest,
|
||||
SceneResponse,
|
||||
ExampleUserRequest,
|
||||
ExampleUserResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 基础响应模型
|
||||
"BaseResponse",
|
||||
"ErrorResponse",
|
||||
"DataResponse",
|
||||
"PaginatedResponse",
|
||||
|
||||
# 标准响应模型
|
||||
"StandardResponse",
|
||||
"RequestInfo",
|
||||
"ResponseInfo",
|
||||
|
||||
# 用户相关模型
|
||||
"UserCreateRequest",
|
||||
"UserUpdateRequest",
|
||||
"UserResponse",
|
||||
|
||||
# Hello API模型
|
||||
"HelloRequest",
|
||||
"HelloResponse",
|
||||
"GreetResponse",
|
||||
|
||||
# 场景相关模型
|
||||
"SceneRequest",
|
||||
"SceneResponse",
|
||||
|
||||
# 示例模型
|
||||
"ExampleUserRequest",
|
||||
"ExampleUserResponse",
|
||||
]
|
||||
0
request/src/models/base/__init__.py
Normal file
0
request/src/models/base/__init__.py
Normal file
37
request/src/models/base/response.py
Normal file
37
request/src/models/base/response.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
基础数据模型
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BaseResponse(BaseModel):
|
||||
"""基础响应模型"""
|
||||
success: bool = True
|
||||
message: str
|
||||
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
|
||||
|
||||
|
||||
class ErrorResponse(BaseResponse):
|
||||
"""错误响应模型"""
|
||||
success: bool = False
|
||||
message: str
|
||||
code: str
|
||||
timestamp: float = Field(default_factory=lambda: datetime.now().timestamp())
|
||||
|
||||
|
||||
class DataResponse(BaseResponse):
|
||||
"""数据响应模型"""
|
||||
data: Any
|
||||
|
||||
|
||||
class PaginatedResponse(BaseResponse):
|
||||
"""分页响应模型"""
|
||||
data: Dict[str, Any] # 包含 items 和 pagination
|
||||
|
||||
|
||||
|
||||
|
||||
49
request/src/models/modules/__init__.py
Normal file
49
request/src/models/modules/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
业务模块数据模型
|
||||
"""
|
||||
|
||||
from .user import (
|
||||
UserCreateRequest,
|
||||
UserUpdateRequest,
|
||||
UserResponse
|
||||
)
|
||||
|
||||
from .hello import (
|
||||
HelloRequest,
|
||||
HelloResponse,
|
||||
GreetResponse
|
||||
)
|
||||
|
||||
from .scene import (
|
||||
SceneRequest,
|
||||
SceneResponse
|
||||
)
|
||||
|
||||
from .example import (
|
||||
ExampleUserRequest,
|
||||
ExampleUserResponse
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 用户相关模型
|
||||
"UserCreateRequest",
|
||||
"UserUpdateRequest",
|
||||
"UserResponse",
|
||||
|
||||
# Hello API模型
|
||||
"HelloRequest",
|
||||
"HelloResponse",
|
||||
"GreetResponse",
|
||||
|
||||
# 场景相关模型
|
||||
"SceneRequest",
|
||||
"SceneResponse",
|
||||
|
||||
# 示例模型
|
||||
"ExampleUserRequest",
|
||||
"ExampleUserResponse",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
28
request/src/models/modules/example.py
Normal file
28
request/src/models/modules/example.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Example API 数据模型
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ExampleUserRequest(BaseModel):
|
||||
"""示例用户请求模型"""
|
||||
name: str = Field(..., description="用户姓名", min_length=1, max_length=50)
|
||||
email: str = Field(..., description="用户邮箱")
|
||||
age: Optional[int] = Field(None, ge=0, le=150, description="用户年龄")
|
||||
|
||||
|
||||
class ExampleUserResponse(BaseModel):
|
||||
"""示例用户响应模型"""
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
age: Optional[int]
|
||||
created_at: float
|
||||
|
||||
|
||||
|
||||
|
||||
30
request/src/models/modules/hello.py
Normal file
30
request/src/models/modules/hello.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Hello API 数据模型
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class HelloRequest(BaseModel):
|
||||
"""Hello请求模型"""
|
||||
name: str = Field(..., description="你的名字", min_length=1, max_length=50)
|
||||
message: Optional[str] = Field(None, description="自定义消息")
|
||||
|
||||
|
||||
class HelloResponse(BaseModel):
|
||||
"""Hello响应模型"""
|
||||
greeting: str
|
||||
message: str
|
||||
timestamp: str
|
||||
|
||||
|
||||
class GreetResponse(BaseModel):
|
||||
"""Greet响应模型"""
|
||||
message: str
|
||||
visitor_count: int
|
||||
|
||||
|
||||
|
||||
|
||||
27
request/src/models/modules/scene.py
Normal file
27
request/src/models/modules/scene.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
场景注册 API 数据模型
|
||||
"""
|
||||
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SceneRequest(BaseModel):
|
||||
"""场景注册请求模型"""
|
||||
name: str = Field(..., description="场景名称", min_length=1, max_length=100)
|
||||
description: Optional[str] = Field(None, description="场景描述")
|
||||
tags: List[str] = Field(default_factory=list, description="场景标签")
|
||||
|
||||
|
||||
class SceneResponse(BaseModel):
|
||||
"""场景响应模型"""
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
tags: List[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
|
||||
|
||||
36
request/src/models/modules/user.py
Normal file
36
request/src/models/modules/user.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
用户相关数据模型
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserCreateRequest(BaseModel):
|
||||
"""创建用户请求模型"""
|
||||
name: str = Field(..., description="用户姓名", min_length=1, max_length=50)
|
||||
email: str = Field(..., description="用户邮箱", pattern=r'^[^@]+@[^@]+\.[^@]+$')
|
||||
age: Optional[int] = Field(None, ge=0, le=150, description="用户年龄")
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
"""更新用户请求模型"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=50, description="用户姓名")
|
||||
email: Optional[str] = Field(None, pattern=r'^[^@]+@[^@]+\.[^@]+$', description="用户邮箱")
|
||||
age: Optional[int] = Field(None, ge=0, le=150, description="用户年龄")
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
"""用户响应模型"""
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
age: Optional[int]
|
||||
created_at: str
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
|
||||
|
||||
|
||||
19
request/src/models/response/__init__.py
Normal file
19
request/src/models/response/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
响应相关模型模块
|
||||
"""
|
||||
|
||||
from .standard import (
|
||||
StandardResponse,
|
||||
RequestInfo,
|
||||
ResponseInfo
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"StandardResponse",
|
||||
"RequestInfo",
|
||||
"ResponseInfo",
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
51
request/src/models/response/standard.py
Normal file
51
request/src/models/response/standard.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class StandardResponse(BaseModel):
|
||||
"""标准响应格式"""
|
||||
status: int # 0表示失败,1表示成功
|
||||
response: Any # 返回结果的内容,由每个接口自定义
|
||||
time: str # 当前时间
|
||||
|
||||
@classmethod
|
||||
def success(cls, data: Any = None) -> "StandardResponse":
|
||||
"""创建成功响应"""
|
||||
return cls(
|
||||
status=1,
|
||||
response=data if data is not None else {},
|
||||
time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def error(cls, error_msg: str = "", error_code: int = 0) -> "StandardResponse":
|
||||
"""创建错误响应"""
|
||||
return cls(
|
||||
status=0,
|
||||
response={
|
||||
"error": error_msg,
|
||||
"code": error_code
|
||||
},
|
||||
time=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
|
||||
class RequestInfo(BaseModel):
|
||||
"""请求信息模型"""
|
||||
method: str
|
||||
path: str
|
||||
query_params: Dict[str, Any] = {}
|
||||
headers: Dict[str, str] = {}
|
||||
body: Any = None
|
||||
|
||||
|
||||
class ResponseInfo(BaseModel):
|
||||
"""响应信息模型"""
|
||||
status_code: int
|
||||
headers: Dict[str, str] = {}
|
||||
body: Any = None
|
||||
|
||||
|
||||
|
||||
|
||||
1
request/src/services/__init__.py
Normal file
1
request/src/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 业务服务模块
|
||||
546
request/src/services/file_upload.py
Normal file
546
request/src/services/file_upload.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
文件上传服务
|
||||
提供文件上传、存储、管理等功能
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import hashlib
|
||||
import aiofiles
|
||||
from typing import Dict, List, Optional, BinaryIO
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class FileUploadStatus(Enum):
|
||||
"""文件上传状态"""
|
||||
UPLOADING = "uploading"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class UploadedFile:
|
||||
"""已上传文件信息"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_id: str,
|
||||
filename: str,
|
||||
original_filename: str,
|
||||
file_path: str,
|
||||
file_size: int,
|
||||
content_type: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
):
|
||||
self.file_id = file_id
|
||||
self.filename = filename # 存储的文件名(带ID)
|
||||
self.original_filename = original_filename # 原始文件名
|
||||
self.file_path = file_path # 完整文件路径
|
||||
self.file_size = file_size
|
||||
self.content_type = content_type
|
||||
self.description = description
|
||||
self.status = FileUploadStatus.COMPLETED
|
||||
self.uploaded_at = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
self.download_count = 0
|
||||
self.file_hash = None # MD5哈希值(可选)
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"file_id": self.file_id,
|
||||
"filename": self.filename,
|
||||
"original_filename": self.original_filename,
|
||||
"file_path": self.file_path,
|
||||
"file_size": self.file_size,
|
||||
"content_type": self.content_type,
|
||||
"description": self.description,
|
||||
"status": self.status.value,
|
||||
"uploaded_at": self.uploaded_at,
|
||||
"updated_at": self.updated_at,
|
||||
"download_count": self.download_count,
|
||||
"file_hash": self.file_hash
|
||||
}
|
||||
|
||||
|
||||
class FileUploadService:
|
||||
"""文件上传服务"""
|
||||
|
||||
def __init__(self, upload_dir: str = "data"):
|
||||
"""
|
||||
初始化文件上传服务
|
||||
|
||||
Args:
|
||||
upload_dir: 文件上传目录,相对于项目根目录
|
||||
"""
|
||||
# 获取项目根目录(FT-Platform目录)
|
||||
# file_upload.py位于: X-Request/src/services/file_upload.py
|
||||
# 需要向上4级到达项目根目录: d:/Code/Project/FT-Platform
|
||||
current_file = Path(__file__)
|
||||
project_root = current_file.parent.parent.parent.parent
|
||||
|
||||
upload_path = Path(upload_dir)
|
||||
if upload_path.is_absolute():
|
||||
self.upload_dir = upload_path
|
||||
else:
|
||||
# 使用项目根目录的data文件夹
|
||||
self.upload_dir = project_root / upload_dir
|
||||
|
||||
self.upload_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 调试信息:打印实际的文件存储路径
|
||||
print(f"[FileUploadService] 文件存储路径: {self.upload_dir}")
|
||||
|
||||
# 文件信息存储(内存存储,可扩展为数据库)
|
||||
self.files: Dict[str, UploadedFile] = {}
|
||||
|
||||
# 配置
|
||||
self.max_file_size = 100 * 1024 * 1024 # 100MB 默认最大文件大小
|
||||
self.allowed_extensions = None # None表示允许所有扩展名
|
||||
|
||||
# 初始化时加载已存在的文件信息
|
||||
self._load_existing_files()
|
||||
|
||||
def _generate_file_id(self) -> str:
|
||||
"""生成唯一文件ID"""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def _load_existing_files(self):
|
||||
"""
|
||||
从文件名映射文件中加载已存在的文件信息
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
mapping_file = self.upload_dir / "filename_mapping.json"
|
||||
if not mapping_file.exists():
|
||||
print(f"[FileUploadService] 映射文件不存在,跳过加载")
|
||||
return
|
||||
|
||||
print(f"[FileUploadService] 正在加载映射文件: {mapping_file}")
|
||||
with open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
mapping_data = json.load(f)
|
||||
mappings = mapping_data.get("mappings", {})
|
||||
|
||||
loaded_count = 0
|
||||
for file_id, mapping_info in mappings.items():
|
||||
try:
|
||||
storage_filename = mapping_info.get("storage_filename", f"{file_id}.json")
|
||||
file_path = self.upload_dir / storage_filename
|
||||
|
||||
# 检查物理文件是否存在
|
||||
if not file_path.exists():
|
||||
print(f"[FileUploadService] 警告: 文件不存在 {file_path}")
|
||||
continue
|
||||
|
||||
# 获取文件大小
|
||||
file_size = file_path.stat().st_size
|
||||
|
||||
# 创建UploadedFile对象
|
||||
uploaded_file = UploadedFile(
|
||||
file_id=file_id,
|
||||
filename=storage_filename,
|
||||
original_filename=mapping_info.get("original_filename", storage_filename),
|
||||
file_path=str(file_path),
|
||||
file_size=file_size,
|
||||
content_type="application/json",
|
||||
description=mapping_info.get("original_filename", storage_filename)
|
||||
)
|
||||
|
||||
# 设置上传时间
|
||||
uploaded_at = mapping_info.get("uploaded_at")
|
||||
if uploaded_at:
|
||||
uploaded_file.uploaded_at = uploaded_at
|
||||
uploaded_file.updated_at = uploaded_at
|
||||
|
||||
# 计算文件哈希
|
||||
try:
|
||||
uploaded_file.file_hash = self._calculate_file_hash(file_path)
|
||||
except Exception:
|
||||
uploaded_file.file_hash = None
|
||||
|
||||
# 添加到内存字典
|
||||
self.files[file_id] = uploaded_file
|
||||
loaded_count += 1
|
||||
print(f"[FileUploadService] 已加载文件: {uploaded_file.original_filename} (ID: {file_id})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FileUploadService] 加载文件 {file_id} 时出错: {e}")
|
||||
continue
|
||||
|
||||
print(f"[FileUploadService] 加载完成,共加载 {loaded_count} 个文件")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[FileUploadService] 加载映射文件时出错: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _generate_storage_filename(self, original_filename: str, file_id: str) -> str:
|
||||
"""生成存储文件名"""
|
||||
# 获取文件扩展名
|
||||
ext = Path(original_filename).suffix
|
||||
# 使用文件ID + 扩展名
|
||||
return f"{file_id}{ext}"
|
||||
|
||||
def _calculate_file_hash(self, file_path: Path) -> str:
|
||||
"""计算文件MD5哈希值"""
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
|
||||
async def upload_file(
|
||||
self,
|
||||
file_content: bytes,
|
||||
original_filename: str,
|
||||
content_type: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> UploadedFile:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
Args:
|
||||
file_content: 文件内容(字节)
|
||||
original_filename: 原始文件名
|
||||
content_type: 文件MIME类型
|
||||
description: 文件描述
|
||||
|
||||
Returns:
|
||||
UploadedFile: 上传的文件信息
|
||||
|
||||
Raises:
|
||||
ValueError: 文件大小超限或扩展名不允许
|
||||
"""
|
||||
# 检查文件大小
|
||||
file_size = len(file_content)
|
||||
if file_size > self.max_file_size:
|
||||
raise ValueError(f"文件大小超过限制 ({self.max_file_size / 1024 / 1024:.0f}MB)")
|
||||
|
||||
# 检查文件扩展名
|
||||
if self.allowed_extensions:
|
||||
ext = Path(original_filename).suffix.lower()
|
||||
if ext not in self.allowed_extensions:
|
||||
raise ValueError(f"不允许的文件类型: {ext}")
|
||||
|
||||
# 生成文件ID和存储文件名
|
||||
file_id = self._generate_file_id()
|
||||
storage_filename = self._generate_storage_filename(original_filename, file_id)
|
||||
file_path = self.upload_dir / storage_filename
|
||||
|
||||
# 保存文件
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
await f.write(file_content)
|
||||
|
||||
# 计算文件哈希
|
||||
file_hash = self._calculate_file_hash(file_path)
|
||||
|
||||
# 创建文件信息对象
|
||||
uploaded_file = UploadedFile(
|
||||
file_id=file_id,
|
||||
filename=storage_filename,
|
||||
original_filename=original_filename,
|
||||
file_path=str(file_path),
|
||||
file_size=file_size,
|
||||
content_type=content_type,
|
||||
description=description
|
||||
)
|
||||
uploaded_file.file_hash = file_hash
|
||||
|
||||
# 存储文件信息
|
||||
self.files[file_id] = uploaded_file
|
||||
|
||||
# 更新文件名映射
|
||||
await self._update_filename_mapping(file_id, original_filename, storage_filename)
|
||||
|
||||
return uploaded_file
|
||||
|
||||
async def upload_file_stream(
|
||||
self,
|
||||
file_stream: BinaryIO,
|
||||
original_filename: str,
|
||||
content_type: Optional[str] = None,
|
||||
description: Optional[str] = None
|
||||
) -> UploadedFile:
|
||||
"""
|
||||
通过文件流上传文件(适用于大文件)
|
||||
|
||||
Args:
|
||||
file_stream: 文件流对象
|
||||
original_filename: 原始文件名
|
||||
content_type: 文件MIME类型
|
||||
description: 文件描述
|
||||
|
||||
Returns:
|
||||
UploadedFile: 上传的文件信息
|
||||
"""
|
||||
# 读取文件内容
|
||||
file_content = await file_stream.read()
|
||||
|
||||
# 使用普通上传方法
|
||||
return await self.upload_file(
|
||||
file_content=file_content,
|
||||
original_filename=original_filename,
|
||||
content_type=content_type,
|
||||
description=description
|
||||
)
|
||||
|
||||
def get_file(self, file_id: str) -> Optional[UploadedFile]:
|
||||
"""
|
||||
获取文件信息
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
UploadedFile: 文件信息,不存在返回None
|
||||
"""
|
||||
return self.files.get(file_id)
|
||||
|
||||
def get_file_path(self, file_id: str) -> Optional[Path]:
|
||||
"""
|
||||
获取文件的完整路径
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
Path: 文件路径对象,不存在返回None
|
||||
"""
|
||||
file_info = self.files.get(file_id)
|
||||
if not file_info:
|
||||
return None
|
||||
|
||||
return Path(file_info.file_path)
|
||||
|
||||
def get_all_files(self) -> List[UploadedFile]:
|
||||
"""
|
||||
获取所有文件信息
|
||||
|
||||
Returns:
|
||||
List[UploadedFile]: 文件信息列表
|
||||
"""
|
||||
return list(self.files.values())
|
||||
|
||||
def delete_file(self, file_id: str) -> bool:
|
||||
"""
|
||||
删除文件
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
bool: 删除是否成功
|
||||
"""
|
||||
file_info = self.files.get(file_id)
|
||||
if not file_info:
|
||||
return False
|
||||
|
||||
# 删除物理文件
|
||||
file_path = Path(file_info.file_path)
|
||||
if file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
except Exception:
|
||||
pass # 忽略删除错误
|
||||
|
||||
# 删除文件信息
|
||||
del self.files[file_id]
|
||||
|
||||
# 删除文件名映射
|
||||
import asyncio
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
# 如果在异步上下文中,使用create_task
|
||||
asyncio.create_task(self._remove_filename_mapping(file_id))
|
||||
else:
|
||||
# 否则直接运行
|
||||
loop.run_until_complete(self._remove_filename_mapping(file_id))
|
||||
except Exception:
|
||||
pass # 忽略错误
|
||||
|
||||
return True
|
||||
|
||||
def file_exists(self, file_id: str) -> bool:
|
||||
"""
|
||||
检查文件是否存在
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
bool: 文件是否存在
|
||||
"""
|
||||
return file_id in self.files
|
||||
|
||||
async def get_file_content(self, file_id: str) -> Optional[bytes]:
|
||||
"""
|
||||
获取文件内容
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
bytes: 文件内容,不存在返回None
|
||||
"""
|
||||
file_info = self.files.get(file_id)
|
||||
if not file_info:
|
||||
return None
|
||||
|
||||
file_path = Path(file_info.file_path)
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
async with aiofiles.open(file_path, 'rb') as f:
|
||||
content = await f.read()
|
||||
# 更新下载计数
|
||||
file_info.download_count += 1
|
||||
file_info.updated_at = datetime.now().isoformat()
|
||||
return content
|
||||
|
||||
def increment_download_count(self, file_id: str):
|
||||
"""
|
||||
增加下载计数
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
"""
|
||||
file_info = self.files.get(file_id)
|
||||
if file_info:
|
||||
file_info.download_count += 1
|
||||
file_info.updated_at = datetime.now().isoformat()
|
||||
|
||||
def update_file_description(self, file_id: str, description: str) -> bool:
|
||||
"""
|
||||
更新文件描述
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
description: 新描述
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
file_info = self.files.get(file_id)
|
||||
if not file_info:
|
||||
return False
|
||||
|
||||
file_info.description = description
|
||||
file_info.updated_at = datetime.now().isoformat()
|
||||
return True
|
||||
|
||||
def get_storage_statistics(self) -> Dict:
|
||||
"""
|
||||
获取存储统计信息
|
||||
|
||||
Returns:
|
||||
Dict: 统计信息
|
||||
"""
|
||||
total_files = len(self.files)
|
||||
total_size = sum(f.file_size for f in self.files.values())
|
||||
total_downloads = sum(f.download_count for f in self.files.values())
|
||||
|
||||
return {
|
||||
"total_files": total_files,
|
||||
"total_size": total_size,
|
||||
"total_size_mb": round(total_size / 1024 / 1024, 2),
|
||||
"total_downloads": total_downloads,
|
||||
"storage_dir": str(self.upload_dir)
|
||||
}
|
||||
|
||||
async def _update_filename_mapping(self, file_id: str, original_filename: str, storage_filename: str):
|
||||
"""
|
||||
更新文件名映射文件
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
original_filename: 原始文件名
|
||||
storage_filename: 存储文件名
|
||||
"""
|
||||
import json
|
||||
mapping_file = self.upload_dir / "filename_mapping.json"
|
||||
|
||||
# 读取现有映射
|
||||
mappings = {}
|
||||
if mapping_file.exists():
|
||||
try:
|
||||
async with aiofiles.open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
content = await f.read()
|
||||
data = json.loads(content)
|
||||
mappings = data.get("mappings", {})
|
||||
except Exception:
|
||||
mappings = {}
|
||||
|
||||
# 添加新映射
|
||||
mappings[file_id] = {
|
||||
"original_filename": original_filename,
|
||||
"storage_filename": storage_filename,
|
||||
"uploaded_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# 写入映射文件
|
||||
data = {"mappings": mappings}
|
||||
async with aiofiles.open(mapping_file, 'w', encoding='utf-8') as f:
|
||||
await f.write(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
async def _remove_filename_mapping(self, file_id: str):
|
||||
"""
|
||||
从映射文件中删除文件映射
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
"""
|
||||
import json
|
||||
mapping_file = self.upload_dir / "filename_mapping.json"
|
||||
|
||||
if not mapping_file.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
# 读取现有映射
|
||||
async with aiofiles.open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
content = await f.read()
|
||||
data = json.loads(content)
|
||||
mappings = data.get("mappings", {})
|
||||
|
||||
# 删除指定映射
|
||||
if file_id in mappings:
|
||||
del mappings[file_id]
|
||||
|
||||
# 写入映射文件
|
||||
data = {"mappings": mappings}
|
||||
async with aiofiles.open(mapping_file, 'w', encoding='utf-8') as f:
|
||||
await f.write(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
except Exception:
|
||||
pass # 忽略错误
|
||||
|
||||
def get_filename_mapping(self, file_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
获取文件名映射信息
|
||||
|
||||
Args:
|
||||
file_id: 文件ID
|
||||
|
||||
Returns:
|
||||
Dict: 映射信息,不存在返回None
|
||||
"""
|
||||
import json
|
||||
mapping_file = self.upload_dir / "filename_mapping.json"
|
||||
|
||||
if not mapping_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(mapping_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
mappings = data.get("mappings", {})
|
||||
return mappings.get(file_id)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# 全局文件上传服务实例
|
||||
file_upload_service = FileUploadService()
|
||||
169
request/src/services/fine_tune.py
Normal file
169
request/src/services/fine_tune.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
微调任务管理服务
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class FineTuneStatus(Enum):
|
||||
"""微调任务状态"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELED = "canceled"
|
||||
|
||||
|
||||
class FineTuneTask:
|
||||
"""微调任务"""
|
||||
|
||||
def __init__(self, task_id: str, name: str, description: str):
|
||||
self.task_id = task_id
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.status = FineTuneStatus.PENDING
|
||||
self.progress = 0.0
|
||||
self.created_at = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
self.logs: List[str] = []
|
||||
|
||||
def update_status(self, status: FineTuneStatus):
|
||||
"""更新任务状态"""
|
||||
self.status = status
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def update_progress(self, progress: float, log: str = None):
|
||||
"""更新任务进度"""
|
||||
self.progress = min(max(progress, 0.0), 100.0)
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
if log:
|
||||
self.logs.append(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {log}")
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"status": self.status.value,
|
||||
"progress": self.progress,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"logs": self.logs
|
||||
}
|
||||
|
||||
|
||||
class FineTuneManager:
|
||||
"""微调任务管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tasks: Dict[str, FineTuneTask] = {}
|
||||
self.listeners: Dict[str, List[asyncio.Queue]] = {}
|
||||
|
||||
def create_task(self, task_id: str, name: str, description: str) -> FineTuneTask:
|
||||
"""创建新任务"""
|
||||
task = FineTuneTask(task_id, name, description)
|
||||
self.tasks[task_id] = task
|
||||
self.listeners[task_id] = []
|
||||
return task
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[FineTuneTask]:
|
||||
"""获取任务"""
|
||||
return self.tasks.get(task_id)
|
||||
|
||||
def get_all_tasks(self) -> List[FineTuneTask]:
|
||||
"""获取所有任务"""
|
||||
return list(self.tasks.values())
|
||||
|
||||
def update_task_status(self, task_id: str, status: FineTuneStatus):
|
||||
"""更新任务状态"""
|
||||
task = self.tasks.get(task_id)
|
||||
if task:
|
||||
task.update_status(status)
|
||||
self._notify_listeners(task_id)
|
||||
|
||||
def update_task_progress(self, task_id: str, progress: float, log: str = None):
|
||||
"""更新任务进度"""
|
||||
task = self.tasks.get(task_id)
|
||||
if task:
|
||||
task.update_progress(progress, log)
|
||||
self._notify_listeners(task_id)
|
||||
|
||||
def delete_task(self, task_id: str):
|
||||
"""删除任务"""
|
||||
if task_id in self.tasks:
|
||||
del self.tasks[task_id]
|
||||
if task_id in self.listeners:
|
||||
# 通知所有监听器任务已删除
|
||||
for queue in self.listeners[task_id]:
|
||||
queue.put_nowait(None) # None表示任务已删除
|
||||
del self.listeners[task_id]
|
||||
|
||||
async def add_listener(self, task_id: str) -> asyncio.Queue:
|
||||
"""添加任务监听器"""
|
||||
queue = asyncio.Queue()
|
||||
if task_id in self.listeners:
|
||||
self.listeners[task_id].append(queue)
|
||||
else:
|
||||
self.listeners[task_id] = [queue]
|
||||
return queue
|
||||
|
||||
def remove_listener(self, task_id: str, queue: asyncio.Queue):
|
||||
"""移除任务监听器"""
|
||||
if task_id in self.listeners:
|
||||
self.listeners[task_id].remove(queue)
|
||||
# 如果没有监听器了,清理
|
||||
if not self.listeners[task_id]:
|
||||
del self.listeners[task_id]
|
||||
|
||||
def _notify_listeners(self, task_id: str):
|
||||
"""通知所有监听器"""
|
||||
if task_id in self.listeners:
|
||||
task = self.tasks.get(task_id)
|
||||
if task:
|
||||
for queue in self.listeners[task_id]:
|
||||
queue.put_nowait(task.to_dict())
|
||||
|
||||
def task_exists(self, task_id: str) -> bool:
|
||||
"""检查任务是否存在"""
|
||||
return task_id in self.tasks
|
||||
|
||||
|
||||
# 全局微调任务管理器实例
|
||||
fine_tune_manager = FineTuneManager()
|
||||
|
||||
|
||||
async def simulate_fine_tune(task_id: str):
|
||||
"""模拟微调任务执行"""
|
||||
manager = fine_tune_manager
|
||||
task = manager.get_task(task_id)
|
||||
|
||||
if not task:
|
||||
return
|
||||
|
||||
# 更新状态为运行中
|
||||
manager.update_task_status(task_id, FineTuneStatus.RUNNING)
|
||||
|
||||
# 模拟训练过程
|
||||
steps = [
|
||||
("初始化模型", 10),
|
||||
("加载数据集", 20),
|
||||
("训练第1轮", 35),
|
||||
("训练第2轮", 50),
|
||||
("训练第3轮", 65),
|
||||
("训练第4轮", 80),
|
||||
("模型评估", 90),
|
||||
("保存模型", 100)
|
||||
]
|
||||
|
||||
for step_name, step_progress in steps:
|
||||
# 模拟步骤执行
|
||||
await asyncio.sleep(2)
|
||||
manager.update_task_progress(task_id, step_progress, f"{step_name}完成")
|
||||
|
||||
# 更新状态为完成
|
||||
manager.update_task_status(task_id, FineTuneStatus.COMPLETED)
|
||||
manager.update_task_progress(task_id, 100.0, "微调任务已完成")
|
||||
81
request/src/utils/__init__.py
Normal file
81
request/src/utils/__init__.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# 统一日志系统 - 推荐使用
|
||||
from .unified_logger import (
|
||||
get_logger as get_unified_logger,
|
||||
get_route_logger,
|
||||
configure_logging,
|
||||
cleanup_logs,
|
||||
get_log_stats,
|
||||
unified_logger_manager,
|
||||
UnifiedLogger,
|
||||
)
|
||||
|
||||
# 向后兼容 - 保持原有接口
|
||||
from .logger import (
|
||||
get_logger,
|
||||
log,
|
||||
log_debug,
|
||||
log_info,
|
||||
log_warning,
|
||||
log_error,
|
||||
log_critical,
|
||||
logger_manager,
|
||||
)
|
||||
|
||||
from .decorators import (
|
||||
log_function_call,
|
||||
log_performance,
|
||||
retry_on_failure,
|
||||
log_it,
|
||||
time_it,
|
||||
)
|
||||
|
||||
from .exceptions import (
|
||||
BaseAPIException,
|
||||
ValidationException,
|
||||
AuthenticationException,
|
||||
AuthorizationException,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
RateLimitException,
|
||||
ExternalServiceException,
|
||||
BusinessException,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 统一日志系统 (推荐使用)
|
||||
"get_unified_logger",
|
||||
"get_route_logger",
|
||||
"configure_logging",
|
||||
"cleanup_logs",
|
||||
"get_log_stats",
|
||||
"unified_logger_manager",
|
||||
"UnifiedLogger",
|
||||
|
||||
# 向后兼容接口
|
||||
"get_logger",
|
||||
"log",
|
||||
"log_debug",
|
||||
"log_info",
|
||||
"log_warning",
|
||||
"log_error",
|
||||
"log_critical",
|
||||
"logger_manager",
|
||||
|
||||
# Decorators
|
||||
"log_function_call",
|
||||
"log_performance",
|
||||
"retry_on_failure",
|
||||
"log_it",
|
||||
"time_it",
|
||||
|
||||
# Exceptions
|
||||
"BaseAPIException",
|
||||
"ValidationException",
|
||||
"AuthenticationException",
|
||||
"AuthorizationException",
|
||||
"NotFoundException",
|
||||
"ConflictException",
|
||||
"RateLimitException",
|
||||
"ExternalServiceException",
|
||||
"BusinessException",
|
||||
]
|
||||
435
request/src/utils/advanced_logger.py
Normal file
435
request/src/utils/advanced_logger.py
Normal file
@@ -0,0 +1,435 @@
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Any
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import threading
|
||||
import json
|
||||
from .logger import get_logger
|
||||
|
||||
|
||||
class AdvancedLoggerManager:
|
||||
"""高级日志管理器 - 支持按日期和路由分类"""
|
||||
|
||||
def __init__(self,
|
||||
logs_dir: str = "logs",
|
||||
max_log_days: int = 30,
|
||||
enable_cleanup: bool = True):
|
||||
self.logs_dir = Path(logs_dir)
|
||||
self.max_log_days = max_log_days
|
||||
self.enable_cleanup = enable_cleanup
|
||||
self._file_handlers: Dict[str, logging.FileHandler] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._cleanup_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="log-cleanup")
|
||||
|
||||
# 确保logs目录存在
|
||||
self.logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
# 获取内部logger
|
||||
self.logger = get_logger(__name__)
|
||||
|
||||
def get_date_log_dir(self, date: Optional[datetime] = None) -> Path:
|
||||
"""获取指定日期的日志目录"""
|
||||
if date is None:
|
||||
date = datetime.now()
|
||||
date_str = date.strftime("%Y-%m-%d")
|
||||
return self.logs_dir / date_str
|
||||
|
||||
def get_route_log_path(self, route_name: str, date: Optional[datetime] = None) -> Path:
|
||||
"""获取指定路由和日期的日志文件路径"""
|
||||
date_dir = self.get_date_log_dir(date)
|
||||
return date_dir / f"{route_name}.log"
|
||||
|
||||
def ensure_date_dir(self, date: Optional[datetime] = None) -> Path:
|
||||
"""确保日期目录存在"""
|
||||
date_dir = self.get_date_log_dir(date)
|
||||
date_dir.mkdir(parents=True, exist_ok=True)
|
||||
return date_dir
|
||||
|
||||
def get_file_handler(self, route_name: str, date: Optional[datetime] = None) -> logging.FileHandler:
|
||||
"""获取或创建指定路由的文件处理器"""
|
||||
date_str = date.strftime("%Y-%m-%d") if date else datetime.now().strftime("%Y-%m-%d")
|
||||
handler_key = f"{date_str}_{route_name}"
|
||||
|
||||
with self._lock:
|
||||
# 检查是否已有缓存的处理句柄
|
||||
if handler_key in self._file_handlers:
|
||||
handler = self._file_handlers[handler_key]
|
||||
# 检查文件是否仍然有效(防止日志轮转)
|
||||
if handler.baseFilename == str(self.get_route_log_path(route_name, date)):
|
||||
return handler
|
||||
else:
|
||||
# 文件路径已变化,清理旧的处理句柄
|
||||
handler.close()
|
||||
del self._file_handlers[handler_key]
|
||||
|
||||
# 创建新的日期目录
|
||||
try:
|
||||
date_dir = self.ensure_date_dir(date)
|
||||
if not date_dir.exists():
|
||||
self.logger.error(f"Failed to create date directory: {date_dir}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating date directory: {str(e)}")
|
||||
raise
|
||||
|
||||
# 创建新的文件处理器
|
||||
try:
|
||||
log_file_path = self.get_route_log_path(route_name, date)
|
||||
# 确保父目录存在
|
||||
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
handler = logging.FileHandler(str(log_file_path), encoding='utf-8', mode='a')
|
||||
|
||||
# 设置JSON格式化器
|
||||
handler.setFormatter(JsonLogFormatter())
|
||||
|
||||
# 缓存处理句柄
|
||||
self._file_handlers[handler_key] = handler
|
||||
|
||||
return handler
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating file handler for {route_name}: {str(e)}")
|
||||
raise
|
||||
|
||||
def log_request(self,
|
||||
route_name: str,
|
||||
method: str,
|
||||
path: str,
|
||||
status_code: int,
|
||||
process_time: float,
|
||||
client_ip: str,
|
||||
request_id: str,
|
||||
user_agent: str = "Unknown",
|
||||
query_params: Optional[Dict] = None,
|
||||
request_body: Optional[Dict] = None,
|
||||
response_body: Optional[Dict] = None,
|
||||
error: Optional[str] = None,
|
||||
content_type: Optional[str] = None,
|
||||
content_length: Optional[str] = None):
|
||||
"""记录请求日志 - 成功写入*_success.log,失败写入*_error.log"""
|
||||
|
||||
# 确定日志级别
|
||||
if error or status_code >= 400:
|
||||
log_level = "ERROR"
|
||||
else:
|
||||
log_level = "INFO"
|
||||
|
||||
# 构建请求信息
|
||||
request_info = {
|
||||
"method": method,
|
||||
"path": path,
|
||||
"query_params": query_params or {},
|
||||
"client_ip": client_ip,
|
||||
"request_id": request_id,
|
||||
"user_agent": user_agent
|
||||
}
|
||||
|
||||
# 添加请求头信息
|
||||
if content_type:
|
||||
request_info["content_type"] = content_type
|
||||
if content_length:
|
||||
request_info["content_length"] = content_length
|
||||
if request_body:
|
||||
request_info["body"] = request_body
|
||||
|
||||
# 构建响应信息
|
||||
response_info = {
|
||||
"status_code": status_code,
|
||||
"process_time_ms": round(process_time * 1000, 2)
|
||||
}
|
||||
|
||||
# 如果有响应体,添加到响应信息中
|
||||
if response_body:
|
||||
response_info["body"] = response_body
|
||||
|
||||
# 如果有错误,添加到响应信息中
|
||||
if error:
|
||||
response_info["error"] = error
|
||||
|
||||
# 构建日志数据
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": log_level,
|
||||
"route": route_name,
|
||||
"request": request_info,
|
||||
"response": response_info
|
||||
}
|
||||
|
||||
# 根据状态码决定写入成功还是失败日志文件
|
||||
if status_code == 200 and not error:
|
||||
# 只有200状态码才算是成功
|
||||
success_log_name = f"{route_name}_success"
|
||||
self._write_to_route_log(success_log_name, log_data)
|
||||
else:
|
||||
# 其他所有状态码(包括3xx重定向)都算作失败
|
||||
error_log_name = f"{route_name}_error"
|
||||
self._write_to_route_log(error_log_name, log_data)
|
||||
|
||||
def _write_to_route_log(self, route_name: str, log_data: Dict[str, Any]):
|
||||
"""写入日志到指定路由的文件"""
|
||||
try:
|
||||
# 获取日志文件路径
|
||||
log_file_path = self.get_route_log_path(route_name)
|
||||
|
||||
# 确保目录存在
|
||||
log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 格式化为易读的JSON
|
||||
formatted_log = json.dumps(log_data, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
# 构建完整的日志条目(包含分割线)
|
||||
log_entry = f"{'='*80}\n{formatted_log}\n"
|
||||
|
||||
# 直接写入文件(更可靠的方法)
|
||||
with open(str(log_file_path), 'a', encoding='utf-8') as f:
|
||||
f.write(log_entry)
|
||||
f.flush()
|
||||
|
||||
except Exception as e:
|
||||
# 如果写入失败,使用标准logger记录错误,并打印详细错误信息
|
||||
import traceback
|
||||
error_details = traceback.format_exc()
|
||||
try:
|
||||
self.logger.error(
|
||||
f"Failed to write log to {route_name}",
|
||||
error=str(e),
|
||||
error_type=type(e).__name__,
|
||||
traceback=error_details,
|
||||
log_data=log_data
|
||||
)
|
||||
except:
|
||||
# 如果连标准logger都失败了,至少打印到控制台
|
||||
print(f"CRITICAL: Failed to write log to {route_name}: {str(e)}")
|
||||
print(error_details)
|
||||
|
||||
def get_route_logger(self, route_name: str):
|
||||
"""获取指定路由的logger实例"""
|
||||
return RouteLogger(self, route_name)
|
||||
|
||||
def cleanup_old_logs(self, days: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""清理过期的日志文件"""
|
||||
if not self.enable_cleanup:
|
||||
return {"status": "disabled", "message": "Log cleanup is disabled"}
|
||||
|
||||
cleanup_days = days or self.max_log_days
|
||||
cutoff_date = datetime.now() - timedelta(days=cleanup_days)
|
||||
|
||||
cleanup_stats = {
|
||||
"deleted_dirs": 0,
|
||||
"deleted_files": 0,
|
||||
"freed_bytes": 0,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
try:
|
||||
# 遍历logs目录下的所有日期文件夹
|
||||
for date_dir in self.logs_dir.iterdir():
|
||||
if not date_dir.is_dir():
|
||||
continue
|
||||
|
||||
# 尝试解析日期
|
||||
try:
|
||||
dir_date = datetime.strptime(date_dir.name, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
# 不是日期格式的目录,跳过
|
||||
continue
|
||||
|
||||
# 如果目录日期早于截止日期,删除整个目录
|
||||
if dir_date < cutoff_date:
|
||||
try:
|
||||
dir_size = self._get_dir_size(date_dir)
|
||||
shutil.rmtree(date_dir)
|
||||
cleanup_stats["deleted_dirs"] += 1
|
||||
cleanup_stats["freed_bytes"] += dir_size
|
||||
self.logger.info(f"Deleted old log directory: {date_dir}")
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to delete directory {date_dir}: {str(e)}"
|
||||
cleanup_stats["errors"].append(error_msg)
|
||||
self.logger.error(error_msg)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Cleanup failed: {str(e)}"
|
||||
cleanup_stats["errors"].append(error_msg)
|
||||
self.logger.error(error_msg)
|
||||
|
||||
# 清理内存中的旧处理句柄
|
||||
self._cleanup_old_handlers(cutoff_date)
|
||||
|
||||
cleanup_stats["freed_mb"] = round(cleanup_stats["freed_bytes"] / (1024 * 1024), 2)
|
||||
|
||||
return cleanup_stats
|
||||
|
||||
def _get_dir_size(self, directory: Path) -> int:
|
||||
"""获取目录大小"""
|
||||
total_size = 0
|
||||
try:
|
||||
for file_path in directory.rglob("*"):
|
||||
if file_path.is_file():
|
||||
total_size += file_path.stat().st_size
|
||||
except Exception:
|
||||
pass
|
||||
return total_size
|
||||
|
||||
def _cleanup_old_handlers(self, cutoff_date: datetime):
|
||||
"""清理内存中的旧处理句柄"""
|
||||
with self._lock:
|
||||
keys_to_remove = []
|
||||
for key, handler in self._file_handlers.items():
|
||||
try:
|
||||
date_str = key.split("_")[0]
|
||||
handler_date = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
if handler_date < cutoff_date:
|
||||
handler.close()
|
||||
keys_to_remove.append(key)
|
||||
except Exception:
|
||||
# 如果解析失败,也清理掉
|
||||
keys_to_remove.append(key)
|
||||
|
||||
for key in keys_to_remove:
|
||||
del self._file_handlers[key]
|
||||
|
||||
def schedule_cleanup(self):
|
||||
"""计划清理任务(每天执行一次)"""
|
||||
if not self.enable_cleanup:
|
||||
return
|
||||
|
||||
def cleanup_task():
|
||||
try:
|
||||
result = self.cleanup_old_logs()
|
||||
if result.get("deleted_dirs", 0) > 0:
|
||||
self.logger.info(f"Log cleanup completed: {result}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Scheduled cleanup failed: {str(e)}")
|
||||
|
||||
# 提交清理任务到线程池
|
||||
self._cleanup_executor.submit(cleanup_task)
|
||||
|
||||
def get_log_stats(self) -> Dict[str, Any]:
|
||||
"""获取日志统计信息"""
|
||||
stats = {
|
||||
"total_directories": 0,
|
||||
"total_files": 0,
|
||||
"total_size_bytes": 0,
|
||||
"active_handlers": len(self._file_handlers),
|
||||
"routes": set()
|
||||
}
|
||||
|
||||
try:
|
||||
for date_dir in self.logs_dir.iterdir():
|
||||
if not date_dir.is_dir():
|
||||
continue
|
||||
|
||||
try:
|
||||
datetime.strptime(date_dir.name, "%Y-%m-%d")
|
||||
stats["total_directories"] += 1
|
||||
|
||||
for log_file in date_dir.glob("*.log"):
|
||||
stats["total_files"] += 1
|
||||
stats["total_size_bytes"] += log_file.stat().st_size
|
||||
stats["routes"].add(log_file.stem)
|
||||
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to get log stats: {str(e)}")
|
||||
|
||||
stats["total_size_mb"] = round(stats["total_size_bytes"] / (1024 * 1024), 2)
|
||||
stats["routes"] = list(sorted(stats["routes"]))
|
||||
|
||||
return stats
|
||||
|
||||
def __del__(self):
|
||||
"""清理资源"""
|
||||
try:
|
||||
# 关闭所有文件处理器
|
||||
for handler in self._file_handlers.values():
|
||||
handler.close()
|
||||
|
||||
# 关闭线程池
|
||||
if hasattr(self, '_cleanup_executor'):
|
||||
self._cleanup_executor.shutdown(wait=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class JsonLogFormatter(logging.Formatter):
|
||||
"""JSON格式的日志格式化器"""
|
||||
|
||||
def format(self, record):
|
||||
# 如果record.msg是字典,直接使用
|
||||
if isinstance(record.msg, dict):
|
||||
log_data = record.msg.copy()
|
||||
else:
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": record.levelname,
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno
|
||||
}
|
||||
|
||||
# 添加异常信息
|
||||
if record.exc_info:
|
||||
log_data["exception"] = self.formatException(record.exc_info)
|
||||
|
||||
return json.dumps(log_data, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
class RouteLogger:
|
||||
"""路由特定的日志器"""
|
||||
|
||||
def __init__(self, manager: AdvancedLoggerManager, route_name: str):
|
||||
self.manager = manager
|
||||
self.route_name = route_name
|
||||
|
||||
def log_request(self, **kwargs):
|
||||
"""记录请求日志"""
|
||||
self.manager.log_request(route_name=self.route_name, **kwargs)
|
||||
|
||||
def log_info(self, message: str, **kwargs):
|
||||
"""记录信息日志"""
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": "INFO",
|
||||
"route": self.route_name,
|
||||
"message": message,
|
||||
**kwargs
|
||||
}
|
||||
self.manager._write_to_route_log(self.route_name, log_data)
|
||||
|
||||
def log_warning(self, message: str, **kwargs):
|
||||
"""记录警告日志"""
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": "WARNING",
|
||||
"route": self.route_name,
|
||||
"message": message,
|
||||
**kwargs
|
||||
}
|
||||
self.manager._write_to_route_log(self.route_name, log_data)
|
||||
|
||||
def log_error(self, message: str, **kwargs):
|
||||
"""记录错误日志"""
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": "ERROR",
|
||||
"route": self.route_name,
|
||||
"message": message,
|
||||
**kwargs
|
||||
}
|
||||
self.manager._write_to_route_log(self.route_name, log_data)
|
||||
# 同时写入全局错误日志
|
||||
self.manager._write_to_route_log("errors", log_data)
|
||||
|
||||
|
||||
# 全局高级日志管理器实例
|
||||
advanced_logger_manager = AdvancedLoggerManager()
|
||||
|
||||
|
||||
def get_route_logger(route_name: str) -> RouteLogger:
|
||||
"""获取指定路由的日志器"""
|
||||
return advanced_logger_manager.get_route_logger(route_name)
|
||||
321
request/src/utils/decorators.py
Normal file
321
request/src/utils/decorators.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import functools
|
||||
import time
|
||||
import asyncio
|
||||
import inspect
|
||||
from typing import Any, Callable, Dict, Optional, Union
|
||||
from .logger import get_logger
|
||||
|
||||
|
||||
def log_function_call(
|
||||
log_level: str = "info",
|
||||
log_args: bool = True,
|
||||
log_result: bool = True,
|
||||
log_execution_time: bool = True,
|
||||
include_request_id: bool = True,
|
||||
):
|
||||
"""函数调用日志装饰器
|
||||
|
||||
Args:
|
||||
log_level: 日志级别
|
||||
log_args: 是否记录函数参数
|
||||
log_result: 是否记录返回值
|
||||
log_execution_time: 是否记录执行时间
|
||||
include_request_id: 是否包含request_id
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
func_name = f"{func.__module__}.{func.__qualname__}"
|
||||
start_time = time.time()
|
||||
|
||||
# 构建日志上下文
|
||||
log_context = {"function": func_name}
|
||||
|
||||
# 添加参数到日志
|
||||
if log_args:
|
||||
# 过滤敏感参数
|
||||
safe_kwargs = _filter_sensitive_data(kwargs)
|
||||
log_context.update({
|
||||
"args_count": len(args),
|
||||
"kwargs": safe_kwargs
|
||||
})
|
||||
|
||||
# 记录函数开始执行
|
||||
getattr(logger, log_level)(
|
||||
f"开始执行函数: {func_name}",
|
||||
**log_context
|
||||
)
|
||||
|
||||
try:
|
||||
# 执行函数
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# 记录执行时间
|
||||
if log_execution_time:
|
||||
execution_time = time.time() - start_time
|
||||
log_context["execution_time"] = f"{execution_time:.4f}s"
|
||||
|
||||
# 记录返回值
|
||||
if log_result:
|
||||
log_context["result"] = _truncate_result(result)
|
||||
|
||||
# 记录函数执行成功
|
||||
getattr(logger, log_level)(
|
||||
f"函数执行成功: {func_name}",
|
||||
**log_context
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# 记录执行时间
|
||||
if log_execution_time:
|
||||
execution_time = time.time() - start_time
|
||||
log_context["execution_time"] = f"{execution_time:.4f}s"
|
||||
|
||||
# 记录异常
|
||||
logger.error(
|
||||
f"函数执行失败: {func_name}",
|
||||
error=str(e),
|
||||
error_type=type(e).__name__,
|
||||
**log_context
|
||||
)
|
||||
raise
|
||||
|
||||
return async_wrapper
|
||||
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
func_name = f"{func.__module__}.{func.__qualname__}"
|
||||
start_time = time.time()
|
||||
|
||||
# 构建日志上下文
|
||||
log_context = {"function": func_name}
|
||||
|
||||
# 添加参数到日志
|
||||
if log_args:
|
||||
safe_kwargs = _filter_sensitive_data(kwargs)
|
||||
log_context.update({
|
||||
"args_count": len(args),
|
||||
"kwargs": safe_kwargs
|
||||
})
|
||||
|
||||
# 记录函数开始执行
|
||||
getattr(logger, log_level)(
|
||||
f"开始执行函数: {func_name}",
|
||||
**log_context
|
||||
)
|
||||
|
||||
try:
|
||||
# 执行函数
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# 记录执行时间
|
||||
if log_execution_time:
|
||||
execution_time = time.time() - start_time
|
||||
log_context["execution_time"] = f"{execution_time:.4f}s"
|
||||
|
||||
# 记录返回值
|
||||
if log_result:
|
||||
log_context["result"] = _truncate_result(result)
|
||||
|
||||
# 记录函数执行成功
|
||||
getattr(logger, log_level)(
|
||||
f"函数执行成功: {func_name}",
|
||||
**log_context
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# 记录执行时间
|
||||
if log_execution_time:
|
||||
execution_time = time.time() - start_time
|
||||
log_context["execution_time"] = f"{execution_time:.4f}s"
|
||||
|
||||
# 记录异常
|
||||
logger.error(
|
||||
f"函数执行失败: {func_name}",
|
||||
error=str(e),
|
||||
error_type=type(e).__name__,
|
||||
**log_context
|
||||
)
|
||||
raise
|
||||
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def log_performance(threshold_ms: float = 1000):
|
||||
"""性能监控装饰器,当执行时间超过阈值时记录警告
|
||||
|
||||
Args:
|
||||
threshold_ms: 时间阈值(毫秒)
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
execution_time = (time.time() - start_time) * 1000 # 转换为毫秒
|
||||
if execution_time > threshold_ms:
|
||||
logger.warning(
|
||||
f"函数执行时间超过阈值: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
execution_time_ms=f"{execution_time:.2f}",
|
||||
threshold_ms=threshold_ms
|
||||
)
|
||||
|
||||
return result
|
||||
return async_wrapper
|
||||
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
execution_time = (time.time() - start_time) * 1000 # 转换为毫秒
|
||||
if execution_time > threshold_ms:
|
||||
logger.warning(
|
||||
f"函数执行时间超过阈值: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
execution_time_ms=f"{execution_time:.2f}",
|
||||
threshold_ms=threshold_ms
|
||||
)
|
||||
|
||||
return result
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def retry_on_failure(
|
||||
max_retries: int = 3,
|
||||
delay: float = 1.0,
|
||||
backoff_factor: float = 2.0,
|
||||
exceptions: tuple = (Exception,),
|
||||
):
|
||||
"""重试装饰器
|
||||
|
||||
Args:
|
||||
max_retries: 最大重试次数
|
||||
delay: 初始延迟时间
|
||||
backoff_factor: 延迟递增因子
|
||||
exceptions: 需要重试的异常类型
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
logger = get_logger(func.__module__)
|
||||
|
||||
if inspect.iscoroutinefunction(func):
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
last_exception = None
|
||||
current_delay = delay
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except exceptions as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
logger.warning(
|
||||
f"函数执行失败,{current_delay:.1f}秒后重试: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
attempt=attempt + 1,
|
||||
max_retries=max_retries + 1,
|
||||
error=str(e),
|
||||
retry_delay=current_delay
|
||||
)
|
||||
await asyncio.sleep(current_delay)
|
||||
current_delay *= backoff_factor
|
||||
else:
|
||||
logger.error(
|
||||
f"函数重试失败,已达到最大重试次数: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
max_retries=max_retries + 1,
|
||||
final_error=str(e)
|
||||
)
|
||||
|
||||
raise last_exception
|
||||
|
||||
return async_wrapper
|
||||
|
||||
else:
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
import time as sync_time
|
||||
last_exception = None
|
||||
current_delay = delay
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exceptions as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
logger.warning(
|
||||
f"函数执行失败,{current_delay:.1f}秒后重试: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
attempt=attempt + 1,
|
||||
max_retries=max_retries + 1,
|
||||
error=str(e),
|
||||
retry_delay=current_delay
|
||||
)
|
||||
sync_time.sleep(current_delay)
|
||||
current_delay *= backoff_factor
|
||||
else:
|
||||
logger.error(
|
||||
f"函数重试失败,已达到最大重试次数: {func.__qualname__}",
|
||||
function=func.__qualname__,
|
||||
max_retries=max_retries + 1,
|
||||
final_error=str(e)
|
||||
)
|
||||
|
||||
raise last_exception
|
||||
|
||||
return sync_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _filter_sensitive_data(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""过滤敏感数据"""
|
||||
sensitive_keys = {'password', 'token', 'secret', 'key', 'auth'}
|
||||
filtered_data = {}
|
||||
|
||||
for key, value in data.items():
|
||||
if any(sensitive in key.lower() for sensitive in sensitive_keys):
|
||||
filtered_data[key] = "***"
|
||||
else:
|
||||
filtered_data[key] = value
|
||||
|
||||
return filtered_data
|
||||
|
||||
|
||||
def _truncate_result(result: Any, max_length: int = 200) -> str:
|
||||
"""截断结果字符串"""
|
||||
result_str = str(result)
|
||||
if len(result_str) > max_length:
|
||||
return result_str[:max_length] + "..."
|
||||
return result_str
|
||||
|
||||
|
||||
# 便捷装饰器
|
||||
def log_it(func: Callable) -> Callable:
|
||||
"""简单的函数日志装饰器,使用默认配置"""
|
||||
return log_function_call()(func)
|
||||
|
||||
|
||||
def time_it(func: Callable) -> Callable:
|
||||
"""简单的性能监控装饰器"""
|
||||
return log_performance()(func)
|
||||
345
request/src/utils/doc_generator.py
Normal file
345
request/src/utils/doc_generator.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
API文档生成器 - 离线生成Swagger文档
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from ..utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 文档缓存目录
|
||||
STATIC_DIR = Path("static")
|
||||
CACHE_FILE = STATIC_DIR / ".openapi_cache"
|
||||
DOC_FILE = STATIC_DIR / "doc.html"
|
||||
|
||||
|
||||
def get_openapi_hash(openapi_spec: dict) -> str:
|
||||
"""计算OpenAPI规范的哈希值"""
|
||||
spec_str = json.dumps(openapi_spec, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(spec_str.encode()).hexdigest()
|
||||
|
||||
|
||||
def get_cached_hash() -> Optional[str]:
|
||||
"""获取缓存的哈希值"""
|
||||
try:
|
||||
if CACHE_FILE.exists():
|
||||
return CACHE_FILE.read_text().strip()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def save_cache_hash(hash_value: str):
|
||||
"""保存哈希值到缓存"""
|
||||
try:
|
||||
CACHE_FILE.write_text(hash_value)
|
||||
except Exception as e:
|
||||
logger.warning(f"无法保存文档缓存: {e}")
|
||||
|
||||
|
||||
def generate_doc_html(openapi_spec: dict) -> str:
|
||||
"""生成文档HTML内容"""
|
||||
# 将OpenAPI规范内嵌到HTML中
|
||||
spec_json = json.dumps(openapi_spec, ensure_ascii=False, indent=2)
|
||||
api_version = openapi_spec.get("info", {}).get("version", "1.0.0")
|
||||
api_title = openapi_spec.get("info", {}).get("title", "API文档")
|
||||
|
||||
html_content = f'''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{api_title} - 接口文档</title>
|
||||
<!-- 本地Swagger UI资源 -->
|
||||
<link rel="stylesheet" href="/vendor/swagger-ui.css">
|
||||
<script src="/vendor/swagger-ui-bundle.js"></script>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f8fafc;
|
||||
}}
|
||||
|
||||
.nav {{
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}}
|
||||
|
||||
.nav-brand {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}}
|
||||
|
||||
.nav-brand i {{
|
||||
color: #3b82f6;
|
||||
margin-right: 8px;
|
||||
}}
|
||||
|
||||
.nav-links {{
|
||||
display: flex;
|
||||
margin-left: 24px;
|
||||
gap: 8px;
|
||||
}}
|
||||
|
||||
.nav-link {{
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
transition: all 0.2s;
|
||||
}}
|
||||
|
||||
.nav-link:hover {{
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}}
|
||||
|
||||
.nav-link.active {{
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
border-bottom: 2px solid #3b82f6;
|
||||
}}
|
||||
|
||||
.nav-right {{
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}}
|
||||
|
||||
.api-version {{
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
}}
|
||||
|
||||
.api-version span {{
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}}
|
||||
|
||||
.main {{
|
||||
padding: 16px 24px;
|
||||
}}
|
||||
|
||||
.doc-container {{
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
min-height: calc(100vh - 120px);
|
||||
}}
|
||||
|
||||
/* Swagger UI 样式覆盖 */
|
||||
.swagger-ui .topbar {{
|
||||
display: none;
|
||||
}}
|
||||
|
||||
.swagger-ui .info {{
|
||||
margin: 20px 0;
|
||||
}}
|
||||
|
||||
.swagger-ui .scheme-container {{
|
||||
background: #f8fafc;
|
||||
padding: 15px;
|
||||
box-shadow: none;
|
||||
}}
|
||||
|
||||
/* Font Awesome 图标 (内联) */
|
||||
.fa {{
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
}}
|
||||
.fa-file-text-o:before {{ content: "📄"; }}
|
||||
.fa-file-text:before {{ content: "📝"; }}
|
||||
.fa-book:before {{ content: "📖"; }}
|
||||
.fa-home:before {{ content: "🏠"; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav">
|
||||
<div class="nav-brand">
|
||||
<i class="fa fa-file-text-o"></i>
|
||||
X-Request 管理系统
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a href="/log.html" class="nav-link">
|
||||
<i class="fa fa-file-text"></i> 日志管理
|
||||
</a>
|
||||
<a href="/doc.html" class="nav-link active">
|
||||
<i class="fa fa-book"></i> 接口文档
|
||||
</a>
|
||||
<a href="/" class="nav-link">
|
||||
<i class="fa fa-home"></i> 首页
|
||||
</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<div class="api-version">
|
||||
API版本: <span>{api_version}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="doc-container">
|
||||
<div id="swagger-ui"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// 内嵌的OpenAPI规范
|
||||
const spec = {spec_json};
|
||||
|
||||
window.onload = function() {{
|
||||
SwaggerUIBundle({{
|
||||
spec: spec,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
defaultModelsExpandDepth: 1,
|
||||
defaultModelExpandDepth: 1,
|
||||
docExpansion: "list",
|
||||
filter: true,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true
|
||||
}});
|
||||
}};
|
||||
</script>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
return html_content
|
||||
|
||||
|
||||
def generate_loading_html() -> str:
|
||||
"""生成加载中的HTML页面"""
|
||||
return '''<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>生成文档中...</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
}
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
h2 {
|
||||
color: #1f2937;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
<meta http-equiv="refresh" content="2">
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<h2>正在生成API文档</h2>
|
||||
<p>请稍候,页面将自动刷新...</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
|
||||
def should_regenerate(openapi_spec: dict) -> bool:
|
||||
"""检查是否需要重新生成文档"""
|
||||
# 检查文档文件是否存在
|
||||
if not DOC_FILE.exists():
|
||||
return True
|
||||
|
||||
# 检查本地资源是否存在
|
||||
vendor_dir = STATIC_DIR / "vendor"
|
||||
if not (vendor_dir / "swagger-ui.css").exists() or not (vendor_dir / "swagger-ui-bundle.js").exists():
|
||||
return True
|
||||
|
||||
# 比较哈希值
|
||||
current_hash = get_openapi_hash(openapi_spec)
|
||||
cached_hash = get_cached_hash()
|
||||
|
||||
return current_hash != cached_hash
|
||||
|
||||
|
||||
def generate_docs(app) -> bool:
|
||||
"""
|
||||
生成API文档
|
||||
|
||||
Args:
|
||||
app: FastAPI应用实例
|
||||
|
||||
Returns:
|
||||
bool: 是否重新生成了文档
|
||||
"""
|
||||
try:
|
||||
# 获取OpenAPI规范
|
||||
openapi_spec = app.openapi()
|
||||
|
||||
# 检查是否需要重新生成
|
||||
if not should_regenerate(openapi_spec):
|
||||
logger.info("API文档未变化,使用缓存")
|
||||
return False
|
||||
|
||||
logger.info("检测到API变化,正在生成文档...")
|
||||
|
||||
# 先写入加载页面
|
||||
DOC_FILE.write_text(generate_loading_html(), encoding='utf-8')
|
||||
|
||||
# 生成文档HTML
|
||||
doc_html = generate_doc_html(openapi_spec)
|
||||
|
||||
# 写入文档文件
|
||||
DOC_FILE.write_text(doc_html, encoding='utf-8')
|
||||
|
||||
# 保存哈希值
|
||||
current_hash = get_openapi_hash(openapi_spec)
|
||||
save_cache_hash(current_hash)
|
||||
|
||||
logger.info("API文档生成完成")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成API文档失败: {e}")
|
||||
return False
|
||||
130
request/src/utils/exceptions.py
Normal file
130
request/src/utils/exceptions.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from fastapi import HTTPException
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class BaseAPIException(Exception):
|
||||
"""API异常基类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
error_code: Optional[str] = None,
|
||||
status_code: int = 500,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
self.message = message
|
||||
self.error_code = error_code
|
||||
self.status_code = status_code
|
||||
self.details = details or {}
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class ValidationException(BaseAPIException):
|
||||
"""数据验证异常"""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="VALIDATION_ERROR",
|
||||
status_code=400,
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
class AuthenticationException(BaseAPIException):
|
||||
"""认证异常"""
|
||||
|
||||
def __init__(self, message: str = "认证失败"):
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="AUTHENTICATION_ERROR",
|
||||
status_code=401
|
||||
)
|
||||
|
||||
|
||||
class AuthorizationException(BaseAPIException):
|
||||
"""授权异常"""
|
||||
|
||||
def __init__(self, message: str = "权限不足"):
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="AUTHORIZATION_ERROR",
|
||||
status_code=403
|
||||
)
|
||||
|
||||
|
||||
class NotFoundException(BaseAPIException):
|
||||
"""资源未找到异常"""
|
||||
|
||||
def __init__(self, message: str = "资源未找到", resource: Optional[str] = None):
|
||||
details = {"resource": resource} if resource else None
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="NOT_FOUND",
|
||||
status_code=404,
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
class ConflictException(BaseAPIException):
|
||||
"""资源冲突异常"""
|
||||
|
||||
def __init__(self, message: str = "资源冲突", details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="CONFLICT",
|
||||
status_code=409,
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
class RateLimitException(BaseAPIException):
|
||||
"""限流异常"""
|
||||
|
||||
def __init__(self, message: str = "请求频率过高", retry_after: Optional[int] = None):
|
||||
details = {"retry_after": retry_after} if retry_after else None
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="RATE_LIMIT_EXCEEDED",
|
||||
status_code=429,
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
class ExternalServiceException(BaseAPIException):
|
||||
"""外部服务异常"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "外部服务调用失败",
|
||||
service_name: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
if service_name:
|
||||
details = details or {}
|
||||
details["service_name"] = service_name
|
||||
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code="EXTERNAL_SERVICE_ERROR",
|
||||
status_code=502,
|
||||
details=details
|
||||
)
|
||||
|
||||
|
||||
class BusinessException(BaseAPIException):
|
||||
"""业务逻辑异常"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
business_code: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
error_code = f"BUSINESS_ERROR_{business_code}" if business_code else "BUSINESS_ERROR"
|
||||
super().__init__(
|
||||
message=message,
|
||||
error_code=error_code,
|
||||
status_code=422,
|
||||
details=details
|
||||
)
|
||||
104
request/src/utils/log_example.py
Normal file
104
request/src/utils/log_example.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
高级日志系统使用示例
|
||||
|
||||
演示如何在API中使用新的日志系统
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from ..utils.advanced_logger import get_route_logger, advanced_logger_manager
|
||||
|
||||
|
||||
def example_hello_logging():
|
||||
"""Hello API日志使用示例"""
|
||||
route_logger = get_route_logger("hello")
|
||||
|
||||
# 记录一般信息
|
||||
route_logger.log_info("Hello API 被调用", user_id="12345")
|
||||
|
||||
# 记录请求
|
||||
route_logger.log_request(
|
||||
method="GET",
|
||||
path="/api/hello",
|
||||
status_code=200,
|
||||
process_time=0.05,
|
||||
client_ip="127.0.0.1",
|
||||
request_id="req-123",
|
||||
user_agent="Mozilla/5.0...",
|
||||
query_params={"name": "张三"}
|
||||
)
|
||||
|
||||
# 记录警告
|
||||
route_logger.log_warning("用户频繁调用API", user_id="12345", call_count=10)
|
||||
|
||||
# 记录错误
|
||||
route_logger.log_error("处理请求失败", error="数据库连接超时", user_id="12345")
|
||||
|
||||
|
||||
def example_user_logging():
|
||||
"""User API日志使用示例"""
|
||||
route_logger = get_route_logger("user")
|
||||
|
||||
# 记录用户创建
|
||||
route_logger.log_info("用户注册成功", user_id="12345", email="user@example.com")
|
||||
|
||||
# 记录登录请求
|
||||
route_logger.log_request(
|
||||
method="POST",
|
||||
path="/api/user/login",
|
||||
status_code=200,
|
||||
process_time=0.12,
|
||||
client_ip="192.168.1.100",
|
||||
request_id="req-456",
|
||||
query_params={},
|
||||
request_body={"username": "admin", "password": "***"} # 敏感信息已过滤
|
||||
)
|
||||
|
||||
|
||||
def example_get_log_stats():
|
||||
"""获取日志统计信息示例"""
|
||||
stats = advanced_logger_manager.get_log_stats()
|
||||
|
||||
print("日志统计信息:")
|
||||
print(f"- 总目录数: {stats['total_directories']}")
|
||||
print(f"- 总文件数: {stats['total_files']}")
|
||||
print(f"- 总大小: {stats['total_size_mb']} MB")
|
||||
print(f"- 活跃处理器数: {stats['active_handlers']}")
|
||||
print(f"- 路由列表: {', '.join(stats['routes'])}")
|
||||
|
||||
|
||||
def example_cleanup_logs():
|
||||
"""日志清理示例"""
|
||||
result = advanced_logger_manager.cleanup_old_logs(days=7)
|
||||
|
||||
print("日志清理结果:")
|
||||
print(f"- 删除目录数: {result['deleted_dirs']}")
|
||||
print(f"- 删除文件数: {result['deleted_files']}")
|
||||
print(f"- 释放空间: {result['freed_mb']} MB")
|
||||
|
||||
if result['errors']:
|
||||
print("错误信息:")
|
||||
for error in result['errors']:
|
||||
print(f" - {error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行示例
|
||||
print("=== 高级日志系统使用示例 ===\n")
|
||||
|
||||
print("1. 模拟Hello API调用...")
|
||||
example_hello_logging()
|
||||
|
||||
print("\n2. 模拟User API调用...")
|
||||
example_user_logging()
|
||||
|
||||
print("\n3. 获取日志统计信息...")
|
||||
example_get_log_stats()
|
||||
|
||||
print("\n4. 示例完成!")
|
||||
print("你可以通过以下API端点查看日志管理功能:")
|
||||
print("- GET /logs/stats - 获取日志统计")
|
||||
print("- GET /logs/directories - 获取日志目录列表")
|
||||
print("- GET /logs/search - 搜索日志内容")
|
||||
print("- GET /logs/cleanup - 清理过期日志")
|
||||
print("- GET /logs/files/{date}/{route_name} - 查看指定日志文件")
|
||||
print("- GET /logs/download/{date}/{route_name} - 下载指定日志文件")
|
||||
228
request/src/utils/logger.py
Normal file
228
request/src/utils/logger.py
Normal file
@@ -0,0 +1,228 @@
|
||||
import structlog
|
||||
import logging
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
from pathlib import Path
|
||||
import colorama
|
||||
from colorama import Fore, Back, Style
|
||||
|
||||
# 尝试导入pythonjsonlogger,如果没有安装则使用备用方案
|
||||
try:
|
||||
from pythonjsonlogger import jsonlogger
|
||||
HAS_JSON_LOGGER = True
|
||||
except ImportError:
|
||||
HAS_JSON_LOGGER = False
|
||||
jsonlogger = None
|
||||
|
||||
# 初始化colorama
|
||||
colorama.init()
|
||||
|
||||
|
||||
class ColoredConsoleRenderer:
|
||||
"""带颜色的控制台日志渲染器"""
|
||||
|
||||
def __call__(self, logger, method_name: str, event_dict: Dict[str, Any]) -> str:
|
||||
"""渲染日志事件"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
level = event_dict.get("level", "INFO").upper()
|
||||
message = event_dict.get("event", "")
|
||||
|
||||
# 根据日志级别选择颜色
|
||||
level_colors = {
|
||||
"DEBUG": Fore.CYAN,
|
||||
"INFO": Fore.GREEN,
|
||||
"WARNING": Fore.YELLOW,
|
||||
"ERROR": Fore.RED,
|
||||
"CRITICAL": Fore.RED + Back.WHITE + Style.BRIGHT,
|
||||
}
|
||||
|
||||
color = level_colors.get(level, "")
|
||||
reset = Style.RESET_ALL
|
||||
|
||||
# 基础信息
|
||||
log_line = f"{color}[{timestamp}] {level}{reset} {message}"
|
||||
|
||||
# 添加额外的上下文信息
|
||||
if "request_id" in event_dict:
|
||||
log_line += f" {Fore.BLUE}[req:{event_dict['request_id']}]{reset}"
|
||||
|
||||
if "function" in event_dict:
|
||||
log_line += f" {Fore.MAGIC}{event_dict['function']}(){reset}"
|
||||
|
||||
# 添加其他字段
|
||||
for key, value in event_dict.items():
|
||||
if key not in ["level", "event", "timestamp", "request_id", "function"]:
|
||||
log_line += f" {Fore.CYAN}{key}={value}{reset}"
|
||||
|
||||
return log_line
|
||||
|
||||
|
||||
class JSONRenderer:
|
||||
"""JSON格式的日志渲染器"""
|
||||
|
||||
def __call__(self, logger, method_name: str, event_dict: Dict[str, Any]) -> str:
|
||||
"""渲染日志事件为JSON格式"""
|
||||
log_data = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"level": event_dict.get("level", "INFO"),
|
||||
"message": event_dict.get("event", ""),
|
||||
}
|
||||
|
||||
# 添加其他字段
|
||||
for key, value in event_dict.items():
|
||||
if key not in ["level", "event"]:
|
||||
log_data[key] = value
|
||||
|
||||
return json.dumps(log_data, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
class LoggerManager:
|
||||
"""日志管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self._processors = []
|
||||
self._configured = False
|
||||
|
||||
def configure(self,
|
||||
log_level: str = "INFO",
|
||||
log_format: str = "console",
|
||||
log_file: Optional[str] = None,
|
||||
log_to_console: bool = True):
|
||||
"""配置日志系统"""
|
||||
|
||||
# 配置structlog处理器
|
||||
processors = [
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
]
|
||||
|
||||
# 添加自定义处理器
|
||||
processors.extend(self._processors)
|
||||
|
||||
# 选择渲染器
|
||||
if log_format == "json":
|
||||
renderer = JSONRenderer()
|
||||
else:
|
||||
renderer = ColoredConsoleRenderer()
|
||||
|
||||
processors.append(renderer)
|
||||
|
||||
# 配置structlog
|
||||
structlog.configure(
|
||||
processors=processors,
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
# 配置标准库logging
|
||||
# 防止重复配置
|
||||
root_logger = logging.getLogger()
|
||||
if not root_logger.handlers:
|
||||
level = getattr(logging, log_level.upper())
|
||||
|
||||
# 如果指定了日志文件,配置文件日志
|
||||
if log_file:
|
||||
self._setup_file_handler(log_file, log_level)
|
||||
|
||||
# 如果允许控制台输出,配置控制台日志
|
||||
if log_to_console:
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(level)
|
||||
console_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
root_logger.addHandler(console_handler)
|
||||
else:
|
||||
# 如果不输出到控制台,至少设置日志级别
|
||||
root_logger.setLevel(level)
|
||||
|
||||
self._configured = True
|
||||
|
||||
def _setup_file_handler(self, log_file: str, log_level: str = "INFO"):
|
||||
"""设置文件日志处理器"""
|
||||
log_path = Path(log_file)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
level = getattr(logging, log_level.upper())
|
||||
file_handler.setLevel(level)
|
||||
|
||||
if HAS_JSON_LOGGER:
|
||||
file_handler.setFormatter(jsonlogger.JsonFormatter(
|
||||
'%(asctime)s %(name)s %(levelname)s %(message)s'
|
||||
))
|
||||
else:
|
||||
# 使用标准库的JSON格式化器
|
||||
file_handler.setFormatter(logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
))
|
||||
|
||||
# 获取根logger并添加文件处理器
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(file_handler)
|
||||
root_logger.setLevel(level)
|
||||
|
||||
def add_processor(self, processor):
|
||||
"""添加自定义处理器"""
|
||||
self._processors.append(processor)
|
||||
|
||||
def get_logger(self, name: Optional[str] = None) -> structlog.stdlib.BoundLogger:
|
||||
"""获取logger实例"""
|
||||
if not self._configured:
|
||||
self.configure()
|
||||
return structlog.get_logger(name)
|
||||
|
||||
|
||||
# 全局日志管理器实例
|
||||
logger_manager = LoggerManager()
|
||||
|
||||
# 获取logger的便捷函数
|
||||
def get_logger(name: Optional[str] = None) -> structlog.stdlib.BoundLogger:
|
||||
"""获取logger实例"""
|
||||
return logger_manager.get_logger(name)
|
||||
|
||||
|
||||
# 简单的日志打印函数
|
||||
def log(message: str, level: str = "info", **kwargs):
|
||||
"""简单的日志打印函数
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别 (debug, info, warning, error, critical)
|
||||
**kwargs: 额外的上下文信息
|
||||
"""
|
||||
logger = get_logger()
|
||||
log_method = getattr(logger, level.lower(), logger.info)
|
||||
log_method(message, **kwargs)
|
||||
|
||||
|
||||
# 带上下文的日志函数
|
||||
def log_debug(message: str, **kwargs):
|
||||
"""打印debug级别日志"""
|
||||
log(message, "debug", **kwargs)
|
||||
|
||||
|
||||
def log_info(message: str, **kwargs):
|
||||
"""打印info级别日志"""
|
||||
log(message, "info", **kwargs)
|
||||
|
||||
|
||||
def log_warning(message: str, **kwargs):
|
||||
"""打印warning级别日志"""
|
||||
log(message, "warning", **kwargs)
|
||||
|
||||
|
||||
def log_error(message: str, **kwargs):
|
||||
"""打印error级别日志"""
|
||||
log(message, "error", **kwargs)
|
||||
|
||||
|
||||
def log_critical(message: str, **kwargs):
|
||||
"""打印critical级别日志"""
|
||||
log(message, "critical", **kwargs)
|
||||
368
request/src/utils/unified_logger.py
Normal file
368
request/src/utils/unified_logger.py
Normal file
@@ -0,0 +1,368 @@
|
||||
"""
|
||||
统一日志管理器 - 整合基础日志和高级日志系统
|
||||
提供简单易用的统一接口,同时保持向后兼容
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional, Union
|
||||
from pathlib import Path
|
||||
from contextlib import contextmanager
|
||||
|
||||
try:
|
||||
import structlog
|
||||
from pythonjsonlogger import jsonlogger
|
||||
STRUCTLOG_AVAILABLE = True
|
||||
except ImportError:
|
||||
STRUCTLOG_AVAILABLE = False
|
||||
|
||||
from .logger import LoggerManager, ColoredConsoleRenderer, JSONRenderer
|
||||
|
||||
|
||||
class UnifiedLoggerManager:
|
||||
"""统一日志管理器 - 整合所有日志功能"""
|
||||
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
"""单例模式"""
|
||||
if cls._instance is None:
|
||||
with cls._lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, '_initialized'):
|
||||
self._initialized = True
|
||||
self._config = {}
|
||||
self._loggers = {}
|
||||
self._route_loggers = {}
|
||||
self._file_handlers = {}
|
||||
self._setup_default_config()
|
||||
|
||||
def _setup_default_config(self):
|
||||
"""设置默认配置"""
|
||||
self._config = {
|
||||
# 基础配置
|
||||
'level': 'INFO',
|
||||
'format': 'json', # 'json' 或 'console'
|
||||
'console_output': True,
|
||||
|
||||
# 高级配置
|
||||
'advanced_mode': True,
|
||||
'route_based_logging': True,
|
||||
'logs_dir': 'logs',
|
||||
'max_log_days': 30,
|
||||
'enable_cleanup': True,
|
||||
|
||||
# 文件配置
|
||||
'file_rotation': False,
|
||||
'max_file_size': '10MB',
|
||||
'backup_count': 5,
|
||||
|
||||
# 性能配置
|
||||
'async_write': True,
|
||||
'buffer_size': 100,
|
||||
'flush_interval': 5,
|
||||
}
|
||||
|
||||
def configure(self, **kwargs):
|
||||
"""配置日志系统"""
|
||||
self._config.update(kwargs)
|
||||
|
||||
# 重新配置基础日志管理器
|
||||
if hasattr(self, '_logger_manager'):
|
||||
self._logger_manager.configure(
|
||||
level=self._config['level'],
|
||||
format=self._config['format'],
|
||||
to_file=bool(self._config.get('log_file')),
|
||||
to_console=self._config['console_output']
|
||||
)
|
||||
|
||||
def get_logger(self, name: str = None, route_name: str = None) -> 'UnifiedLogger':
|
||||
"""获取统一的日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
route_name: 路由名称(用于路由专用日志)
|
||||
|
||||
Returns:
|
||||
UnifiedLogger: 统一日志记录器实例
|
||||
"""
|
||||
key = f"{name}:{route_name}" if route_name else name
|
||||
|
||||
if key not in self._loggers:
|
||||
logger = UnifiedLogger(
|
||||
name=name or 'default',
|
||||
route_name=route_name,
|
||||
config=self._config
|
||||
)
|
||||
self._loggers[key] = logger
|
||||
|
||||
return self._loggers[key]
|
||||
|
||||
def get_route_logger(self, route_name: str) -> 'UnifiedLogger':
|
||||
"""获取路由专用日志记录器"""
|
||||
return self.get_logger(route_name=route_name)
|
||||
|
||||
def cleanup_logs(self, days: int = None) -> Dict[str, Any]:
|
||||
"""清理过期日志文件"""
|
||||
days = days or self._config['max_log_days']
|
||||
if not self._config['enable_cleanup']:
|
||||
return {'status': 'disabled', 'message': 'Log cleanup is disabled'}
|
||||
|
||||
logs_dir = Path(self._config['logs_dir'])
|
||||
if not logs_dir.exists():
|
||||
return {'status': 'success', 'deleted_count': 0, 'deleted_files': []}
|
||||
|
||||
deleted_files = []
|
||||
cutoff_time = time.time() - (days * 24 * 3600)
|
||||
|
||||
for log_file in logs_dir.rglob('*.log'):
|
||||
try:
|
||||
if log_file.stat().st_mtime < cutoff_time:
|
||||
log_file.unlink()
|
||||
deleted_files.append(str(log_file))
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to delete log file {log_file}: {e}")
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'deleted_count': len(deleted_files),
|
||||
'deleted_files': deleted_files
|
||||
}
|
||||
|
||||
def get_log_stats(self) -> Dict[str, Any]:
|
||||
"""获取日志统计信息"""
|
||||
logs_dir = Path(self._config['logs_dir'])
|
||||
stats = {
|
||||
'total_files': 0,
|
||||
'total_size': 0,
|
||||
'by_date': {},
|
||||
'by_route': {}
|
||||
}
|
||||
|
||||
if not logs_dir.exists():
|
||||
return stats
|
||||
|
||||
for log_file in logs_dir.rglob('*.log'):
|
||||
try:
|
||||
file_stat = log_file.stat()
|
||||
file_size = file_stat.st_size
|
||||
|
||||
stats['total_files'] += 1
|
||||
stats['total_size'] += file_size
|
||||
|
||||
# 按日期统计
|
||||
date_dir = log_file.parent.name
|
||||
if date_dir not in stats['by_date']:
|
||||
stats['by_date'][date_dir] = {'files': 0, 'size': 0}
|
||||
stats['by_date'][date_dir]['files'] += 1
|
||||
stats['by_date'][date_dir]['size'] += file_size
|
||||
|
||||
# 按路由统计
|
||||
route_name = log_file.stem.split('_')[0] # 去掉 _success 或 _error
|
||||
if route_name not in stats['by_route']:
|
||||
stats['by_route'][route_name] = {'files': 0, 'size': 0}
|
||||
stats['by_route'][route_name]['files'] += 1
|
||||
stats['by_route'][route_name]['size'] += file_size
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to stat log file {log_file}: {e}")
|
||||
|
||||
return stats
|
||||
|
||||
def list_log_files(self, date: str = None) -> Dict[str, Any]:
|
||||
"""列出日志文件"""
|
||||
logs_dir = Path(self._config['logs_dir'])
|
||||
files = []
|
||||
|
||||
search_dir = logs_dir / date if date else logs_dir
|
||||
|
||||
if not search_dir.exists():
|
||||
return {'date': date, 'files': []}
|
||||
|
||||
for log_file in search_dir.glob('*.log'):
|
||||
try:
|
||||
file_stat = log_file.stat()
|
||||
files.append({
|
||||
'name': log_file.name,
|
||||
'path': str(log_file.relative_to(logs_dir)),
|
||||
'size': file_stat.st_size,
|
||||
'modified': datetime.fromtimestamp(file_stat.st_mtime).isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to read log file info {log_file}: {e}")
|
||||
|
||||
files.sort(key=lambda x: x['name'])
|
||||
|
||||
return {'date': date or 'all', 'files': files}
|
||||
|
||||
|
||||
class UnifiedLogger:
|
||||
"""统一日志记录器 - 提供一致的日志接口"""
|
||||
|
||||
def __init__(self, name: str, route_name: str = None, config: Dict[str, Any] = None):
|
||||
self.name = name
|
||||
self.route_name = route_name
|
||||
self.config = config or {}
|
||||
|
||||
# 根据配置选择底层日志系统
|
||||
if self.config.get('advanced_mode', True):
|
||||
self._init_advanced_logger()
|
||||
else:
|
||||
self._init_basic_logger()
|
||||
|
||||
def _init_advanced_logger(self):
|
||||
"""初始化高级日志系统"""
|
||||
if self.config.get('route_based_logging', True) and self.route_name:
|
||||
from .advanced_logger import advanced_logger_manager
|
||||
self._logger = advanced_logger_manager.get_route_logger(self.route_name)
|
||||
else:
|
||||
# 创建高级日志记录器
|
||||
if STRUCTLOG_AVAILABLE:
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
ColoredConsoleRenderer() if self.config.get('console_output') else JSONRenderer()
|
||||
],
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
self._logger = structlog.get_logger(self.name)
|
||||
else:
|
||||
self._init_basic_logger()
|
||||
|
||||
def _init_basic_logger(self):
|
||||
"""初始化基础日志系统"""
|
||||
self._logger_manager = LoggerManager()
|
||||
self._logger = self._logger_manager.get_logger(self.name)
|
||||
|
||||
def debug(self, message: str, **kwargs):
|
||||
"""记录调试日志"""
|
||||
self._log('debug', message, **kwargs)
|
||||
|
||||
def info(self, message: str, **kwargs):
|
||||
"""记录信息日志"""
|
||||
self._log('info', message, **kwargs)
|
||||
|
||||
def warning(self, message: str, **kwargs):
|
||||
"""记录警告日志"""
|
||||
self._log('warning', message, **kwargs)
|
||||
|
||||
def error(self, message: str, **kwargs):
|
||||
"""记录错误日志"""
|
||||
self._log('error', message, **kwargs)
|
||||
|
||||
def critical(self, message: str, **kwargs):
|
||||
"""记录严重错误日志"""
|
||||
self._log('critical', message, **kwargs)
|
||||
|
||||
def _log(self, level: str, message: str, **kwargs):
|
||||
"""内部日志记录方法"""
|
||||
if hasattr(self._logger, level):
|
||||
if kwargs:
|
||||
getattr(self._logger, level)(message, **kwargs)
|
||||
else:
|
||||
getattr(self._logger, level)(message)
|
||||
else:
|
||||
# 降级处理
|
||||
print(f"[{level.upper()}] {message}", file=sys.stderr if level in ['error', 'critical'] else sys.stdout)
|
||||
|
||||
def log_request(self, method: str, path: str, request_id: str = None, **kwargs):
|
||||
"""记录请求日志"""
|
||||
message = f"{method} {path}"
|
||||
extra = {
|
||||
'event_type': 'request',
|
||||
'method': method,
|
||||
'path': path,
|
||||
'request_id': request_id,
|
||||
**kwargs
|
||||
}
|
||||
self.info(message, **extra)
|
||||
|
||||
def log_response(self, status_code: int, process_time: float = None, request_id: str = None, **kwargs):
|
||||
"""记录响应日志"""
|
||||
level = 'info' if status_code < 400 else 'warning' if status_code < 500 else 'error'
|
||||
message = f"Response {status_code}"
|
||||
|
||||
extra = {
|
||||
'event_type': 'response',
|
||||
'status_code': status_code,
|
||||
'process_time_ms': round(process_time * 1000, 2) if process_time else None,
|
||||
'request_id': request_id,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
self._log(level, message, **extra)
|
||||
|
||||
def log_exception(self, exception: Exception, **kwargs):
|
||||
"""记录异常日志"""
|
||||
message = f"Exception: {str(exception)}"
|
||||
extra = {
|
||||
'event_type': 'exception',
|
||||
'exception_type': type(exception).__name__,
|
||||
'exception_message': str(exception),
|
||||
**kwargs
|
||||
}
|
||||
self.error(message, **extra)
|
||||
|
||||
@contextmanager
|
||||
def log_context(self, **context):
|
||||
"""日志上下文管理器"""
|
||||
if hasattr(self._logger, 'bind'):
|
||||
bound_logger = self._logger.bind(**context)
|
||||
original_logger = self._logger
|
||||
self._logger = bound_logger
|
||||
try:
|
||||
yield self
|
||||
finally:
|
||||
self._logger = original_logger
|
||||
else:
|
||||
# 基础日志系统的降级处理
|
||||
yield self
|
||||
|
||||
|
||||
# 全局统一日志管理器实例
|
||||
unified_logger_manager = UnifiedLoggerManager()
|
||||
|
||||
# 便捷函数
|
||||
def get_logger(name: str = None, route_name: str = None) -> UnifiedLogger:
|
||||
"""获取统一日志记录器的便捷函数"""
|
||||
return unified_logger_manager.get_logger(name=name, route_name=route_name)
|
||||
|
||||
def get_route_logger(route_name: str) -> UnifiedLogger:
|
||||
"""获取路由日志记录器的便捷函数"""
|
||||
return unified_logger_manager.get_route_logger(route_name)
|
||||
|
||||
def configure_logging(**kwargs):
|
||||
"""配置日志系统的便捷函数"""
|
||||
unified_logger_manager.configure(**kwargs)
|
||||
|
||||
def cleanup_logs(days: int = None) -> Dict[str, Any]:
|
||||
"""清理日志的便捷函数"""
|
||||
return unified_logger_manager.cleanup_logs(days=days)
|
||||
|
||||
def get_log_stats() -> Dict[str, Any]:
|
||||
"""获取日志统计的便捷函数"""
|
||||
return unified_logger_manager.get_log_stats()
|
||||
|
||||
|
||||
# 向后兼容的别名
|
||||
logger_manager = unified_logger_manager
|
||||
log_manager = unified_logger_manager
|
||||
10
request/start-backend.bat
Normal file
10
request/start-backend.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
echo Starting Backend API Server on Port 3000...
|
||||
echo.
|
||||
echo Backend API: http://localhost:3000/models/
|
||||
echo Press Ctrl+C to stop the server
|
||||
echo.
|
||||
cd /d "%~dp0"
|
||||
set PYTHONPATH=src
|
||||
/d/Softwares/Anaconda/python -m uvicorn src.core.app:app --host 0.0.0.0 --port 3000
|
||||
pause
|
||||
99
request/start.sh
Normal file
99
request/start.sh
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "🚀 启动 X-Request 高性能 FastAPI 框架"
|
||||
|
||||
# 永远从脚本所在目录运行(避免在别的目录执行导致找不到 venv / requirements / .env)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
mkdir -p logs
|
||||
PID_FILE="logs/xrequest.pid"
|
||||
LOG_FILE="logs/xrequest.server.log"
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d "xrequest" ]; then
|
||||
echo "❌ 虚拟环境不存在,请先运行:"
|
||||
echo " ./setup.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 选择虚拟环境里的 Python(不要依赖 source activate,也不要回退系统 Python)
|
||||
VENV_PY=""
|
||||
if [ -f "xrequest/Scripts/python.exe" ]; then
|
||||
VENV_PY="xrequest/Scripts/python.exe"
|
||||
elif [ -f "xrequest/bin/python" ]; then
|
||||
VENV_PY="xrequest/bin/python"
|
||||
fi
|
||||
|
||||
if [ -z "$VENV_PY" ]; then
|
||||
echo "❌ 未找到虚拟环境 Python,可尝试重新创建虚拟环境:"
|
||||
echo " rm -rf xrequest && ./setup.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 使用虚拟环境 Python: $VENV_PY"
|
||||
|
||||
# 检查 .env 文件是否存在
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "❌ 环境配置文件 .env 不存在,请先运行:"
|
||||
echo " ./setup.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 读取 .env 文件中的配置
|
||||
PORT="8000" # 默认端口
|
||||
HOST="0.0.0.0" # 默认主机
|
||||
|
||||
# 解析 .env 文件(简单解析 PORT 和 HOST)
|
||||
if [ -f ".env" ]; then
|
||||
# 使用 grep 和 sed 来提取 PORT 和 HOST(忽略注释)
|
||||
PORT=$(grep -v '^#' .env | grep '^PORT=' | head -1 | cut -d= -f2 | tr -d '"' || echo "8000")
|
||||
HOST=$(grep -v '^#' .env | grep '^HOST=' | head -1 | cut -d= -f2 | tr -d '"' || echo "0.0.0.0")
|
||||
fi
|
||||
|
||||
echo "📋 使用配置: HOST=$HOST, PORT=$PORT"
|
||||
|
||||
# 快速校验关键依赖是否可导入(避免跑到一半才 ModuleNotFoundError)
|
||||
if ! "$VENV_PY" -c "import uvicorn" >/dev/null 2>&1; then
|
||||
echo "❌ 虚拟环境缺少 uvicorn(或未正确安装依赖)。请运行:"
|
||||
echo " ./setup.sh"
|
||||
echo " 或手动安装:$VENV_PY -m pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
OLD_PID="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" >/dev/null 2>&1; then
|
||||
echo "✅ X-Request 已在后台运行 (pid=$OLD_PID)"
|
||||
echo "📄 日志: $LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$PID_FILE" || true
|
||||
fi
|
||||
|
||||
echo "🎯 启动应用服务..."
|
||||
echo "📚 API文档地址: http://localhost:$PORT/docs"
|
||||
echo "🏥 健康检查: http://localhost:$PORT/health"
|
||||
echo "📊 应用信息: http://localhost:$PORT/info"
|
||||
echo "⏹️ 停止服务: ./stop.sh"
|
||||
echo "📄 日志: $LOG_FILE"
|
||||
echo ""
|
||||
|
||||
nohup "$VENV_PY" "main.py" >>"$LOG_FILE" 2>&1 &
|
||||
PID_NUM=$!
|
||||
echo $PID_NUM > "$PID_FILE"
|
||||
sleep 0.5 # 等待PID文件写入
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
SAVED_PID=$(cat "$PID_FILE")
|
||||
echo "✅ 已在后台启动 (pid=$SAVED_PID)"
|
||||
else
|
||||
echo "⚠️ 启动成功但PID文件创建失败,使用进程号: $PID_NUM"
|
||||
echo $PID_NUM > "$PID_FILE"
|
||||
echo "✅ 已在后台启动 (pid=$PID_NUM)"
|
||||
fi
|
||||
echo " 查看日志: tail -f $LOG_FILE"
|
||||
echo " 停止服务: ./stop.sh"
|
||||
|
||||
1
request/static/.openapi_cache
Normal file
1
request/static/.openapi_cache
Normal file
@@ -0,0 +1 @@
|
||||
58852b1a007ac2ceb1f790a423c070f2
|
||||
1473
request/static/doc.html
Normal file
1473
request/static/doc.html
Normal file
File diff suppressed because it is too large
Load Diff
4
request/static/font-awesome.min.css
vendored
Normal file
4
request/static/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
request/static/fontawesome-webfont.ttf
Normal file
BIN
request/static/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
request/static/fontawesome-webfont.woff
Normal file
BIN
request/static/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
request/static/fontawesome-webfont.woff2
Normal file
BIN
request/static/fontawesome-webfont.woff2
Normal file
Binary file not shown.
2717
request/static/index.html
Normal file
2717
request/static/index.html
Normal file
File diff suppressed because it is too large
Load Diff
697
request/static/log.html
Normal file
697
request/static/log.html
Normal file
@@ -0,0 +1,697 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>X-Request 日志管理</title>
|
||||
<!-- 引入Tailwind CSS (离线版本) -->
|
||||
<script src="vendor/tailwind.min.js"></script>
|
||||
<!-- 引入内联 SVG 图标系统 (完全离线) -->
|
||||
<script src="vendor/icons.js"></script>
|
||||
<style>
|
||||
.inline-icon, .inline-emoji {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.inline-icon svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
.inline-emoji {
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.inline-icon[data-spin="true"], .inline-emoji[data-spin="true"] {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
<!-- 配置Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#8b5cf6',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
danger: '#ef4444',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏样式 */
|
||||
html, body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.max-w-7xl {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 调整网格布局高度 */
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 调整滚动区域最大高度 */
|
||||
.max-h-screen-content {
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="bg-white shadow-md h-16">
|
||||
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<span class="text-3xl font-bold text-primary">X</span>
|
||||
<span class="ml-2 text-xl font-semibold text-gray-800">X-Request 管理系统</span>
|
||||
</div>
|
||||
<div class="ml-6 flex items-center space-x-4">
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-home mr-1"></i> 首页
|
||||
</a>
|
||||
<a href="/log.html" class="px-3 py-2 rounded-md text-sm font-medium text-primary bg-primary/10 border-b-2 border-primary">
|
||||
<i class="fa fa-file-text mr-1"></i> 日志管理
|
||||
</a>
|
||||
<a href="/doc.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-book mr-1"></i> 接口文档
|
||||
</a>
|
||||
<a href="/status.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-heartbeat mr-1"></i> 系统状态
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="mr-4">
|
||||
<span class="text-sm text-gray-500">更新时间: </span>
|
||||
<span id="last-updated" class="font-medium text-gray-800">--:--:--</span>
|
||||
</div>
|
||||
<button type="button" id="refresh-btn" class="bg-primary hover:bg-primary/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors mr-2">
|
||||
<i class="fa fa-refresh mr-1"></i> 刷新
|
||||
</button>
|
||||
<button type="button" id="batch-download-btn" onclick="batchDownload()" class="bg-success hover:bg-success/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed mr-2" disabled>
|
||||
<i class="fa fa-download mr-1"></i> 批量下载
|
||||
<span id="selected-count-download" class="ml-1 bg-white/20 px-2 py-0.5 rounded-full text-xs">0</span>
|
||||
</button>
|
||||
<button type="button" id="batch-delete-btn" onclick="batchDelete()" class="bg-danger hover:bg-danger/90 text-white px-3 py-1.5 rounded-md text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
||||
<i class="fa fa-trash mr-1"></i> 批量删除
|
||||
<span id="selected-count-delete" class="ml-1 bg-white/20 px-2 py-0.5 rounded-full text-xs">0</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="w-full px-4 sm:px-6 lg:px-8 py-4">
|
||||
<!-- 日志管理视图 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-4 h-full">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">日志管理</h2>
|
||||
|
||||
<!-- 日志分类和内容 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 h-screen-content">
|
||||
<!-- 左侧:日期列表 -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4">按日期分类</h3>
|
||||
<div class="space-y-2 flex-1 flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="select-all-dates" class="rounded text-primary focus:ring-primary h-4 w-4 mr-2">
|
||||
<label for="select-all-dates" class="text-sm font-medium text-gray-700">全选日期</label>
|
||||
</div>
|
||||
<div id="dates-list" class="space-y-1 flex-1 overflow-y-auto mt-2">
|
||||
<!-- 日期列表将通过JavaScript动态生成 -->
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<i class="fa fa-spinner fa-spin text-xl mb-2"></i>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:日志文件列表 -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 id="selected-date-title" class="text-lg font-semibold text-gray-800">选择日期查看日志</h3>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="select-all-logs" class="rounded text-primary focus:ring-primary h-4 w-4 mr-2">
|
||||
<label for="select-all-logs" class="text-sm font-medium text-gray-700">全选日志</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs-list" class="space-y-2 flex-1 overflow-y-auto">
|
||||
<!-- 日志文件列表将通过JavaScript动态生成 -->
|
||||
<div class="text-center text-gray-500 py-8">
|
||||
<p>请选择左侧日期</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:日志内容 -->
|
||||
<div class="lg:col-span-7">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 h-full flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 id="current-log-title" class="text-lg font-semibold text-gray-800">日志内容</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<select id="log-mode" class="border border-gray-300 rounded-md text-sm px-2 py-1">
|
||||
<option value="latest" selected>最新的N条日志(顶部)</option>
|
||||
<option value="oldest">最早的N条日志(顶部)</option>
|
||||
</select>
|
||||
<select id="log-lines" class="border border-gray-300 rounded-md text-sm px-2 py-1">
|
||||
<option value="50">50行</option>
|
||||
<option value="100" selected>100行</option>
|
||||
<option value="200">200行</option>
|
||||
<option value="500">500行</option>
|
||||
<option value="1000">1000行</option>
|
||||
<option value="2000">2000行</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-900 text-gray-200 rounded-md p-4 flex-1 overflow-auto">
|
||||
<pre id="log-content" class="text-sm whitespace-pre-wrap">请选择日志文件查看内容...</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 自定义确认对话框 -->
|
||||
<div id="confirm-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-2" id="confirm-title">确认操作</h3>
|
||||
<p class="text-gray-600 mb-6" id="confirm-message">确定要执行此操作吗?</p>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" id="confirm-cancel" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button type="button" id="confirm-ok" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-danger/90 transition-colors">
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
// 全局变量
|
||||
let selectedDate = null;
|
||||
let currentLogFile = null;
|
||||
let selectedFiles = new Set(); // 存储选中的日志文件路径
|
||||
let dates = []; // 存储所有日期
|
||||
|
||||
// 自定义确认对话框
|
||||
let confirmResolver = null;
|
||||
function showConfirm(title, message) {
|
||||
return new Promise((resolve) => {
|
||||
confirmResolver = resolve;
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
const titleEl = document.getElementById('confirm-title');
|
||||
const messageEl = document.getElementById('confirm-message');
|
||||
|
||||
titleEl.textContent = title;
|
||||
messageEl.textContent = message;
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
});
|
||||
}
|
||||
|
||||
// 确认对话框事件监听
|
||||
document.getElementById('confirm-cancel').addEventListener('click', () => {
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (confirmResolver) {
|
||||
confirmResolver(false);
|
||||
confirmResolver = null;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('confirm-ok').addEventListener('click', () => {
|
||||
const modal = document.getElementById('confirm-modal');
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
if (confirmResolver) {
|
||||
confirmResolver(true);
|
||||
confirmResolver = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 获取日期列表
|
||||
async function fetchDates() {
|
||||
try {
|
||||
const response = await fetch('/monitoring/logs');
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
dates = data.data.dates || [];
|
||||
renderDatesList();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日期列表失败:', error);
|
||||
document.getElementById('dates-list').innerHTML = '<div class="text-center text-red-500 py-8"><i class="fa fa-exclamation-circle text-xl mb-2"></i><p>获取日期列表失败</p></div>';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 渲染日期列表
|
||||
function renderDatesList() {
|
||||
const datesList = document.getElementById('dates-list');
|
||||
|
||||
if (dates.length === 0) {
|
||||
datesList.innerHTML = '<div class="text-center text-gray-500 py-8"><p>没有找到日志日期</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const date of dates) {
|
||||
const isSelected = selectedDate === date.date;
|
||||
html += `
|
||||
<div class="flex items-center p-2 rounded-md border border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors ${isSelected ? 'bg-primary/10 border-l-4 border-primary' : ''}">
|
||||
<input type="checkbox" data-date="${date.date}" class="date-checkbox rounded text-primary focus:ring-primary h-4 w-4 mr-2">
|
||||
<div class="flex-1" onclick="selectDate('${date.date}')">
|
||||
<div class="font-medium text-gray-800">${date.date}</div>
|
||||
<div class="text-xs text-gray-500">${date.log_count} 个日志文件</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">${new Date(date.last_modified * 1000).toLocaleString()}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
datesList.innerHTML = html;
|
||||
|
||||
// 添加日期复选框事件监听
|
||||
document.querySelectorAll('.date-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', handleDateCheckboxChange);
|
||||
});
|
||||
}
|
||||
|
||||
// 选择日期
|
||||
async function selectDate(date) {
|
||||
selectedDate = date;
|
||||
currentLogFile = null;
|
||||
selectedFiles.clear();
|
||||
updateSelectedCount();
|
||||
|
||||
// 更新日期列表样式
|
||||
renderDatesList();
|
||||
|
||||
// 更新日志列表
|
||||
await fetchLogsByDate(date);
|
||||
|
||||
// 清空日志内容
|
||||
document.getElementById('current-log-title').textContent = '日志内容';
|
||||
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
|
||||
}
|
||||
|
||||
// 获取指定日期的日志文件
|
||||
async function fetchLogsByDate(date) {
|
||||
try {
|
||||
const response = await fetch(`/monitoring/logs?date=${date}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
const logs = data.data.logs || [];
|
||||
renderLogsList(logs, date);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日志文件列表失败:', error);
|
||||
document.getElementById('logs-list').innerHTML = '<div class="text-center text-red-500 py-8"><i class="fa fa-exclamation-circle text-xl mb-2"></i><p>获取日志文件列表失败</p></div>';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 渲染日志文件列表
|
||||
function renderLogsList(logs, date) {
|
||||
const logsList = document.getElementById('logs-list');
|
||||
const selectedDateTitle = document.getElementById('selected-date-title');
|
||||
|
||||
selectedDateTitle.textContent = `${date} 日志文件 (${logs.length} 个)`;
|
||||
|
||||
if (logs.length === 0) {
|
||||
logsList.innerHTML = '<div class="text-center text-gray-500 py-8"><p>该日期没有日志文件</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const log of logs) {
|
||||
const isSelected = selectedFiles.has(log.relative_path);
|
||||
// 将Windows风格的路径分隔符替换为Unix风格
|
||||
const displayPath = log.relative_path.replace(/\\/g, '/');
|
||||
html += `
|
||||
<div class="flex items-center p-2 rounded-md border border-gray-200 hover:bg-gray-100 transition-colors">
|
||||
<input type="checkbox" data-path="${log.relative_path}" class="log-checkbox rounded text-primary focus:ring-primary h-4 w-4 mr-2" ${isSelected ? 'checked' : ''}>
|
||||
<div class="flex-1" onclick="viewLog('${displayPath}', '${log.name}')">
|
||||
<div class="font-medium text-gray-800">${log.name}</div>
|
||||
<div class="text-xs text-gray-500">${formatBytes(log.size)} · ${new Date(log.modified_at * 1000).toLocaleString()}</div>
|
||||
</div>
|
||||
<a href="/monitoring/logs/${log.relative_path}/download" target="_blank" class="text-primary hover:text-primary/80 mr-2" title="下载">
|
||||
<i class="fa fa-download"></i>
|
||||
</a>
|
||||
<button type="button" onclick="deleteLog('${displayPath}', '${log.name}')" class="text-danger hover:text-danger/80" title="删除">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
logsList.innerHTML = html;
|
||||
|
||||
// 添加日志复选框事件监听
|
||||
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', handleLogCheckboxChange);
|
||||
});
|
||||
}
|
||||
|
||||
// 查看日志内容
|
||||
async function viewLog(filePath, fileName) {
|
||||
currentLogFile = filePath;
|
||||
|
||||
// 更新标题
|
||||
document.getElementById('current-log-title').textContent = `日志内容 - ${fileName}`;
|
||||
|
||||
try {
|
||||
const entries = document.getElementById('log-lines').value;
|
||||
const mode = document.getElementById('log-mode').value;
|
||||
// 将Windows风格的路径分隔符替换为Unix风格
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const parts = normalizedPath.split('/');
|
||||
const date = parts[0];
|
||||
const logName = parts.slice(1).join('/');
|
||||
|
||||
const response = await fetch(`/monitoring/logs/${date}/${logName}?entries=${entries}&mode=${mode}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
document.getElementById('log-content').textContent = data.data.content;
|
||||
// 根据显示模式滚动
|
||||
const logContent = document.getElementById('log-content');
|
||||
logContent.scrollTop = 0;
|
||||
} else {
|
||||
document.getElementById('log-content').textContent = `获取日志内容失败: ${data.message}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取日志内容失败:', error);
|
||||
document.getElementById('log-content').textContent = `获取日志内容失败: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理日期复选框变化
|
||||
async function handleDateCheckboxChange(e) {
|
||||
const date = e.target.dataset.date;
|
||||
const isChecked = e.target.checked;
|
||||
|
||||
if (isChecked) {
|
||||
// 选中该日期下的所有日志文件
|
||||
await fetchLogsByDate(date);
|
||||
|
||||
// 勾选该日期下的所有日志文件
|
||||
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
|
||||
const path = checkbox.dataset.path;
|
||||
if (path && path.startsWith(date)) {
|
||||
checkbox.checked = true;
|
||||
selectedFiles.add(path);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 取消选中该日期下的所有日志文件
|
||||
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
|
||||
const path = checkbox.dataset.path;
|
||||
if (path && path.startsWith(date)) {
|
||||
checkbox.checked = false;
|
||||
selectedFiles.delete(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
// 处理日志复选框变化
|
||||
function handleLogCheckboxChange(e) {
|
||||
const filePath = e.target.dataset.path;
|
||||
const isChecked = e.target.checked;
|
||||
|
||||
if (isChecked) {
|
||||
selectedFiles.add(filePath);
|
||||
} else {
|
||||
selectedFiles.delete(filePath);
|
||||
}
|
||||
|
||||
updateSelectedCount();
|
||||
}
|
||||
|
||||
// 更新选中计数
|
||||
function updateSelectedCount() {
|
||||
const count = selectedFiles.size;
|
||||
// 更新批量下载的计数
|
||||
document.getElementById('selected-count-download').textContent = count;
|
||||
// 更新批量删除的计数
|
||||
document.getElementById('selected-count-delete').textContent = count;
|
||||
// 更新按钮状态
|
||||
document.getElementById('batch-download-btn').disabled = count === 0;
|
||||
document.getElementById('batch-delete-btn').disabled = count === 0;
|
||||
}
|
||||
|
||||
// 删除单个日志文件
|
||||
async function deleteLog(filePath, fileName) {
|
||||
const confirmed = await showConfirm('删除确认', `确定要删除日志文件 "${fileName}" 吗?此操作不可恢复。`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/monitoring/logs/${filePath}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// 更新日志列表
|
||||
await fetchLogsByDate(selectedDate);
|
||||
|
||||
// 清空日志内容如果当前显示的是被删除的日志
|
||||
if (currentLogFile === filePath) {
|
||||
document.getElementById('current-log-title').textContent = '日志内容';
|
||||
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
|
||||
currentLogFile = null;
|
||||
}
|
||||
|
||||
// 更新选中文件集合
|
||||
selectedFiles.delete(filePath);
|
||||
updateSelectedCount();
|
||||
|
||||
// 显示成功提示
|
||||
alert(`日志文件 "${fileName}" 删除成功`);
|
||||
} else {
|
||||
alert(`删除失败: ${data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除日志失败:', error);
|
||||
alert(`删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除日志文件
|
||||
async function batchDelete() {
|
||||
if (selectedFiles.size === 0) {
|
||||
alert('请先选择日志文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await showConfirm('批量删除确认', `确定要删除选中的 ${selectedFiles.size} 个日志文件吗?此操作不可恢复。`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/monitoring/logs/batch/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: Array.from(selectedFiles)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// 更新日志列表
|
||||
await fetchLogsByDate(selectedDate);
|
||||
|
||||
// 清空当前日志内容
|
||||
document.getElementById('current-log-title').textContent = '日志内容';
|
||||
document.getElementById('log-content').textContent = '请选择日志文件查看内容...';
|
||||
currentLogFile = null;
|
||||
|
||||
// 清空选中文件集合
|
||||
selectedFiles.clear();
|
||||
updateSelectedCount();
|
||||
|
||||
// 显示成功提示
|
||||
alert(`批量删除完成。成功删除 ${data.data.deleted} 个文件,失败 ${data.data.failed} 个文件。`);
|
||||
} else {
|
||||
alert(`批量删除失败: ${data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error);
|
||||
alert(`批量删除失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 全选/取消全选日期
|
||||
function toggleSelectAllDates() {
|
||||
const selectAllCheckbox = document.getElementById('select-all-dates');
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
document.querySelectorAll('.date-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
// 触发change事件,调用handleDateCheckboxChange处理日志文件联动
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
}
|
||||
|
||||
// 全选/取消全选日志
|
||||
function toggleSelectAllLogs() {
|
||||
const selectAllCheckbox = document.getElementById('select-all-logs');
|
||||
const isChecked = selectAllCheckbox.checked;
|
||||
|
||||
document.querySelectorAll('.log-checkbox').forEach(checkbox => {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.dispatchEvent(new Event('change'));
|
||||
});
|
||||
}
|
||||
|
||||
// 批量下载日志
|
||||
async function batchDownload() {
|
||||
if (selectedFiles.size === 0) {
|
||||
alert('请先选择日志文件');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/monitoring/logs/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
files: Array.from(selectedFiles)
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// 创建下载链接
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `logs_${new Date().toISOString().slice(0, 10)}_batch.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
alert(`批量下载失败: ${data.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量下载失败:', error);
|
||||
alert(`批量下载失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有数据
|
||||
async function refreshAll() {
|
||||
await fetchDates();
|
||||
if (selectedDate) {
|
||||
await fetchLogsByDate(selectedDate);
|
||||
}
|
||||
|
||||
// 更新最后更新时间
|
||||
const now = new Date();
|
||||
document.getElementById('last-updated').textContent = now.toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
// 初始加载数据
|
||||
await refreshAll();
|
||||
|
||||
// 事件监听
|
||||
document.getElementById('refresh-btn').addEventListener('click', refreshAll);
|
||||
document.getElementById('select-all-dates').addEventListener('change', toggleSelectAllDates);
|
||||
document.getElementById('select-all-logs').addEventListener('change', toggleSelectAllLogs);
|
||||
|
||||
// 日志行数或显示模式变化事件
|
||||
document.getElementById('log-lines').addEventListener('change', () => {
|
||||
if (currentLogFile) {
|
||||
// 直接调用viewLog,使用当前的filePath和fileName
|
||||
viewLog(currentLogFile, document.getElementById('current-log-title').textContent.replace('日志内容 - ', ''));
|
||||
}
|
||||
});
|
||||
|
||||
// 显示模式变化事件
|
||||
document.getElementById('log-mode').addEventListener('change', () => {
|
||||
if (currentLogFile) {
|
||||
// 直接调用viewLog,使用当前的filePath和fileName
|
||||
viewLog(currentLogFile, document.getElementById('current-log-title').textContent.replace('日志内容 - ', ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
98
request/static/sse-test.html
Normal file
98
request/static/sse-test.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSE流式输出演示</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#messages {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.done-indicator {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 大模型流式输出演示</h1>
|
||||
<div id="messages">正在连接服务器...</div>
|
||||
|
||||
<script>
|
||||
const messageBox = document.getElementById('messages');
|
||||
let currentContent = '';
|
||||
|
||||
// 使用相对路径,避免跨域问题
|
||||
const source = new EventSource('/sse');
|
||||
|
||||
source.onopen = function() {
|
||||
console.log('SSE连接已建立');
|
||||
messageBox.innerHTML = '正在生成内容...\n\n';
|
||||
};
|
||||
|
||||
source.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.error) {
|
||||
messageBox.innerHTML += '<div style="color: red;">错误: ' + data.error + '</div>';
|
||||
source.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.content !== undefined && data.content !== null) {
|
||||
// 追加内容
|
||||
currentContent += data.content;
|
||||
messageBox.innerHTML = currentContent;
|
||||
|
||||
// 自动滚动到底部
|
||||
messageBox.scrollTop = messageBox.scrollHeight;
|
||||
}
|
||||
|
||||
if (data.done) {
|
||||
console.log('流式输出完成');
|
||||
messageBox.innerHTML += '\n\n<div class="done-indicator">✅ 内容生成完成!</div>';
|
||||
source.close();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析数据失败:', e);
|
||||
console.log('原始数据:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
source.onerror = function(error) {
|
||||
console.error('SSE连接出错:', error);
|
||||
messageBox.innerHTML += '<div style="color: red; margin-top: 20px;">❌ 连接失败</div>';
|
||||
source.close();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
474
request/static/status.html
Normal file
474
request/static/status.html
Normal file
@@ -0,0 +1,474 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统状态 - X-Request</title>
|
||||
<!-- 引入Tailwind CSS (离线版本) -->
|
||||
<script src="vendor/tailwind.min.js"></script>
|
||||
<!-- 引入内联 SVG 图标系统 (完全离线) -->
|
||||
<script src="vendor/icons.js"></script>
|
||||
<style>
|
||||
.inline-icon, .inline-emoji {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.inline-icon svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
}
|
||||
.inline-emoji {
|
||||
font-size: 1em;
|
||||
line-height: 1;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.inline-icon[data-spin="true"], .inline-emoji[data-spin="true"] {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
<!-- 配置Tailwind -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#3b82f6',
|
||||
secondary: '#8b5cf6',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
danger: '#ef4444',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.content-auto {
|
||||
content-visibility: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏样式 */
|
||||
html, body {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<!-- 顶部导航栏 -->
|
||||
<nav class="bg-white shadow-md h-16">
|
||||
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<span class="text-3xl font-bold text-primary">X</span>
|
||||
<span class="ml-2 text-xl font-semibold text-gray-800">X-Request 管理系统</span>
|
||||
</div>
|
||||
<div class="ml-6 flex items-center space-x-4">
|
||||
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-home mr-1"></i> 首页
|
||||
</a>
|
||||
<a href="/log.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-file-text mr-1"></i> 日志管理
|
||||
</a>
|
||||
<a href="/doc.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-primary hover:bg-primary/10 hover:border-b-2 hover:border-primary transition-colors">
|
||||
<i class="fa fa-book mr-1"></i> 接口文档
|
||||
</a>
|
||||
<a href="/status.html" class="px-3 py-2 rounded-md text-sm font-medium text-primary bg-primary/10 border-b-2 border-primary">
|
||||
<i class="fa fa-heartbeat mr-1"></i> 系统状态
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button id="refresh-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors flex items-center">
|
||||
<i class="fa fa-refresh mr-2"></i>
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
<a href="index.html" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors flex items-center">
|
||||
<i class="fa fa-home mr-2"></i>
|
||||
<span>返回首页</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<main class="w-full px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<!-- 健康检查卡片 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 status-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-heartbeat text-primary mr-3"></i>
|
||||
健康检查
|
||||
</h2>
|
||||
<div id="health-status" class="flex items-center">
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium bg-gray-200 text-gray-700">
|
||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||
检查中...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="health-content" class="space-y-3">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
<span class="ml-3 text-gray-600">正在加载健康检查信息...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 应用信息卡片 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 status-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-info-circle text-primary mr-3"></i>
|
||||
应用信息
|
||||
</h2>
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fa fa-clock-o mr-1"></i>
|
||||
<span id="last-updated">--</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="info-content" class="space-y-3">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
<span class="ml-3 text-gray-600">正在加载应用信息...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态卡片 -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 status-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-2xl font-bold text-gray-800 flex items-center">
|
||||
<i class="fa fa-server text-primary mr-3"></i>
|
||||
系统状态
|
||||
</h2>
|
||||
</div>
|
||||
<div id="system-content" class="space-y-3">
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
|
||||
<span class="ml-3 text-gray-600">正在加载系统状态...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 页脚 -->
|
||||
<footer class="bg-white border-t border-gray-200 py-4 mt-6">
|
||||
<div class="w-full mx-auto px-4 sm:px-6 lg:px-8 text-center text-sm text-gray-500">
|
||||
<p>X-Request © 2025</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// 工具函数
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return '--';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
return date.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
// 获取健康检查信息
|
||||
async function fetchHealth() {
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const data = await response.json();
|
||||
|
||||
const healthStatus = document.getElementById('health-status');
|
||||
const healthContent = document.getElementById('health-content');
|
||||
|
||||
if (data.status === 1 && data.response) {
|
||||
// 健康状态
|
||||
healthStatus.innerHTML = `
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium bg-success/20 text-success border border-success/30">
|
||||
<i class="fa fa-check-circle mr-2"></i>
|
||||
健康
|
||||
</span>
|
||||
`;
|
||||
|
||||
// 健康信息内容
|
||||
healthContent.innerHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-success/5 border border-success/20 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-check-circle text-success text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">服务状态</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold text-success">${data.response.status || 'healthy'}</p>
|
||||
</div>
|
||||
<div class="bg-primary/5 border border-primary/20 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-cog text-primary text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">服务名称</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${data.response.service || 'Unknown'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fa fa-clock-o mr-1"></i>
|
||||
检查时间: ${data.time || '--'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// 不健康状态
|
||||
healthStatus.innerHTML = `
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium bg-danger/20 text-danger border border-danger/30">
|
||||
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||
异常
|
||||
</span>
|
||||
`;
|
||||
healthContent.innerHTML = `
|
||||
<div class="bg-danger/5 border border-danger/20 rounded-lg p-4">
|
||||
<p class="text-danger font-semibold">健康检查失败</p>
|
||||
<p class="text-sm text-gray-600 mt-2">${data.response?.error || '未知错误'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const healthStatus = document.getElementById('health-status');
|
||||
const healthContent = document.getElementById('health-content');
|
||||
|
||||
healthStatus.innerHTML = `
|
||||
<span class="px-3 py-1 rounded-full text-sm font-medium bg-danger/20 text-danger border border-danger/30">
|
||||
<i class="fa fa-times-circle mr-2"></i>
|
||||
错误
|
||||
</span>
|
||||
`;
|
||||
healthContent.innerHTML = `
|
||||
<div class="bg-danger/5 border border-danger/20 rounded-lg p-4">
|
||||
<p class="text-danger font-semibold">无法获取健康检查信息</p>
|
||||
<p class="text-sm text-gray-600 mt-2">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应用信息
|
||||
async function fetchInfo() {
|
||||
try {
|
||||
const response = await fetch('/info');
|
||||
const data = await response.json();
|
||||
|
||||
const infoContent = document.getElementById('info-content');
|
||||
|
||||
if (data.status === 1 && data.response) {
|
||||
const info = data.response;
|
||||
infoContent.innerHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="bg-primary/5 border border-primary/20 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-tag text-primary text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">应用名称</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${info.app_name || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-secondary/5 border border-secondary/20 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-code-fork text-secondary text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">版本号</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${info.version || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-${info.debug ? 'warning' : 'success'}/5 border border-${info.debug ? 'warning' : 'success'}/20 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-${info.debug ? 'bug' : 'shield'} text-${info.debug ? 'warning' : 'success'} text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">调试模式</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-${info.debug ? 'warning' : 'success'}">${info.debug ? '已启用' : '已禁用'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-server text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">主机地址</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${info.host || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-plug text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">端口号</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${info.port || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-link text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">访问地址</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">http://${info.host === '0.0.0.0' ? 'localhost' : info.host}:${info.port || 'Unknown'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fa fa-clock-o mr-1"></i>
|
||||
更新时间: ${data.time || '--'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
infoContent.innerHTML = `
|
||||
<div class="bg-danger/5 border border-danger/20 rounded-lg p-4">
|
||||
<p class="text-danger font-semibold">无法获取应用信息</p>
|
||||
<p class="text-sm text-gray-600 mt-2">${data.response?.error || '未知错误'}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const infoContent = document.getElementById('info-content');
|
||||
infoContent.innerHTML = `
|
||||
<div class="bg-danger/5 border border-danger/20 rounded-lg p-4">
|
||||
<p class="text-danger font-semibold">无法获取应用信息</p>
|
||||
<p class="text-sm text-gray-600 mt-2">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统状态
|
||||
async function fetchSystemStatus() {
|
||||
try {
|
||||
const response = await fetch('/monitoring/status');
|
||||
const data = await response.json();
|
||||
|
||||
const systemContent = document.getElementById('system-content');
|
||||
|
||||
if (data.success && data.data && data.data.system) {
|
||||
const system = data.data.system;
|
||||
systemContent.innerHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-desktop text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">主机名</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${system.hostname || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-linux text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">系统类型</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${system.system_type || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-python text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">Python 版本</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${system.python_version || 'Unknown'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center mb-2">
|
||||
<i class="fa fa-clock-o text-gray-600 text-xl mr-2"></i>
|
||||
<span class="font-semibold text-gray-800">时间戳</span>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-gray-700">${formatTimestamp(system.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
systemContent.innerHTML = `
|
||||
<div class="bg-warning/5 border border-warning/20 rounded-lg p-4">
|
||||
<p class="text-warning font-semibold">系统状态信息不可用</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
const systemContent = document.getElementById('system-content');
|
||||
systemContent.innerHTML = `
|
||||
<div class="bg-warning/5 border border-warning/20 rounded-lg p-4">
|
||||
<p class="text-warning font-semibold">无法获取系统状态</p>
|
||||
<p class="text-sm text-gray-600 mt-2">${error.message}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新所有信息
|
||||
async function refreshAll() {
|
||||
const lastUpdated = document.getElementById('last-updated');
|
||||
lastUpdated.textContent = formatDateTime(new Date());
|
||||
|
||||
await Promise.all([
|
||||
fetchHealth(),
|
||||
fetchInfo(),
|
||||
fetchSystemStatus()
|
||||
]);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
// 初始加载数据
|
||||
await refreshAll();
|
||||
|
||||
// 刷新按钮事件
|
||||
document.getElementById('refresh-btn').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('refresh-btn');
|
||||
const icon = btn.querySelector('i');
|
||||
icon.classList.add('fa-spin');
|
||||
btn.disabled = true;
|
||||
|
||||
await refreshAll();
|
||||
|
||||
icon.classList.remove('fa-spin');
|
||||
btn.disabled = false;
|
||||
});
|
||||
|
||||
// 自动刷新(每30秒)
|
||||
setInterval(refreshAll, 30000);
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
request/static/vendor/font-awesome/font-awesome.min.css
vendored
Normal file
4
request/static/vendor/font-awesome/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf
vendored
Normal file
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf
vendored
Normal file
Binary file not shown.
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.woff
vendored
Normal file
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.woff
vendored
Normal file
Binary file not shown.
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2
vendored
Normal file
BIN
request/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2
vendored
Normal file
Binary file not shown.
140
request/static/vendor/icons.js
vendored
Normal file
140
request/static/vendor/icons.js
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Emoji Icons - Beautiful Emoji Icons (Offline)
|
||||
* Modern, beautiful emoji icons, completely offline
|
||||
* Compatible with existing fa-* class names
|
||||
*/
|
||||
|
||||
const Icons = {
|
||||
// 文档相关图标
|
||||
'document-text': '📄',
|
||||
'document': '📄',
|
||||
'file-text': '📝',
|
||||
'book-open': '📖',
|
||||
'book': '📖',
|
||||
|
||||
// 健康/状态相关图标
|
||||
'heart-pulse': '💓',
|
||||
'heart': '❤️',
|
||||
'check-circle': '✅',
|
||||
'x-circle': '❌',
|
||||
'exclamation-triangle': '⚠️',
|
||||
'information-circle': 'ℹ️',
|
||||
|
||||
// 导航相关图标
|
||||
'home': '🏠',
|
||||
'arrow-right': '➡️',
|
||||
'arrow-left': '⬅️',
|
||||
|
||||
// 操作相关图标
|
||||
'arrow-downTray': '⬇️',
|
||||
'download': '⬇️',
|
||||
'trash': '🗑️',
|
||||
'folder': '📁',
|
||||
'magnifying-glass': '🔍',
|
||||
'eye': '👁️',
|
||||
'check-square': '☑️',
|
||||
'square': '⬜',
|
||||
'ellipsis-horizontal': '⋯',
|
||||
|
||||
// 系统相关图标
|
||||
'server': '🖥️',
|
||||
'chart-bar': '📊',
|
||||
'calendar': '📅',
|
||||
'clock': '🕐',
|
||||
'user': '👤',
|
||||
'cpu-chip': '💾',
|
||||
'circle-stack': '🗃️',
|
||||
'globe-alt': '🌐',
|
||||
|
||||
// 设置相关图标
|
||||
'cog-6-tooth': '⚙️',
|
||||
'bell': '🔔',
|
||||
'inbox': '📥',
|
||||
|
||||
// 新增:文件操作
|
||||
'folder-plus': '📁➕',
|
||||
'document-plus': '📝➕',
|
||||
'arrow-path': '🔄',
|
||||
};
|
||||
|
||||
// 创建图标元素的辅助函数
|
||||
function createIcon(name, className = '') {
|
||||
const emoji = Icons[name];
|
||||
if (!emoji) {
|
||||
console.warn(`Icon "${name}" not found`);
|
||||
return '';
|
||||
}
|
||||
return `<span class="inline-emoji ${className}" data-icon="${name}">${emoji}</span>`;
|
||||
}
|
||||
|
||||
// 初始化:替换所有 <i class="fa fa-"> 为 Emoji 图标
|
||||
function initIcons() {
|
||||
// 映射旧图标名到新图标名
|
||||
const iconMapping = {
|
||||
'fa-file-text-o': 'document-text',
|
||||
'fa-file-text': 'document-text',
|
||||
'fa-book': 'book-open',
|
||||
'fa-book-o': 'book-open',
|
||||
'fa-heartbeat': 'heart-pulse',
|
||||
'fa-home': 'home',
|
||||
'fa-arrow-right': 'arrow-right',
|
||||
'fa-download': 'download',
|
||||
'fa-trash': 'trash',
|
||||
'fa-check-circle': 'check-circle',
|
||||
'fa-times-circle': 'x-circle',
|
||||
'fa-info-circle': 'information-circle',
|
||||
'fa-exclamation-circle': 'exclamation-triangle',
|
||||
'fa-refresh': 'arrow-path',
|
||||
'fa-server': 'server',
|
||||
'fa-clock-o': 'clock',
|
||||
'fa-eye': 'eye',
|
||||
};
|
||||
|
||||
// 查找所有 Font Awesome 图标
|
||||
document.querySelectorAll('i.fa').forEach(icon => {
|
||||
const classes = Array.from(icon.classList);
|
||||
const iconName = classes.find(cls => cls.startsWith('fa-') && cls !== 'fa');
|
||||
|
||||
if (iconName) {
|
||||
// 映射到新图标名
|
||||
const mappedName = iconMapping[iconName] || iconName.replace('fa-', '');
|
||||
const emoji = Icons[mappedName];
|
||||
|
||||
if (emoji) {
|
||||
// 保留原有的类名(除了 fa 和 fa-*)
|
||||
const otherClasses = classes.filter(cls => !cls.startsWith('fa'));
|
||||
const newElement = document.createElement('span');
|
||||
newElement.className = `inline-emoji ${otherClasses.join(' ')}`;
|
||||
newElement.textContent = emoji;
|
||||
newElement.style.display = 'inline-block';
|
||||
newElement.style.verticalAlign = 'middle';
|
||||
|
||||
// 处理旋转动画
|
||||
if (classes.includes('fa-spin')) {
|
||||
newElement.setAttribute('data-spin', 'true');
|
||||
}
|
||||
|
||||
// 复制样式
|
||||
const computedStyle = window.getComputedStyle(icon);
|
||||
if (computedStyle.fontSize) {
|
||||
newElement.style.fontSize = computedStyle.fontSize;
|
||||
}
|
||||
|
||||
icon.parentNode.replaceChild(newElement, icon);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initIcons);
|
||||
} else {
|
||||
initIcons();
|
||||
}
|
||||
|
||||
// 添加便捷方法到全局
|
||||
window.Icons = {
|
||||
create: createIcon,
|
||||
render: (name, className = '') => createIcon(name, className),
|
||||
};
|
||||
3
request/static/vendor/swagger-ui-bundle.js
vendored
Normal file
3
request/static/vendor/swagger-ui-bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
request/static/vendor/swagger-ui.css
vendored
Normal file
3
request/static/vendor/swagger-ui.css
vendored
Normal file
File diff suppressed because one or more lines are too long
65
request/static/vendor/tailwind.min.js
vendored
Normal file
65
request/static/vendor/tailwind.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
83
request/stop.sh
Normal file
83
request/stop.sh
Normal file
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "🛑 停止 X-Request (FastAPI)"
|
||||
|
||||
# 永远从脚本所在目录运行
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
PID_FILE="logs/xrequest.pid"
|
||||
|
||||
# 读取 .env 文件中的配置
|
||||
PORT="8000" # 默认端口
|
||||
|
||||
# 解析 .env 文件中的 PORT
|
||||
if [ -f ".env" ]; then
|
||||
# 使用 grep 和 sed 来提取 PORT(忽略注释)
|
||||
PORT=$(grep -v '^#' .env | grep '^PORT=' | head -1 | cut -d= -f2 | tr -d '"' || echo "8000")
|
||||
fi
|
||||
|
||||
echo "📋 使用配置: PORT=$PORT"
|
||||
|
||||
kill_pid() {
|
||||
local pid="$1"
|
||||
if [ -z "$pid" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Windows Git Bash: 优先 taskkill(对 python.exe 更稳)
|
||||
if command -v taskkill >/dev/null 2>&1; then
|
||||
taskkill //F //PID "$pid" >/dev/null 2>&1 || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Linux/macOS
|
||||
kill "$pid" >/dev/null 2>&1 || true
|
||||
sleep 0.3
|
||||
kill -9 "$pid" >/dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$PID" ] && kill -0 "$PID" >/dev/null 2>&1; then
|
||||
echo "🔪 终止进程 pid=$PID ..."
|
||||
kill_pid "$PID"
|
||||
rm -f "$PID_FILE" || true
|
||||
echo "✅ 已停止"
|
||||
exit 0
|
||||
fi
|
||||
# pid 文件存在但进程不存在:清理
|
||||
rm -f "$PID_FILE" || true
|
||||
fi
|
||||
|
||||
echo "ℹ️ 未找到有效 PID 文件,尝试按端口 $PORT 查找并停止..."
|
||||
|
||||
PIDS_BY_PORT=""
|
||||
|
||||
if command -v netstat >/dev/null 2>&1; then
|
||||
# Windows Git Bash (netstat -ano)
|
||||
PIDS_BY_PORT="$(netstat -ano 2>/dev/null | grep -E "[:.]$PORT[[:space:]]" | grep LISTENING | awk '{print $NF}' | sort -u | tr '\n' ' ' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$PIDS_BY_PORT" ] && command -v lsof >/dev/null 2>&1; then
|
||||
PIDS_BY_PORT="$(lsof -ti tcp:$PORT 2>/dev/null | sort -u | tr '\n' ' ' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$PIDS_BY_PORT" ] && command -v ss >/dev/null 2>&1; then
|
||||
PIDS_BY_PORT="$(ss -ltnp 2>/dev/null | grep ':$PORT' | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u | tr '\n' ' ' || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$PIDS_BY_PORT" ]; then
|
||||
echo "✅ 未发现 $PORT 端口监听进程(无需停止)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for p in $PIDS_BY_PORT; do
|
||||
echo "🔪 终止进程 pid=$p (by port $PORT)..."
|
||||
kill_pid "$p"
|
||||
done
|
||||
|
||||
echo "✅ 已停止(by port $PORT)"
|
||||
277
request/view_logs.sh
Normal file
277
request/view_logs.sh
Normal file
@@ -0,0 +1,277 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 直接设置项目根目录
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
LOGS_DIR="${LOGS_DIR:-$PROJECT_ROOT/logs}"
|
||||
|
||||
echo "📋 X-Request 高级日志查看工具"
|
||||
echo "============================"
|
||||
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
TODAY_DIR="$LOGS_DIR/$TODAY"
|
||||
|
||||
# 检查日志目录是否存在
|
||||
if [ ! -d "$LOGS_DIR" ]; then
|
||||
echo "❌ 日志目录不存在: $LOGS_DIR"
|
||||
echo "💡 请先启动应用生成日志"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "📁 日志目录: $LOGS_DIR"
|
||||
echo "📅 今天的日志: $TODAY_DIR"
|
||||
echo ""
|
||||
|
||||
# 显示可用日期目录
|
||||
echo "📅 可用的日志日期:"
|
||||
echo "=================="
|
||||
ls -1 "$LOGS_DIR" | grep -E "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" | sort -r
|
||||
echo ""
|
||||
|
||||
# 显示今天的日志文件统计
|
||||
if [ -d "$TODAY_DIR" ]; then
|
||||
echo "📄 今天的日志文件:"
|
||||
echo "=================="
|
||||
echo "成功日志:"
|
||||
ls -lh "$TODAY_DIR"/*_success.log 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' || echo " 无成功日志"
|
||||
echo ""
|
||||
echo "失败日志:"
|
||||
ls -lh "$TODAY_DIR"/*_error.log 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' || echo " 无失败日志"
|
||||
echo ""
|
||||
else
|
||||
echo "⚠️ 今天还没有日志文件"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
|
||||
# 主循环函数
|
||||
show_menu_and_process() {
|
||||
while true; do
|
||||
echo ""
|
||||
echo "请选择操作:"
|
||||
echo "1) 实时监控成功日志"
|
||||
echo "2) 实时监控失败日志"
|
||||
echo "3) 搜索今天的日志 (按关键词)"
|
||||
echo "4) 选择特定日期的日志"
|
||||
echo "5) 查看日志统计"
|
||||
echo "6) 清理旧日志 (7天前)"
|
||||
echo "0) 退出"
|
||||
echo ""
|
||||
read -p "请输入选项 (0-6): " choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
if [ ! -d "$TODAY_DIR" ]; then
|
||||
echo "❌ 今天的日志目录不存在"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 获取可用路由列表
|
||||
echo "📋 请选择要监控的路由:"
|
||||
echo "======================"
|
||||
success_routes=$(find "$TODAY_DIR" -name "*_success.log" -exec basename {} _success.log \; 2>/dev/null | sort | uniq)
|
||||
|
||||
if [ -z "$success_routes" ]; then
|
||||
echo "❌ 今天没有成功日志的路由"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$success_routes" | nl
|
||||
echo ""
|
||||
read -p "请输入路由编号: " route_num
|
||||
|
||||
selected_route=$(echo "$success_routes" | sed -n "${route_num}p")
|
||||
if [ -n "$selected_route" ] && [ -f "$TODAY_DIR/${selected_route}_success.log" ]; then
|
||||
echo "🔄 实时监控 $selected_route 路由成功日志"
|
||||
echo "========================================"
|
||||
echo "💡 提示:监控将持续运行,输入 'x' 并按回车可退出监控"
|
||||
echo ""
|
||||
# 在后台启动tail命令
|
||||
tail -f "$TODAY_DIR/${selected_route}_success.log" &
|
||||
TAIL_PID=$!
|
||||
|
||||
# 等待用户输入x退出
|
||||
while true; do
|
||||
read -p "" input
|
||||
if [ "$input" = "x" ] || [ "$input" = "X" ]; then
|
||||
kill $TAIL_PID 2>/dev/null
|
||||
echo ""
|
||||
echo "✅ 已停止实时监控"
|
||||
break
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "❌ 无效的路由选择"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
if [ ! -d "$TODAY_DIR" ]; then
|
||||
echo "❌ 今天的日志目录不存在"
|
||||
continue
|
||||
fi
|
||||
|
||||
# 获取可用路由列表
|
||||
echo "📋 请选择要监控的路由:"
|
||||
echo "======================"
|
||||
error_routes=$(find "$TODAY_DIR" -name "*_error.log" -exec basename {} _error.log \; 2>/dev/null | sort | uniq)
|
||||
|
||||
if [ -z "$error_routes" ]; then
|
||||
echo "❌ 今天没有失败日志的路由"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$error_routes" | nl
|
||||
echo ""
|
||||
read -p "请输入路由编号: " route_num
|
||||
|
||||
selected_route=$(echo "$error_routes" | sed -n "${route_num}p")
|
||||
if [ -n "$selected_route" ] && [ -f "$TODAY_DIR/${selected_route}_error.log" ]; then
|
||||
echo "🔄 实时监控 $selected_route 路由失败日志"
|
||||
echo "========================================"
|
||||
echo "💡 提示:监控将持续运行,输入 'x' 并按回车可退出监控"
|
||||
echo ""
|
||||
# 在后台启动tail命令
|
||||
tail -f "$TODAY_DIR/${selected_route}_error.log" &
|
||||
TAIL_PID=$!
|
||||
|
||||
# 等待用户输入x退出
|
||||
while true; do
|
||||
read -p "" input
|
||||
if [ "$input" = "x" ] || [ "$input" = "X" ]; then
|
||||
kill $TAIL_PID 2>/dev/null
|
||||
echo ""
|
||||
echo "✅ 已停止实时监控"
|
||||
break
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "❌ 无效的路由选择"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
read -p "请输入搜索关键词: " keyword
|
||||
if [ -z "$keyword" ]; then
|
||||
echo "❌ 搜索关键词不能为空"
|
||||
continue
|
||||
fi
|
||||
echo "🔍 搜索结果 (关键词: $keyword):"
|
||||
echo "==================================="
|
||||
found_any=false
|
||||
|
||||
# 使用临时文件来避免子shell问题
|
||||
temp_result="/tmp/search_result_$$"
|
||||
> "$temp_result"
|
||||
|
||||
# 搜索包含关键词的文件
|
||||
find "$TODAY_DIR" -name "*.log" -exec grep -l "$keyword" {} \; 2>/dev/null | while read file; do
|
||||
echo "📄 $file:" >> "$temp_result"
|
||||
grep "$keyword" "$file" | tail -5 >> "$temp_result"
|
||||
echo "" >> "$temp_result"
|
||||
done
|
||||
|
||||
# 检查是否有结果
|
||||
if [ -s "$temp_result" ]; then
|
||||
cat "$temp_result"
|
||||
else
|
||||
echo "❌ 未找到匹配的日志"
|
||||
fi
|
||||
|
||||
# 清理临时文件
|
||||
rm -f "$temp_result"
|
||||
;;
|
||||
4)
|
||||
echo "📅 选择特定日期:"
|
||||
echo "=================="
|
||||
echo "可用的日期:"
|
||||
ls -1 "$LOGS_DIR" | grep -E "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" | sort -r | nl
|
||||
echo ""
|
||||
read -p "请输入日期编号 (或按Enter返回): " date_num
|
||||
|
||||
if [ -z "$date_num" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
selected_date=$(ls -1 "$LOGS_DIR" | grep -E "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" | sort -r | sed -n "${date_num}p")
|
||||
if [ -n "$selected_date" ] && [ -d "$LOGS_DIR/$selected_date" ]; then
|
||||
echo ""
|
||||
echo "📄 $selected_date 的日志文件:"
|
||||
echo "========================"
|
||||
ls -la "$LOGS_DIR/$selected_date/" | grep -E "\.log$"
|
||||
|
||||
# 询问是否查看该日期的某个文件
|
||||
echo ""
|
||||
read -p "是否查看该日期的某个日志文件? (y/N): " view_file
|
||||
if [[ $view_file == [yY] ]]; then
|
||||
echo "该日期的日志文件列表:"
|
||||
ls -1 "$LOGS_DIR/$selected_date/" | grep -E "\.log$" | nl
|
||||
read -p "请输入文件编号: " file_num
|
||||
selected_file=$(ls -1 "$LOGS_DIR/$selected_date/" | grep -E "\.log$" | sed -n "${file_num}p")
|
||||
if [ -f "$LOGS_DIR/$selected_date/$selected_file" ]; then
|
||||
echo ""
|
||||
echo "📄 查看文件: $LOGS_DIR/$selected_date/$selected_file"
|
||||
echo "==========================================="
|
||||
tail -20 "$LOGS_DIR/$selected_date/$selected_file"
|
||||
else
|
||||
echo "❌ 无效的文件选择"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "❌ 无效的日期选择"
|
||||
fi
|
||||
;;
|
||||
5)
|
||||
echo "📊 日志统计:"
|
||||
echo "============"
|
||||
|
||||
if [ -d "$TODAY_DIR" ]; then
|
||||
success_count=$(find "$TODAY_DIR" -name "*_success.log" -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}' || echo "0")
|
||||
error_count=$(find "$TODAY_DIR" -name "*_error.log" -exec wc -l {} + 2>/dev/null | tail -1 | awk '{print $1}' || echo "0")
|
||||
|
||||
echo "今天的日志统计:"
|
||||
echo " 成功请求: $success_count 条"
|
||||
echo " 失败请求: $error_count 条"
|
||||
|
||||
if [ $((success_count + error_count)) -gt 0 ]; then
|
||||
success_rate=$(echo "scale=2; $success_count * 100 / ($success_count + $error_count)" | bc 2>/dev/null || echo "0")
|
||||
echo " 成功率: ${success_rate}%"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "路由统计 (按请求条数):"
|
||||
find "$TODAY_DIR" -name "*.log" -exec basename {} .log \; | sort | uniq -c | sort -nr
|
||||
|
||||
echo ""
|
||||
echo "文件大小统计:"
|
||||
find "$TODAY_DIR" -name "*.log" -exec ls -lh {} \; | awk '{print " " $9 " (" $5 ")"}'
|
||||
else
|
||||
echo "❌ 今天没有日志文件"
|
||||
fi
|
||||
;;
|
||||
6)
|
||||
read -p "确认清理7天前的日志? (y/N): " confirm
|
||||
if [[ $confirm == [yY] ]]; then
|
||||
echo "🧹 正在清理7天前的日志..."
|
||||
deleted_dirs=$(find "$LOGS_DIR" -type d -name "????-??-??" -mtime +7 -exec rm -rf {} \; 2>/dev/null | wc -l)
|
||||
echo "✅ 旧日志清理完成,删除了 $deleted_dirs 个目录"
|
||||
else
|
||||
echo "❌ 操作已取消"
|
||||
fi
|
||||
;;
|
||||
0)
|
||||
echo "👋 退出日志查看工具"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "❌ 无效选项,请输入0-6之间的数字"
|
||||
;;
|
||||
esac
|
||||
|
||||
# 如果不是退出选项,等待用户按回车继续
|
||||
if [ "$choice" != "0" ]; then
|
||||
echo ""
|
||||
read -p "按 Enter 键继续..." dummy
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# 启动主循环
|
||||
show_menu_and_process
|
||||
247
request/xrequest/Scripts/Activate.ps1
Normal file
247
request/xrequest/Scripts/Activate.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
63
request/xrequest/Scripts/activate
Normal file
63
request/xrequest/Scripts/activate
Normal file
@@ -0,0 +1,63 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV='D:\Code\Project\FT-Platform\request\xrequest'
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"Scripts":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(xrequest) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(xrequest) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
34
request/xrequest/Scripts/activate.bat
Normal file
34
request/xrequest/Scripts/activate.bat
Normal file
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
|
||||
rem This file is UTF-8 encoded, so we need to update the current code page while executing it
|
||||
for /f "tokens=2 delims=:." %%a in ('"%SystemRoot%\System32\chcp.com"') do (
|
||||
set _OLD_CODEPAGE=%%a
|
||||
)
|
||||
if defined _OLD_CODEPAGE (
|
||||
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
||||
)
|
||||
|
||||
set "VIRTUAL_ENV=D:\Code\Project\FT-Platform\request\xrequest"
|
||||
|
||||
if not defined PROMPT set PROMPT=$P$G
|
||||
|
||||
if defined _OLD_VIRTUAL_PROMPT set PROMPT=%_OLD_VIRTUAL_PROMPT%
|
||||
if defined _OLD_VIRTUAL_PYTHONHOME set PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT=%PROMPT%
|
||||
set PROMPT=(xrequest) %PROMPT%
|
||||
|
||||
if defined PYTHONHOME set _OLD_VIRTUAL_PYTHONHOME=%PYTHONHOME%
|
||||
set PYTHONHOME=
|
||||
|
||||
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
||||
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
||||
|
||||
set "PATH=%VIRTUAL_ENV%\Scripts;%PATH%"
|
||||
set "VIRTUAL_ENV_PROMPT=(xrequest) "
|
||||
|
||||
:END
|
||||
if defined _OLD_CODEPAGE (
|
||||
"%SystemRoot%\System32\chcp.com" %_OLD_CODEPAGE% > nul
|
||||
set _OLD_CODEPAGE=
|
||||
)
|
||||
22
request/xrequest/Scripts/deactivate.bat
Normal file
22
request/xrequest/Scripts/deactivate.bat
Normal file
@@ -0,0 +1,22 @@
|
||||
@echo off
|
||||
|
||||
if defined _OLD_VIRTUAL_PROMPT (
|
||||
set "PROMPT=%_OLD_VIRTUAL_PROMPT%"
|
||||
)
|
||||
set _OLD_VIRTUAL_PROMPT=
|
||||
|
||||
if defined _OLD_VIRTUAL_PYTHONHOME (
|
||||
set "PYTHONHOME=%_OLD_VIRTUAL_PYTHONHOME%"
|
||||
set _OLD_VIRTUAL_PYTHONHOME=
|
||||
)
|
||||
|
||||
if defined _OLD_VIRTUAL_PATH (
|
||||
set "PATH=%_OLD_VIRTUAL_PATH%"
|
||||
)
|
||||
|
||||
set _OLD_VIRTUAL_PATH=
|
||||
|
||||
set VIRTUAL_ENV=
|
||||
set VIRTUAL_ENV_PROMPT=
|
||||
|
||||
:END
|
||||
BIN
request/xrequest/Scripts/dotenv.exe
Normal file
BIN
request/xrequest/Scripts/dotenv.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/httpx.exe
Normal file
BIN
request/xrequest/Scripts/httpx.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/pip.exe
Normal file
BIN
request/xrequest/Scripts/pip.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/pip3.11.exe
Normal file
BIN
request/xrequest/Scripts/pip3.11.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/pip3.exe
Normal file
BIN
request/xrequest/Scripts/pip3.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/python.exe
Normal file
BIN
request/xrequest/Scripts/python.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/pythonw.exe
Normal file
BIN
request/xrequest/Scripts/pythonw.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/uvicorn.exe
Normal file
BIN
request/xrequest/Scripts/uvicorn.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/watchfiles.exe
Normal file
BIN
request/xrequest/Scripts/watchfiles.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/websockets.exe
Normal file
BIN
request/xrequest/Scripts/websockets.exe
Normal file
Binary file not shown.
BIN
request/xrequest/Scripts/wheel.exe
Normal file
BIN
request/xrequest/Scripts/wheel.exe
Normal file
Binary file not shown.
5
request/xrequest/pyvenv.cfg
Normal file
5
request/xrequest/pyvenv.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
home = D:\Softwares\Anaconda\envs\ft
|
||||
include-system-site-packages = false
|
||||
version = 3.11.14
|
||||
executable = D:\Softwares\Anaconda\envs\ft\python.exe
|
||||
command = D:\Softwares\Anaconda\envs\ft\python.exe -m venv D:\Code\Project\FT-Platform\request\xrequest
|
||||
@@ -1,5 +0,0 @@
|
||||
flask==3.0.0
|
||||
flask-cors==4.0.0
|
||||
pymysql==1.1.0
|
||||
pyyaml==6.0.1
|
||||
cryptography==41.0.7
|
||||
506
src/main.py
506
src/main.py
@@ -1,506 +0,0 @@
|
||||
"""
|
||||
远光软件微调平台 - Flask 后端 API
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pymysql
|
||||
import yaml
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
# 获取项目根目录
|
||||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, PROJECT_ROOT)
|
||||
|
||||
# 加载配置
|
||||
CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml')
|
||||
|
||||
|
||||
def load_config():
|
||||
"""加载配置文件"""
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
CONFIG = load_config()
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
"""获取数据库连接"""
|
||||
db_config = CONFIG['database']
|
||||
return pymysql.connect(
|
||||
host=db_config['host'],
|
||||
port=db_config['port'],
|
||||
user=db_config['username'],
|
||||
password=db_config['password'],
|
||||
database=db_config['name'],
|
||||
charset=db_config.get('charset', 'utf8mb4'),
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
|
||||
|
||||
def init_database():
|
||||
"""初始化数据库表"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
tables = [
|
||||
# 精调训练表
|
||||
"""CREATE TABLE IF NOT EXISTS fine_tune (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
base_model VARCHAR(255),
|
||||
train_type VARCHAR(50),
|
||||
train_method VARCHAR(50),
|
||||
dataset_id INT,
|
||||
valid_split VARCHAR(50),
|
||||
valid_ratio INT DEFAULT 10,
|
||||
output_model_name VARCHAR(255),
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
progress INT DEFAULT 0,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 我的模型表
|
||||
"""CREATE TABLE IF NOT EXISTS my_models (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(100),
|
||||
version VARCHAR(50),
|
||||
description TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 模型评测表
|
||||
"""CREATE TABLE IF NOT EXISTS model_eval (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
model_name VARCHAR(255) NOT NULL,
|
||||
dataset VARCHAR(255),
|
||||
metric VARCHAR(100),
|
||||
score DECIMAL(10, 4),
|
||||
status VARCHAR(50) DEFAULT 'completed',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 模型部署表
|
||||
"""CREATE TABLE IF NOT EXISTS model_deploy (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
model_name VARCHAR(255) NOT NULL,
|
||||
endpoint VARCHAR(255),
|
||||
instance VARCHAR(100),
|
||||
status VARCHAR(50) DEFAULT 'running',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 数据集管理表
|
||||
"""CREATE TABLE IF NOT EXISTS dataset_manage (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(100),
|
||||
size VARCHAR(50),
|
||||
count INT,
|
||||
description TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 数据生成表
|
||||
"""CREATE TABLE IF NOT EXISTS data_generate (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
template VARCHAR(255),
|
||||
count INT DEFAULT 0,
|
||||
status VARCHAR(50) DEFAULT 'pending',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 权限管理表
|
||||
"""CREATE TABLE IF NOT EXISTS permission (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'user',
|
||||
permissions TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 模型管理表
|
||||
"""CREATE TABLE IF NOT EXISTS model_manage (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(100),
|
||||
version VARCHAR(50),
|
||||
path VARCHAR(500),
|
||||
description TEXT,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 系统配置表
|
||||
"""CREATE TABLE IF NOT EXISTS sys_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE,
|
||||
config_value TEXT,
|
||||
description VARCHAR(255),
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)""",
|
||||
|
||||
# 用户表
|
||||
"""CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'user',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)"""
|
||||
]
|
||||
|
||||
for table_sql in tables:
|
||||
cursor.execute(table_sql)
|
||||
|
||||
# 插入默认管理员用户
|
||||
cursor.execute("SELECT * FROM users WHERE username = 'admin'")
|
||||
if not cursor.fetchone():
|
||||
cursor.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin', 'admin')")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print("数据库初始化完成")
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = CONFIG['secret_key']
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
|
||||
|
||||
# ============ 健康检查 ============
|
||||
@app.route('/api/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""健康检查接口"""
|
||||
return jsonify({'status': 'ok', 'code': 0})
|
||||
|
||||
|
||||
# ============ 通用 CRUD 操作 ============
|
||||
def generic_get_all(table_name, order_by='create_time DESC'):
|
||||
"""通用查询所有"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"SELECT * FROM {table_name} ORDER BY {order_by}")
|
||||
result = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
|
||||
def generic_get_by_id(table_name, id_val):
|
||||
"""通用按ID查询"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"SELECT * FROM {table_name} WHERE id = %s", (id_val,))
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return result
|
||||
|
||||
|
||||
def generic_create(table_name, data):
|
||||
"""通用创建"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
columns = ', '.join(data.keys())
|
||||
placeholders = ', '.join(['%s'] * len(data))
|
||||
sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders})"
|
||||
cursor.execute(sql, list(data.values()))
|
||||
conn.commit()
|
||||
new_id = cursor.lastrowid
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return new_id
|
||||
|
||||
|
||||
def generic_update(table_name, id_val, data):
|
||||
"""通用更新"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
set_clause = ', '.join([f"{k} = %s" for k in data.keys()])
|
||||
sql = f"UPDATE {table_name} SET {set_clause} WHERE id = %s"
|
||||
values = list(data.values()) + [id_val]
|
||||
cursor.execute(sql, values)
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
def generic_delete(table_name, id_val):
|
||||
"""通用删除"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f"DELETE FROM {table_name} WHERE id = %s", (id_val,))
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
# ============ 登录接口 ============
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def login():
|
||||
data = request.json
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
|
||||
user = cursor.fetchone()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
if user:
|
||||
return jsonify({'code': 0, 'message': '登录成功', 'data': {'username': user['username'], 'role': user['role']}})
|
||||
return jsonify({'code': 1, 'message': '用户名或密码错误'})
|
||||
|
||||
|
||||
# ============ 精调训练接口 ============
|
||||
@app.route('/api/fine-tune', methods=['GET'])
|
||||
def get_fine_tune():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('fine_tune')})
|
||||
|
||||
|
||||
@app.route('/api/fine-tune', methods=['POST'])
|
||||
def create_fine_tune():
|
||||
data = request.json
|
||||
new_id = generic_create('fine_tune', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/fine-tune/<int:id>', methods=['PUT'])
|
||||
def update_fine_tune(id):
|
||||
data = request.json
|
||||
generic_update('fine_tune', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/fine-tune/<int:id>', methods=['DELETE'])
|
||||
def delete_fine_tune(id):
|
||||
generic_delete('fine_tune', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 我的模型接口 ============
|
||||
@app.route('/api/my-models', methods=['GET'])
|
||||
def get_my_models():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('my_models')})
|
||||
|
||||
|
||||
@app.route('/api/my-models', methods=['POST'])
|
||||
def create_my_model():
|
||||
data = request.json
|
||||
new_id = generic_create('my_models', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/my-models/<int:id>', methods=['PUT'])
|
||||
def update_my_model(id):
|
||||
data = request.json
|
||||
generic_update('my_models', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/my-models/<int:id>', methods=['DELETE'])
|
||||
def delete_my_model(id):
|
||||
generic_delete('my_models', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 模型评测接口 ============
|
||||
@app.route('/api/model-eval', methods=['GET'])
|
||||
def get_model_eval():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('model_eval')})
|
||||
|
||||
|
||||
@app.route('/api/model-eval', methods=['POST'])
|
||||
def create_model_eval():
|
||||
data = request.json
|
||||
new_id = generic_create('model_eval', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/model-eval/<int:id>', methods=['PUT'])
|
||||
def update_model_eval(id):
|
||||
data = request.json
|
||||
generic_update('model_eval', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/model-eval/<int:id>', methods=['DELETE'])
|
||||
def delete_model_eval(id):
|
||||
generic_delete('model_eval', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 模型部署接口 ============
|
||||
@app.route('/api/model-deploy', methods=['GET'])
|
||||
def get_model_deploy():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('model_deploy')})
|
||||
|
||||
|
||||
@app.route('/api/model-deploy', methods=['POST'])
|
||||
def create_model_deploy():
|
||||
data = request.json
|
||||
new_id = generic_create('model_deploy', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/model-deploy/<int:id>', methods=['PUT'])
|
||||
def update_model_deploy(id):
|
||||
data = request.json
|
||||
generic_update('model_deploy', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/model-deploy/<int:id>', methods=['DELETE'])
|
||||
def delete_model_deploy(id):
|
||||
generic_delete('model_deploy', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 数据集管理接口 ============
|
||||
@app.route('/api/dataset-manage', methods=['GET'])
|
||||
def get_dataset_manage():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('dataset_manage')})
|
||||
|
||||
|
||||
@app.route('/api/dataset-manage', methods=['POST'])
|
||||
def create_dataset_manage():
|
||||
data = request.json
|
||||
new_id = generic_create('dataset_manage', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/dataset-manage/<int:id>', methods=['PUT'])
|
||||
def update_dataset_manage(id):
|
||||
data = request.json
|
||||
generic_update('dataset_manage', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/dataset-manage/<int:id>', methods=['DELETE'])
|
||||
def delete_dataset_manage(id):
|
||||
generic_delete('dataset_manage', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 数据生成接口 ============
|
||||
@app.route('/api/data-generate', methods=['GET'])
|
||||
def get_data_generate():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('data_generate')})
|
||||
|
||||
|
||||
@app.route('/api/data-generate', methods=['POST'])
|
||||
def create_data_generate():
|
||||
data = request.json
|
||||
new_id = generic_create('data_generate', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/data-generate/<int:id>', methods=['PUT'])
|
||||
def update_data_generate(id):
|
||||
data = request.json
|
||||
generic_update('data_generate', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/data-generate/<int:id>', methods=['DELETE'])
|
||||
def delete_data_generate(id):
|
||||
generic_delete('data_generate', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 权限管理接口 ============
|
||||
@app.route('/api/permission', methods=['GET'])
|
||||
def get_permission():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('permission')})
|
||||
|
||||
|
||||
@app.route('/api/permission', methods=['POST'])
|
||||
def create_permission():
|
||||
data = request.json
|
||||
new_id = generic_create('permission', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/permission/<int:id>', methods=['PUT'])
|
||||
def update_permission(id):
|
||||
data = request.json
|
||||
generic_update('permission', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/permission/<int:id>', methods=['DELETE'])
|
||||
def delete_permission(id):
|
||||
generic_delete('permission', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 模型管理接口 ============
|
||||
@app.route('/api/model-manage', methods=['GET'])
|
||||
def get_model_manage():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('model_manage')})
|
||||
|
||||
|
||||
@app.route('/api/model-manage', methods=['POST'])
|
||||
def create_model_manage():
|
||||
data = request.json
|
||||
new_id = generic_create('model_manage', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/model-manage/<int:id>', methods=['PUT'])
|
||||
def update_model_manage(id):
|
||||
data = request.json
|
||||
generic_update('model_manage', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/model-manage/<int:id>', methods=['DELETE'])
|
||||
def delete_model_manage(id):
|
||||
generic_delete('model_manage', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
# ============ 系统配置接口 ============
|
||||
@app.route('/api/sys-config', methods=['GET'])
|
||||
def get_sys_config():
|
||||
return jsonify({'code': 0, 'data': generic_get_all('sys_config')})
|
||||
|
||||
|
||||
@app.route('/api/sys-config', methods=['POST'])
|
||||
def create_sys_config():
|
||||
data = request.json
|
||||
new_id = generic_create('sys_config', data)
|
||||
return jsonify({'code': 0, 'message': '创建成功', 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/sys-config/<int:id>', methods=['PUT'])
|
||||
def update_sys_config(id):
|
||||
data = request.json
|
||||
generic_update('sys_config', id, data)
|
||||
return jsonify({'code': 0, 'message': '更新成功'})
|
||||
|
||||
|
||||
@app.route('/api/sys-config/<int:id>', methods=['DELETE'])
|
||||
def delete_sys_config(id):
|
||||
generic_delete('sys_config', id)
|
||||
return jsonify({'code': 0, 'message': '删除成功'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_database()
|
||||
app_config = CONFIG['app']
|
||||
app.run(host=app_config['host'], port=app_config['port'], debug=app_config.get('debug', True))
|
||||
14
src/run.sh
14
src/run.sh
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 启动远光软件微调平台后端服务
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# 检查并安装依赖
|
||||
if ! python3 -c "import flask" 2>/dev/null; then
|
||||
echo "正在安装依赖..."
|
||||
pip install -r ../requirements.txt
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
echo "启动后端服务..."
|
||||
python3 main.py
|
||||
396
streaming-test.html
Normal file
396
streaming-test.html
Normal file
@@ -0,0 +1,396 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>流式输出测试页面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #a0aec0;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stream-output {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: #2d3748;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-track {
|
||||
background: #edf2f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-thumb:hover {
|
||||
background: #a0aec0;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #edf2f7;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loading-dots::after {
|
||||
content: '.';
|
||||
animation: dots 1s steps(5, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60% { content: '...'; }
|
||||
80%, 100% { content: '.'; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✨ 流式输出测试</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始流式输出</button>
|
||||
<button id="stopBtn" disabled>停止输出</button>
|
||||
<button id="clearBtn">清空内容</button>
|
||||
</div>
|
||||
|
||||
<div id="output" class="stream-output">
|
||||
<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stats-item">
|
||||
<span>状态:</span>
|
||||
<span id="status" style="font-weight: 600;">就绪</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span>字符数:</span>
|
||||
<span id="charCount">0</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span>耗时:</span>
|
||||
<span id="timeElapsed">0.00s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 页面元素
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
const output = document.getElementById('output');
|
||||
const statusEl = document.getElementById('status');
|
||||
const charCountEl = document.getElementById('charCount');
|
||||
const timeElapsedEl = document.getElementById('timeElapsed');
|
||||
|
||||
// 状态变量
|
||||
let controller = null;
|
||||
let startTime = 0;
|
||||
let charCount = 0;
|
||||
let timer = null;
|
||||
|
||||
// 模拟大模型输出的文本内容
|
||||
const sampleText = `欢迎使用流式输出测试页面!
|
||||
|
||||
这是一个模拟大语言模型生成内容的演示。
|
||||
|
||||
流式输出是一种将生成的内容逐字逐句发送给用户的技术,
|
||||
它可以显著提升用户体验,让用户感觉内容是实时生成的。
|
||||
|
||||
在实际应用中,大语言模型会在生成每个token后立即发送给客户端,
|
||||
而不需要等待整个响应完成。
|
||||
|
||||
这种技术特别适合:
|
||||
- 长文本生成
|
||||
- 对话系统
|
||||
- 代码生成
|
||||
- 实时翻译
|
||||
|
||||
流式输出的优势包括:
|
||||
1. 更快的感知响应速度
|
||||
2. 更好的用户体验
|
||||
3. 更低的内存占用
|
||||
4. 支持更长的内容生成
|
||||
|
||||
在这个测试中,我们将模拟每秒生成约50个字符的速度,
|
||||
您可以看到文本是如何逐字逐句显示的。
|
||||
|
||||
您可以随时点击"停止输出"按钮来中断流式传输,
|
||||
也可以点击"清空内容"按钮重新开始。
|
||||
|
||||
让我们继续探索流式输出的更多应用场景...
|
||||
|
||||
在Web应用中,流式输出通常使用以下技术实现:
|
||||
- Server-Sent Events (SSE)
|
||||
- WebSockets
|
||||
- HTTP/2 Server Push
|
||||
|
||||
每种技术都有其优缺点,选择合适的技术取决于具体的应用场景。
|
||||
|
||||
例如,SSE适合单向的服务器到客户端通信,
|
||||
而WebSockets适合双向通信。
|
||||
|
||||
流式输出不仅提升了用户体验,
|
||||
也为开发者提供了更多的灵活性和控制能力。
|
||||
|
||||
感谢您使用这个测试页面!
|
||||
|
||||
希望您对流式输出技术有了更深入的了解。
|
||||
|
||||
如果您有任何问题或建议,欢迎随时提出。
|
||||
|
||||
祝您使用愉快!`;
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
charCountEl.textContent = charCount;
|
||||
|
||||
if (startTime > 0) {
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
timeElapsedEl.textContent = elapsed.toFixed(2) + 's';
|
||||
}
|
||||
}
|
||||
|
||||
// 清空输出
|
||||
function clearOutput() {
|
||||
output.innerHTML = '<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>';
|
||||
charCount = 0;
|
||||
updateStats();
|
||||
statusEl.textContent = '就绪';
|
||||
}
|
||||
|
||||
// 模拟流式输出
|
||||
async function simulateStreaming() {
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
clearOutput();
|
||||
|
||||
output.textContent = '';
|
||||
statusEl.innerHTML = '生成中 <span class="loading-dots"></span>';
|
||||
|
||||
startTime = Date.now();
|
||||
charCount = 0;
|
||||
|
||||
// 创建AbortController用于取消操作
|
||||
controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
try {
|
||||
// 使用ReadableStream模拟流式输出
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
let index = 0;
|
||||
|
||||
while (index < sampleText.length && !signal.aborted) {
|
||||
// 随机延迟,模拟真实生成速度
|
||||
const delay = Math.random() * 80 + 40; // 40-120ms
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// 每次发送1-5个字符
|
||||
const chunkSize = Math.floor(Math.random() * 5) + 1;
|
||||
const chunk = sampleText.slice(index, index + chunkSize);
|
||||
|
||||
controller.enqueue(new TextEncoder().encode(chunk));
|
||||
index += chunkSize;
|
||||
|
||||
// 更新统计信息
|
||||
charCount += chunk.length;
|
||||
updateStats();
|
||||
}
|
||||
|
||||
controller.close();
|
||||
},
|
||||
cancel() {
|
||||
console.log('流式输出已取消');
|
||||
}
|
||||
});
|
||||
|
||||
// 读取并显示流内容
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// 追加内容到输出
|
||||
output.textContent += decoder.decode(value, { stream: true });
|
||||
|
||||
// 自动滚动到底部
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
// 完成
|
||||
statusEl.textContent = '完成';
|
||||
output.style.borderColor = '#48bb78';
|
||||
|
||||
// 添加完成标记
|
||||
const doneMarker = document.createElement('div');
|
||||
doneMarker.style.cssText = `
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
`;
|
||||
doneMarker.textContent = '✅ 流式输出完成!';
|
||||
output.appendChild(doneMarker);
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
statusEl.textContent = '已停止';
|
||||
output.style.borderColor = '#ed8936';
|
||||
|
||||
const stopMarker = document.createElement('div');
|
||||
stopMarker.style.cssText = `
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
`;
|
||||
stopMarker.textContent = '⏸️ 流式输出已停止';
|
||||
output.appendChild(stopMarker);
|
||||
} else {
|
||||
statusEl.textContent = '错误';
|
||||
output.innerHTML += `\n\n❌ 发生错误: ${error.message}`;
|
||||
output.style.borderColor = '#f56565';
|
||||
}
|
||||
} finally {
|
||||
// 重置状态
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
controller = null;
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止流式输出
|
||||
function stopStreaming() {
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
startBtn.addEventListener('click', simulateStreaming);
|
||||
stopBtn.addEventListener('click', stopStreaming);
|
||||
clearBtn.addEventListener('click', clearOutput);
|
||||
|
||||
// 页面加载完成
|
||||
window.addEventListener('load', () => {
|
||||
console.log('流式输出测试页面已加载');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,95 +0,0 @@
|
||||
# 🔥 大模型微调平台
|
||||
|
||||
> **静态HTML页面,使用假数据模拟系统监控**
|
||||
|
||||
## 🚀 快速启动
|
||||
|
||||
### 方式1: 直接打开文件(推荐)
|
||||
```bash
|
||||
cd web
|
||||
./start.sh
|
||||
```
|
||||
选择 `1` 直接打开文件
|
||||
|
||||
### 方式2: HTTP服务器(可通过IP访问)
|
||||
```bash
|
||||
cd web
|
||||
./start.sh
|
||||
```
|
||||
选择 `2` 启动HTTP服务器
|
||||
|
||||
或直接运行:
|
||||
```bash
|
||||
./start-http-server.sh
|
||||
```
|
||||
|
||||
## 📊 访问应用
|
||||
|
||||
### 直接打开文件
|
||||
- **主页**: `file:///data/code/FT_Platform/YG_FT_Platform/web/pages/main.html`
|
||||
- **登录**: `file:///data/code/FT_Platform/YG_FT_Platform/web/pages/login.html`
|
||||
|
||||
### HTTP服务器访问(端口8000)
|
||||
- **主页**: `http://10.10.10.77:8000/pages/main.html`
|
||||
- **登录**: `http://10.10.10.77:8000/pages/login.html`
|
||||
|
||||
## ✅ 模拟系统数据
|
||||
|
||||
现在显示的是**模拟数据**:
|
||||
- ✅ **CPU使用率**: 随机模拟数据
|
||||
- ✅ **内存使用率**: 随机模拟数据
|
||||
- ✅ **GPU使用率**: 随机模拟数据
|
||||
|
||||
每2秒自动更新一次数据!
|
||||
|
||||
## 🔧 技术架构
|
||||
|
||||
- **前端**: HTML + CSS + JavaScript
|
||||
- **图表**: Chart.js
|
||||
- **样式**: Tailwind CSS
|
||||
- **服务器**: Python HTTP服务器(可选)
|
||||
|
||||
## 📱 特性
|
||||
|
||||
- 静态HTML页面,无需后端服务
|
||||
- 模拟数据自动更新
|
||||
- 响应式设计 (支持手机/平板)
|
||||
- 20个数据点滚动显示
|
||||
- 彩色编码 (CPU红色, GPU绿色, 内存蓝色)
|
||||
|
||||
## 🛑 停止服务器
|
||||
|
||||
### 停止HTTP服务器
|
||||
```bash
|
||||
# 按 Ctrl+C
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 问题: 找不到Python
|
||||
**解决**: 安装Python或使用方式1直接打开文件
|
||||
|
||||
### 问题: 端口8000被占用
|
||||
**解决**:
|
||||
```bash
|
||||
# 找到占用端口的进程
|
||||
lsof -i :8000
|
||||
# 或使用其他端口
|
||||
python3 -m http.server 8001
|
||||
```
|
||||
|
||||
### 问题: IP地址无法访问
|
||||
**解决**:
|
||||
1. 确保使用HTTP服务器方式启动
|
||||
2. 检查防火墙设置,确保端口8000开放
|
||||
3. 使用正确的IP地址
|
||||
|
||||
### 问题: 数据不更新
|
||||
**解决**: 刷新页面或重新启动
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如果遇到问题:
|
||||
1. 首先尝试方式1直接打开文件
|
||||
2. 如果需要IP访问,使用方式2启动HTTP服务器
|
||||
3. 检查Python是否安装
|
||||
57
web/index.html
Normal file
57
web/index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FT-Platform</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
margin: 0;
|
||||
padding: 40px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 FT-Platform</h1>
|
||||
<p>大模型微调平台</p>
|
||||
<a href="/pages/main.html" class="btn">进入平台</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2683
web/main.html
Normal file
2683
web/main.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,455 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>添加自定义工具 / 远光软件微调平台</title>
|
||||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar-section-title {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(191, 203, 217, 0.7);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: rgba(0, 21, 41, 0.2);
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s, outline 0.2s;
|
||||
}
|
||||
.form-input:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.icon-option {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.icon-option:hover {
|
||||
border-color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
.icon-option.selected {
|
||||
border-color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
.bg-primary { background-color: #1890ff; }
|
||||
.text-primary { color: #1890ff; }
|
||||
.border-primary { border-color: #1890ff; }
|
||||
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
|
||||
<!-- 侧边导航 -->
|
||||
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
|
||||
<!-- 平台LOGO区域 -->
|
||||
<div class="p-4 border-b border-[#001529]/30 flex items-center">
|
||||
<img src="../assets/logo/logo.png" alt="Logo" class="w-6 h-6 object-contain mr-2">
|
||||
<span class="text-white font-medium">远光软件微调平台</span>
|
||||
</div>
|
||||
|
||||
<!-- 导航主区域 -->
|
||||
<nav class="flex-1 overflow-y-auto py-2">
|
||||
<!-- 第一分区:模型服务 -->
|
||||
<div class="sidebar-section-title">模型服务</div>
|
||||
<a href="main.html" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cogs w-5 text-center"></i>
|
||||
<span class="ml-2">模型调优</span>
|
||||
</a>
|
||||
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">我的模型</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-eval" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-line-chart w-5 text-center"></i>
|
||||
<span class="ml-2">模型评测</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-deploy" data-page="model-deploy" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-server w-5 text-center"></i>
|
||||
<span class="ml-2">模型对比</span>
|
||||
</a>
|
||||
|
||||
<!-- 第二分区:资源管理 -->
|
||||
<div class="sidebar-section-title mt-6">资源管理</div>
|
||||
<a href="main.html?page=model-manage" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cube w-5 text-center"></i>
|
||||
<span class="ml-2">模型管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=dataset-manage" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-file-text w-5 text-center"></i>
|
||||
<span class="ml-2">数据集管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=data-generate" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">其他工具</span>
|
||||
</a>
|
||||
|
||||
<!-- 第三分区:系统设置 -->
|
||||
<div class="sidebar-section-title mt-6">系统设置</div>
|
||||
<a href="main.html?page=config" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-bar-chart w-5 text-center"></i>
|
||||
<span class="ml-2">平台性能</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- 底部信息区域 -->
|
||||
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
|
||||
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
|
||||
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between px-6 h-14">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="main.html?page=data-generate" class="text-gray-500 hover:text-gray-700 flex items-center">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
<span class="ml-1">上一步</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative group">
|
||||
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
|
||||
<div class="absolute right-0 top-full mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px]">
|
||||
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
|
||||
<i class="fa fa-sign-out mr-1"></i>退出登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||||
<!-- 页面标题 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-b border-gray-100 mb-4">
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-primary cursor-pointer hover:underline" onclick="window.location.href='main.html?page=data-generate'">其他工具</span>
|
||||
<span class="mx-2 text-gray-300">/</span>
|
||||
<span class="text-gray-800 font-medium" id="pageTitle">添加自定义工具</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<div class="p-6 max-w-2xl">
|
||||
<form id="toolForm">
|
||||
<!-- 工具名称 -->
|
||||
<div class="mb-6">
|
||||
<label class="form-label">
|
||||
<span class="text-red-500 mr-1">*</span>工具名称
|
||||
</label>
|
||||
<input type="text" name="name" class="form-input" placeholder="请输入工具名称" maxlength="30">
|
||||
<p class="text-xs text-gray-400 mt-1">支持中文、英文、数字,最多30个字符</p>
|
||||
</div>
|
||||
|
||||
<!-- 工具描述 -->
|
||||
<div class="mb-6">
|
||||
<label class="form-label">工具描述</label>
|
||||
<textarea name="description" rows="3" class="form-input resize-none" placeholder="请输入工具描述" maxlength="100"></textarea>
|
||||
<p class="text-xs text-gray-400 mt-1"><span id="descCount">0</span> / 100</p>
|
||||
</div>
|
||||
|
||||
<!-- 跳转地址 -->
|
||||
<div class="mb-6">
|
||||
<label class="form-label">
|
||||
<span class="text-red-500 mr-1">*</span>跳转地址
|
||||
</label>
|
||||
<input type="text" name="url" class="form-input" placeholder="如:custom-tool.html 或 https://example.com">
|
||||
<p class="text-xs text-gray-400 mt-1">输入工具页面的相对路径或完整URL</p>
|
||||
</div>
|
||||
|
||||
<!-- 图标选择 -->
|
||||
<div class="mb-6">
|
||||
<label class="form-label">选择图标</label>
|
||||
<div class="grid grid-cols-8 gap-2" id="iconGrid"></div>
|
||||
<input type="hidden" id="selectedIcon" value="fa-cog" name="icon">
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-check mr-2"></i>保存
|
||||
</button>
|
||||
<a href="main.html?page=data-generate" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300 transition-colors">
|
||||
<i class="fa fa-times mr-2"></i>取消
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API 基础地址
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:8080/api`;
|
||||
};
|
||||
const API_BASE = getApiBase();
|
||||
|
||||
// 当前是否为编辑模式
|
||||
let isEditMode = false;
|
||||
let editToolId = null;
|
||||
|
||||
// 可选图标列表
|
||||
function getIconOptions() {
|
||||
return [
|
||||
'fa-cog', 'fa-cogs', 'fa-database', 'fa-code', 'fa-file-text',
|
||||
'fa-file-code', 'fa-file-excel', 'fa-file', 'fa-folder',
|
||||
'fa-wrench', 'fa-tools', 'fa-magic', 'fa-puzzle-piece',
|
||||
'fa-plug', 'fa-cube', 'fa-cubes', 'fa-gear', 'fa-sliders',
|
||||
'fa-filter', 'fa-refresh', 'fa-exchange', 'fa-arrows-alt',
|
||||
'fa-random', 'fa-random', 'fa-link', 'fa-chain',
|
||||
'fa-edit', 'fa-pencil', 'fa-pen', 'fa-plus-circle',
|
||||
'fa-star', 'fa-heart', 'fa-bookmark', 'fa-tag'
|
||||
];
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 检查是否为编辑模式
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
isEditMode = urlParams.get('edit') === 'true';
|
||||
|
||||
// 渲染图标选择
|
||||
renderIconGrid();
|
||||
|
||||
// 如果是编辑模式,加载工具数据
|
||||
if (isEditMode) {
|
||||
loadToolForEdit();
|
||||
}
|
||||
|
||||
// 描述字数统计
|
||||
const descInput = document.querySelector('textarea[name="description"]');
|
||||
if (descInput) {
|
||||
descInput.addEventListener('input', () => {
|
||||
document.getElementById('descCount').textContent = descInput.value.length;
|
||||
});
|
||||
}
|
||||
|
||||
// 绑定导航点击事件
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (!this.href.includes('custom-tool-create')) {
|
||||
e.preventDefault();
|
||||
window.location.href = this.href;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 加载工具数据进行编辑
|
||||
function loadToolForEdit() {
|
||||
const editToolStr = localStorage.getItem('editTool');
|
||||
if (editToolStr) {
|
||||
const tool = JSON.parse(editToolStr);
|
||||
editToolId = tool.id;
|
||||
|
||||
// 更新页面标题
|
||||
document.getElementById('pageTitle').textContent = '修改自定义工具';
|
||||
document.title = '修改自定义工具 / 远光软件微调平台';
|
||||
|
||||
// 填充表单
|
||||
document.querySelector('input[name="name"]').value = tool.name || '';
|
||||
document.querySelector('textarea[name="description"]').value = tool.description || '';
|
||||
document.querySelector('input[name="url"]').value = tool.url || '';
|
||||
|
||||
// 选中图标
|
||||
const icon = tool.icon || 'fa-cog';
|
||||
setSelectedIcon(icon);
|
||||
|
||||
// 更新字数统计
|
||||
document.getElementById('descCount').textContent = (tool.description || '').length;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置选中的图标
|
||||
function setSelectedIcon(iconClass) {
|
||||
document.querySelectorAll('.icon-option').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
if (item.dataset.icon === iconClass) {
|
||||
item.classList.add('selected');
|
||||
}
|
||||
});
|
||||
document.querySelector('input[name="icon"]').value = iconClass;
|
||||
}
|
||||
|
||||
// 渲染图标选择网格
|
||||
function renderIconGrid() {
|
||||
const iconGrid = document.getElementById('iconGrid');
|
||||
const icons = getIconOptions();
|
||||
iconGrid.innerHTML = icons.map((icon, idx) => `
|
||||
<div class="icon-option ${idx === 0 ? 'selected' : ''}" data-icon="${icon}" onclick="selectIcon(this)">
|
||||
<i class="fa ${icon} text-lg text-gray-600"></i>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 选择图标
|
||||
function selectIcon(el) {
|
||||
document.querySelectorAll('.icon-option').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
el.classList.add('selected');
|
||||
document.querySelector('input[name="icon"]').value = el.dataset.icon;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submitForm() {
|
||||
const form = document.getElementById('toolForm');
|
||||
const formData = new FormData(form);
|
||||
const name = formData.get('name').trim();
|
||||
const url = formData.get('url').trim();
|
||||
const icon = formData.get('icon');
|
||||
const description = formData.get('description').trim();
|
||||
|
||||
if (!name) {
|
||||
showMessage('提示', '请输入工具名称', 'warning');
|
||||
return;
|
||||
}
|
||||
if (!url) {
|
||||
showMessage('提示', '请输入跳转地址', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEditMode && editToolId) {
|
||||
// 编辑模式:更新现有工具
|
||||
updateCustomTool(editToolId, name, description, icon, url);
|
||||
} else {
|
||||
// 新增模式:创建新工具
|
||||
const toolId = 'custom_' + Date.now();
|
||||
const newTool = {
|
||||
id: toolId,
|
||||
name: name,
|
||||
description: description || '自定义工具',
|
||||
icon: icon,
|
||||
url: url
|
||||
};
|
||||
saveCustomTool(newTool);
|
||||
showMessage('成功', '自定义工具添加成功!', 'success', () => {
|
||||
window.location.href = 'main.html?page=data-generate';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新自定义工具
|
||||
function updateCustomTool(toolId, name, description, icon, url) {
|
||||
let customTools = JSON.parse(localStorage.getItem('customTools') || '[]');
|
||||
const index = customTools.findIndex(t => t.id === toolId);
|
||||
if (index !== -1) {
|
||||
customTools[index] = {
|
||||
...customTools[index],
|
||||
name: name,
|
||||
description: description || '自定义工具',
|
||||
icon: icon,
|
||||
url: url
|
||||
};
|
||||
localStorage.setItem('customTools', JSON.stringify(customTools));
|
||||
// 清除编辑数据
|
||||
localStorage.removeItem('editTool');
|
||||
showMessage('成功', '自定义工具修改成功!', 'success', () => {
|
||||
window.location.href = 'main.html?page=data-generate';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保存自定义工具
|
||||
function saveCustomTool(tool) {
|
||||
let customTools = JSON.parse(localStorage.getItem('customTools') || '[]');
|
||||
customTools.push(tool);
|
||||
localStorage.setItem('customTools', JSON.stringify(customTools));
|
||||
}
|
||||
|
||||
// ============ 自定义消息弹窗 ============
|
||||
function showMessage(title, message, type = 'info', onConfirm) {
|
||||
const modal = document.getElementById('customModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalMessage = document.getElementById('modalMessage');
|
||||
const modalIcon = document.getElementById('modalIcon');
|
||||
const modalConfirmBtn = document.getElementById('modalConfirmBtn2');
|
||||
const modalBtnGroup = document.getElementById('modalBtnGroup');
|
||||
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
|
||||
|
||||
modalTitle.textContent = title;
|
||||
modalMessage.innerHTML = message;
|
||||
|
||||
if (type === 'success') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
|
||||
} else if (type === 'error') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
|
||||
} else if (type === 'warning') {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
|
||||
} else {
|
||||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
|
||||
}
|
||||
|
||||
modalBtnGroup.classList.add('hidden');
|
||||
modalSingleBtnGroup.classList.remove('hidden');
|
||||
modalConfirmBtn.className = type === 'error' ? 'px-6 py-2 bg-danger text-white rounded-lg hover:bg-danger/90 transition-colors' : 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
modalConfirmBtn.onclick = () => {
|
||||
modal.classList.add('hidden');
|
||||
document.body.style.overflow = '';
|
||||
if (onConfirm) onConfirm();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- 自定义消息弹窗 -->
|
||||
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-md w-full mx-4 overflow-hidden">
|
||||
<div class="p-6 text-center">
|
||||
<div id="modalIcon"></div>
|
||||
<h3 id="modalTitle" class="text-lg font-medium text-gray-800 mb-2"></h3>
|
||||
<p id="modalMessage" class="text-gray-600 text-sm"></p>
|
||||
</div>
|
||||
<div id="modalBtnGroup" class="hidden px-6 pb-6">
|
||||
<button id="modalConfirmBtn" class="px-4 py-2 bg-primary text-white rounded-lg w-full hover:bg-primary/90 transition-colors">
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
|
||||
<button id="modalConfirmBtn2" class="px-6 py-2 text-white rounded-lg transition-colors">确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,440 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上传数据集 / 远光软件微调平台</title>
|
||||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar-section-title {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(191, 203, 217, 0.7);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: rgba(0, 21, 41, 0.2);
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s, outline 0.2s;
|
||||
}
|
||||
.form-input:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
}
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s, outline 0.2s;
|
||||
appearance: none;
|
||||
background-color: white;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
.form-select:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
}
|
||||
.radio-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.upload-area:hover,
|
||||
.upload-area.drag-over {
|
||||
border-color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
.bg-primary { background-color: #1890ff; }
|
||||
.text-primary { color: #1890ff; }
|
||||
.border-primary { border-color: #1890ff; }
|
||||
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
|
||||
<!-- 侧边导航 -->
|
||||
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
|
||||
<!-- 平台LOGO区域 -->
|
||||
<div class="p-4 border-b border-[#001529]/30 flex items-center">
|
||||
<img src="../assets/logo/logo.png" alt="Logo" class="w-6 h-6 object-contain mr-2">
|
||||
<span class="text-white font-medium">远光软件微调平台</span>
|
||||
</div>
|
||||
|
||||
<!-- 导航主区域 -->
|
||||
<nav class="flex-1 overflow-y-auto py-2">
|
||||
<!-- 第一分区:模型服务 -->
|
||||
<div class="sidebar-section-title">模型服务</div>
|
||||
<a href="main.html" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cogs w-5 text-center"></i>
|
||||
<span class="ml-2">模型调优</span>
|
||||
</a>
|
||||
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">我的模型</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-eval" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-line-chart w-5 text-center"></i>
|
||||
<span class="ml-2">模型评测</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-deploy" data-page="model-deploy" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-server w-5 text-center"></i>
|
||||
<span class="ml-2">模型部署</span>
|
||||
</a>
|
||||
|
||||
<!-- 第二分区:资源管理 -->
|
||||
<div class="sidebar-section-title mt-6">资源管理</div>
|
||||
<a href="main.html?page=model-manage" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cube w-5 text-center"></i>
|
||||
<span class="ml-2">模型管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=dataset-manage" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-file-text w-5 text-center"></i>
|
||||
<span class="ml-2">数据集管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=data-generate" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">其他工具</span>
|
||||
</a>
|
||||
|
||||
<!-- 第三分区:系统设置 -->
|
||||
<div class="sidebar-section-title mt-6">系统设置</div>
|
||||
<a href="main.html?page=config" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-bar-chart w-5 text-center"></i>
|
||||
<span class="ml-2">平台性能</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- 底部信息区域 -->
|
||||
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
|
||||
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
|
||||
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between px-6 h-14">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="#" onclick="goBack()" class="text-gray-500 hover:text-gray-700 flex items-center">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
<span class="ml-1">上一步</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative group">
|
||||
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
|
||||
<div class="absolute right-0 top-full mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px]">
|
||||
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
|
||||
<i class="fa fa-sign-out mr-1"></i>退出登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||||
<!-- 页面标题 -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-b border-gray-100 mb-4">
|
||||
<div class="flex items-center text-sm">
|
||||
<span id="breadcrumbParent" class="text-primary cursor-pointer hover:underline" onclick="goBack()">数据集管理</span>
|
||||
<span class="mx-2 text-gray-300">/</span>
|
||||
<span class="text-gray-800 font-medium">上传数据集</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<div class="bg-white rounded-lg shadow-sm">
|
||||
<div class="p-6 max-w-3xl">
|
||||
<form id="datasetForm">
|
||||
<!-- 1. 数据集名称输入框 -->
|
||||
<div class="mb-6">
|
||||
<label class="form-label">
|
||||
数据集名称
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="数据集名称"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none"
|
||||
maxlength="20"
|
||||
>
|
||||
<span class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">0 / 20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 数据集类型(单选按钮) -->
|
||||
<div class="mb-6 pl-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">数据集类型</label>
|
||||
<div class="flex items-center space-x-6">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="dataset_type" id="train-set" value="train" checked onchange="switchDatasetType()" class="radio-custom absolute opacity-0">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="w-4 h-4 rounded-full border-2 border-gray-300 flex items-center justify-center">
|
||||
<div class="radio-dot"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">训练集</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="dataset_type" id="eval-set" value="eval" onchange="switchDatasetType()" class="radio-custom absolute opacity-0">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="w-4 h-4 rounded-full border-2 border-gray-300 flex items-center justify-center">
|
||||
<div class="radio-dot"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">评测集</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 存储位置 -->
|
||||
<div class="mb-6 pl-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">存储位置</label>
|
||||
<div class="flex items-center space-x-6">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="storage" value="local" class="radio-custom absolute opacity-0" checked>
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="w-4 h-4 rounded-full border-2 border-gray-300 flex items-center justify-center">
|
||||
<div class="radio-dot"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">本地存储</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="storage" value="cloud" class="radio-custom absolute opacity-0">
|
||||
<div class="flex items-center space-x-1">
|
||||
<div class="w-4 h-4 rounded-full border-2 border-gray-300 flex items-center justify-center">
|
||||
<div class="radio-dot"></div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">云平台存储</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 上传文件区域 -->
|
||||
<div class="mb-6 pl-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">上传文件</label>
|
||||
<p class="text-xs text-gray-500 mb-2">选择文件进行上传,数据格式可下载模板查看,一次最多导入10个文件</p>
|
||||
<div
|
||||
id="upload-area"
|
||||
class="upload-area border-2 border-dashed border-gray-300 rounded-lg p-8 text-center transition-colors cursor-pointer relative"
|
||||
>
|
||||
<input type="file" id="file-upload" class="absolute opacity-0" multiple accept=".jsonl,.xls,.xlsx">
|
||||
<div class="flex flex-col items-center space-y-2">
|
||||
<i class="fa fa-cloud-upload text-2xl text-gray-400"></i>
|
||||
<p class="text-sm text-gray-600">点击或将文件拖拽到这里上传 (0/10)</p>
|
||||
<p class="text-xs text-gray-500">支持扩展名:jsonl, xls, xlsx, 文件最大200MB<br>一次最多导入10个文件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 8. 模板链接 -->
|
||||
<div class="mb-6 pl-4 space-x-4">
|
||||
<a href="#" class="text-primary text-sm hover:underline">
|
||||
<i class="fa fa-file-excel mr-1"></i>EXCEL数据模板
|
||||
</a>
|
||||
<a href="#" class="text-primary text-sm hover:underline">
|
||||
<i class="fa fa-file-code mr-1"></i>JSON数据模板
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-gray-100 mt-8">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
|
||||
保存
|
||||
</button>
|
||||
<a href="main.html?page=dataset-manage" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
|
||||
取消
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API 基础地址
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:8080/api`;
|
||||
};
|
||||
const API_BASE = getApiBase();
|
||||
|
||||
// 返回页面
|
||||
let backUrl = 'main.html?page=dataset-manage';
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 根据URL参数设置返回页面
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const from = urlParams.get('from');
|
||||
const breadcrumbParent = document.getElementById('breadcrumbParent');
|
||||
if (from === 'fine-tune') {
|
||||
backUrl = 'fine-tune-create.html';
|
||||
if (breadcrumbParent) {
|
||||
breadcrumbParent.textContent = '创建训练任务';
|
||||
}
|
||||
}
|
||||
// 文件上传区域拖拽逻辑
|
||||
const uploadArea = document.getElementById('upload-area');
|
||||
const fileUpload = document.getElementById('file-upload');
|
||||
|
||||
// 点击上传区域触发文件选择
|
||||
uploadArea.addEventListener('click', () => fileUpload.click());
|
||||
|
||||
// 拖拽事件处理
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => uploadArea.classList.add('drag-over'), false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
uploadArea.addEventListener(eventName, () => uploadArea.classList.remove('drag-over'), false);
|
||||
});
|
||||
|
||||
// 处理文件拖放
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length) {
|
||||
fileUpload.files = files;
|
||||
console.log('拖放的文件:', files);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听文件选择
|
||||
fileUpload.addEventListener('change', () => {
|
||||
console.log('选择的文件:', fileUpload.files);
|
||||
});
|
||||
|
||||
// 绑定导航点击事件
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (!this.href.includes('dataset-create')) {
|
||||
e.preventDefault();
|
||||
window.location.href = this.href;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化单选框选中样式
|
||||
initRadioStyles();
|
||||
});
|
||||
|
||||
// 初始化单选框选中样式
|
||||
function initRadioStyles() {
|
||||
document.querySelectorAll('.radio-custom').forEach(radio => {
|
||||
updateRadioStyle(radio);
|
||||
radio.addEventListener('change', function() {
|
||||
document.querySelectorAll('.radio-custom').forEach(r => updateRadioStyle(r));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新单选框样式
|
||||
function updateRadioStyle(radio) {
|
||||
const parent = radio.closest('label');
|
||||
const dotContainer = parent.querySelector('.w-4');
|
||||
const dot = parent.querySelector('.radio-dot');
|
||||
if (radio.checked) {
|
||||
if (dotContainer) {
|
||||
dotContainer.classList.add('border-primary', 'bg-primary/10');
|
||||
dotContainer.classList.remove('border-gray-300');
|
||||
}
|
||||
if (dot) {
|
||||
dot.classList.add('bg-primary');
|
||||
dot.classList.remove('bg-transparent');
|
||||
}
|
||||
} else {
|
||||
if (dotContainer) {
|
||||
dotContainer.classList.remove('border-primary', 'bg-primary/10');
|
||||
dotContainer.classList.add('border-gray-300');
|
||||
}
|
||||
if (dot) {
|
||||
dot.classList.remove('bg-primary');
|
||||
dot.classList.add('bg-transparent');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数据集类型切换逻辑
|
||||
function switchDatasetType() {
|
||||
// 数据集类型切换逻辑
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
const form = document.getElementById('datasetForm');
|
||||
const formData = new FormData(form);
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
dataset_type: formData.get('dataset_type'),
|
||||
storage: formData.get('storage')
|
||||
};
|
||||
|
||||
if (!data.name) {
|
||||
alert('请输入数据集名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/dataset-manage`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
alert('创建成功!');
|
||||
window.location.href = 'main.html?page=dataset-manage';
|
||||
} else {
|
||||
alert(result.message || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('创建失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
function goBack() {
|
||||
window.location.href = backUrl;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,787 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>创建训练任务 / 远光软件微调平台</title>
|
||||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar-section-title {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(191, 203, 217, 0.7);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.nav-link:hover {
|
||||
background-color: rgba(0, 21, 41, 0.2);
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s, outline 0.2s;
|
||||
}
|
||||
.form-input:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
}
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
transition: border-color 0.2s, outline 0.2s;
|
||||
appearance: none;
|
||||
background-color: white;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right 0.5rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5em 1.5em;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
.form-select:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
}
|
||||
.card-radio {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.card-radio.active {
|
||||
border-color: #1890ff;
|
||||
background-color: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
.card-radio:hover {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
.bg-primary { background-color: #1890ff; }
|
||||
.text-primary { color: #1890ff; }
|
||||
.border-primary { border-color: #1890ff; }
|
||||
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
|
||||
<!-- 侧边导航 -->
|
||||
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
|
||||
<!-- 平台LOGO区域 -->
|
||||
<div class="p-4 border-b border-[#001529]/30 flex items-center">
|
||||
<img src="../assets/logo/logo.png" alt="Logo" class="w-6 h-6 object-contain mr-2">
|
||||
<span class="text-white font-medium">远光软件微调平台</span>
|
||||
</div>
|
||||
|
||||
<!-- 导航主区域 -->
|
||||
<nav class="flex-1 overflow-y-auto py-2">
|
||||
<!-- 第一分区:模型服务 -->
|
||||
<div class="sidebar-section-title">模型服务</div>
|
||||
<a href="main.html" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cogs w-5 text-center"></i>
|
||||
<span class="ml-2">模型调优</span>
|
||||
</a>
|
||||
<a href="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">我的模型</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-eval" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-line-chart w-5 text-center"></i>
|
||||
<span class="ml-2">模型评测</span>
|
||||
</a>
|
||||
<a href="main.html?page=model-deploy" data-page="model-deploy" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-server w-5 text-center"></i>
|
||||
<span class="ml-2">模型部署</span>
|
||||
</a>
|
||||
|
||||
<!-- 第二分区:资源管理 -->
|
||||
<div class="sidebar-section-title mt-6">资源管理</div>
|
||||
<a href="main.html?page=model-manage" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-cube w-5 text-center"></i>
|
||||
<span class="ml-2">模型管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=dataset-manage" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-file-text w-5 text-center"></i>
|
||||
<span class="ml-2">数据集管理</span>
|
||||
</a>
|
||||
<a href="main.html?page=data-generate" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-database w-5 text-center"></i>
|
||||
<span class="ml-2">其他工具</span>
|
||||
</a>
|
||||
|
||||
<!-- 第三分区:系统设置 -->
|
||||
<div class="sidebar-section-title mt-6">系统设置</div>
|
||||
<a href="main.html?page=config" data-page="config" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||||
<i class="fa fa-bar-chart w-5 text-center"></i>
|
||||
<span class="ml-2">平台性能</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- 底部信息区域 -->
|
||||
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
|
||||
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
|
||||
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- 顶部导航 -->
|
||||
<header class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="flex items-center justify-between px-6 h-14">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="main.html" class="text-gray-500 hover:text-gray-700 flex items-center">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
<span class="ml-1">上一步</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative group">
|
||||
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
|
||||
<div class="absolute right-0 top-full mt-2 bg-white rounded shadow-lg py-1 hidden group-hover:block border border-gray-100 min-w-[140px]">
|
||||
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
|
||||
<i class="fa fa-sign-out mr-1"></i>退出登录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||||
<!-- 页面标题 -->
|
||||
<div class="bg-white rounded-lg shadow-sm w-full p-4 border-b border-gray-100 mb-4">
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="text-primary cursor-pointer hover:underline" onclick="window.location.href='main.html'">模型调优</span>
|
||||
<span class="mx-2 text-gray-300">/</span>
|
||||
<span class="text-gray-800 font-medium">创建训练任务</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<div class="bg-white rounded-lg shadow-sm w-full">
|
||||
<div class="p-6">
|
||||
<form id="createForm">
|
||||
<!-- 基本信息 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-600 mb-3">任务名称</label>
|
||||
<div>
|
||||
<input type="text" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入任务名称" maxlength="50">
|
||||
<p class="text-xs text-gray-400 mt-1"><span id="nameCount">0</span> / 50</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 训练配置 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
|
||||
|
||||
<!-- 训练方式 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 mb-3">训练方式</label>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="card-radio active" data-value="SFT">
|
||||
<div class="flex items-start">
|
||||
<input type="radio" name="train_type" value="SFT" checked class="mt-1 mr-2">
|
||||
<div>
|
||||
<div class="font-medium text-sm">SFT 微调训练</div>
|
||||
<div class="text-xs text-gray-400 mt-1">在监督指令下,增强模型指令跟随的能力,提升全参数微调训练方式</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-radio" data-value="DPO">
|
||||
<div class="flex items-start">
|
||||
<input type="radio" name="train_type" value="DPO" class="mt-1 mr-2">
|
||||
<div>
|
||||
<div class="font-medium text-sm">DPO 偏好训练</div>
|
||||
<div class="text-xs text-gray-400 mt-1">引入人类反馈,降低幻觉,使得模型输出更符合人类偏好</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-radio" data-value="CPT">
|
||||
<div class="flex items-start">
|
||||
<input type="radio" name="train_type" value="CPT" class="mt-1 mr-2">
|
||||
<div>
|
||||
<div class="font-medium text-sm">CPT 继续预训练</div>
|
||||
<div class="text-xs text-gray-400 mt-1">通过无标注数据进行无监督继续训练,强化或新增模型特定能力</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择模型 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 mb-3">选择模型</label>
|
||||
<div class="flex items-center">
|
||||
<select name="base_model" id="baseModelSelect" class="form-select flex-1 max-w-md">
|
||||
<option value="">请选择模型</option>
|
||||
</select>
|
||||
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadModels()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='model-manage-create.html?from=fine-tune'">
|
||||
+ 新增模型
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 训练方法 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 mb-3">训练方法</label>
|
||||
<div class="flex items-center space-x-6">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="train_method" value="lora" class="mr-2" onchange="toggleTrainMethod()" checked>
|
||||
<span class="text-sm">高效训练</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="radio" name="train_method" value="full" class="mr-2" onchange="toggleTrainMethod()">
|
||||
<span class="text-sm">全参训练</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 训练方法参数配置 -->
|
||||
<div id="trainMethodParams" class="mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<div class="flex items-center">
|
||||
<i class="fa fa-sliders text-primary mr-2"></i>
|
||||
<label class="block text-sm font-medium text-gray-700">训练参数配置</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="button" onclick="resetParams()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors" title="恢复默认配置">
|
||||
<i class="fa fa-rotate-left mr-1"></i>
|
||||
<span>重置</span>
|
||||
</button>
|
||||
<button type="button" onclick="toggleParamsCollapse()" class="text-gray-500 hover:text-primary text-sm flex items-center transition-colors">
|
||||
<span id="paramsToggleText">收起</span>
|
||||
<i id="paramsToggleIcon" class="fa fa-chevron-up ml-1 text-xs transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="paramsContent" class="overflow-x-auto">
|
||||
<table class="w-full text-sm divide-y divide-gray-100">
|
||||
<thead class="bg-gradient-to-r from-blue-50 to-indigo-50">
|
||||
<tr>
|
||||
<th class="text-left py-3 px-4 font-medium text-gray-600">参数名称</th>
|
||||
<th class="text-left py-3 px-4 font-medium text-gray-600">参数值</th>
|
||||
<th class="text-left py-3 px-4 font-medium text-gray-600">取值范围</th>
|
||||
<th class="text-left py-3 px-4 font-medium text-gray-600">参数说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="commonParamsBody" class="divide-y divide-gray-100">
|
||||
<!-- 公共参数 -->
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">batch_size</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="batch_size" value="1" min="1" max="64" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 64]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单步训练样本数量,数值越大训练速度越快,但显存占用越高</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">learning_rate</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="learning_rate" value="0.0001" step="0.00001" min="0.000001" max="1" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0.000001, 1]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">模型参数更新步长,过大可能导致训练不稳定,过小收敛速度慢</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">n_epochs</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="n_epochs" value="1" min="1" max="100" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[1, 100]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">完整遍历训练数据集的次数,建议设置在1-10之间</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">eval_steps</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="eval_steps" value="100" min="10" max="10000" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[10, 10000]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">每训练多少步进行一次模型评估,建议设置为100的倍数</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">lr_scheduler_type</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<select name="lr_scheduler_type" class="w-28 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
|
||||
<option value="cosine">cosine</option>
|
||||
<option value="linear">linear</option>
|
||||
<option value="constant">constant</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">3种可选</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率变化策略,cosine为余弦退火,linear为线性下降,constant为保持不变</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">max_length</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="max_length" value="512" min="64" max="4096" class="w-24 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[64, 4096]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">单条训练数据的最大token数,超出部分将被截断</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">warmup_ratio</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="warmup_ratio" value="0.05" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">学习率预热步数占总步数的比例,设置为0则不预热</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">weight_decay</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="weight_decay" value="0.01" step="0.001" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">防止过拟合的正则化技术,值越大对模型参数约束越强</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody id="loraParamsBody" class="hidden divide-y divide-gray-100">
|
||||
<!-- LoRA参数 -->
|
||||
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">lora_alpha</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<select name="lora_alpha" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
|
||||
<option value="16" selected>16</option>
|
||||
<option value="32">32</option>
|
||||
<option value="64">64</option>
|
||||
<option value="128">128</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA缩放系数,用于控制低秩适配矩阵的权重,影响模型对微调数据的敏感度</td>
|
||||
</tr>
|
||||
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">lora_dropout</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<input type="number" name="lora_dropout" value="0.1" step="0.01" min="0" max="1" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm text-center focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all">
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">[0, 1]</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA层 dropout 概率,在低秩适配矩阵中随机丢弃部分神经元以防止过拟合</td>
|
||||
</tr>
|
||||
<tr class="bg-blue-50/50 hover:bg-blue-50/70 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<span class="text-gray-700 font-mono text-sm">lora_rank</span>
|
||||
<span class="text-red-500 ml-1">*</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<select name="lora_rank" class="w-20 px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all bg-white">
|
||||
<option value="8" selected>8</option>
|
||||
<option value="16">16</option>
|
||||
<option value="32">32</option>
|
||||
<option value="64">64</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 text-gray-600 font-mono">4种可选</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-xs text-gray-500 leading-relaxed">LoRA低秩矩阵的秩,值越大表示低秩矩阵的维度越高,微调能力越强</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据配置 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">数据配置</h3>
|
||||
|
||||
<!-- 训练集 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 mb-1">
|
||||
<span class="text-red-500 mr-1">*</span>训练集
|
||||
</label>
|
||||
<div class="flex items-center">
|
||||
<select name="dataset_id" id="trainDatasetSelect" class="form-select flex-1 max-w-md">
|
||||
<option value="">请选择训练数据集</option>
|
||||
</select>
|
||||
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadDatasets()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='dataset-create.html?from=fine-tune'">
|
||||
+ 新增数据集
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 验证集 -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 mb-3">验证集 <span class="text-red-500">*</span></label>
|
||||
<div class="flex items-center space-x-6 mb-3">
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="valid_split" value="auto" checked class="mr-2" onchange="toggleValidSplit()">
|
||||
<span class="text-sm">自动切分</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="valid_split" value="custom" class="mr-2" onchange="toggleValidSplit()">
|
||||
<span class="text-sm">选择数据集</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="autoSplitSection" class="flex items-center">
|
||||
<span class="text-sm text-gray-600 mr-2">从当前训练集随机分割</span>
|
||||
<input type="number" name="valid_ratio" value="10" class="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center focus:border-primary focus:outline-none">
|
||||
<span class="text-sm text-gray-600 ml-2">% 作为验证集</span>
|
||||
</div>
|
||||
<div id="customSplitSection" class="hidden">
|
||||
<div class="flex items-center">
|
||||
<select name="valid_dataset_id" id="validDatasetSelect" class="form-select flex-1 max-w-md">
|
||||
<option value="">请选择验证数据集</option>
|
||||
</select>
|
||||
<button type="button" class="ml-2 text-primary text-sm flex items-center hover:text-primary/80" onclick="loadDatasets()">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<button type="button" class="ml-3 bg-white border border-primary text-primary rounded px-3 py-1.5 text-sm hover:bg-primary/5" onclick="window.location.href='dataset-create.html?from=fine-tune'">
|
||||
+ 新增数据集
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 训练产出 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练产出</h3>
|
||||
|
||||
<!-- 模型名称 -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-600 mb-3">模型名称</label>
|
||||
<div>
|
||||
<input type="text" name="output_model_name" class="w-64 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入模型名称" maxlength="50">
|
||||
<p class="text-xs text-gray-400 mt-1"><span id="modelNameCount">0</span> / 50</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型加密 -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<span class="text-sm text-gray-600 mr-2">模型加密</span>
|
||||
<span class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">安全升级</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">为保障您的数据安全,平台会为导出的模型文件开启 OSS 服务端加密</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button type="button" onclick="submitForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
|
||||
开始训练
|
||||
</button>
|
||||
<a href="main.html" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
|
||||
取消
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center text-sm">
|
||||
<a href="#" class="text-primary hover:underline">训练费用 (预估)</a>
|
||||
<span class="mx-2 text-gray-300">|</span>
|
||||
<a href="#" class="text-primary hover:underline">计算详情</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API 基础地址
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
return `${protocol}//${hostname}:8080/api`;
|
||||
};
|
||||
const API_BASE = getApiBase();
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 卡片式单选框
|
||||
document.querySelectorAll('.card-radio').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const parent = card.parentElement;
|
||||
parent.querySelectorAll('.card-radio').forEach(c => c.classList.remove('active'));
|
||||
card.classList.add('active');
|
||||
card.querySelector('input').checked = true;
|
||||
});
|
||||
});
|
||||
|
||||
// 任务名称字数统计
|
||||
const nameInput = document.querySelector('input[name="name"]');
|
||||
nameInput.addEventListener('input', () => {
|
||||
document.getElementById('nameCount').textContent = nameInput.value.length;
|
||||
});
|
||||
|
||||
// 模型名称字数统计
|
||||
const modelNameInput = document.querySelector('input[name="output_model_name"]');
|
||||
modelNameInput.addEventListener('input', () => {
|
||||
document.getElementById('modelNameCount').textContent = modelNameInput.value.length;
|
||||
});
|
||||
|
||||
// 加载数据集列表
|
||||
loadDatasets();
|
||||
|
||||
// 加载模型列表
|
||||
loadModels();
|
||||
|
||||
// 初始化训练方法参数显示
|
||||
toggleTrainMethod();
|
||||
|
||||
// 绑定导航点击事件
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (!this.href.includes('fine-tune-create')) {
|
||||
e.preventDefault();
|
||||
window.location.href = this.href;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 切换验证集切分方式
|
||||
function toggleValidSplit() {
|
||||
const validSplit = document.querySelector('input[name="valid_split"]:checked').value;
|
||||
const autoSection = document.getElementById('autoSplitSection');
|
||||
const customSection = document.getElementById('customSplitSection');
|
||||
if (validSplit === 'auto') {
|
||||
autoSection.classList.remove('hidden');
|
||||
customSection.classList.add('hidden');
|
||||
} else {
|
||||
autoSection.classList.add('hidden');
|
||||
customSection.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换训练方法 - 显示/隐藏LoRA参数
|
||||
function toggleTrainMethod() {
|
||||
const trainMethod = document.querySelector('input[name="train_method"]:checked').value;
|
||||
const loraParamsBody = document.getElementById('loraParamsBody');
|
||||
if (trainMethod === 'lora') {
|
||||
loraParamsBody.classList.remove('hidden');
|
||||
} else {
|
||||
loraParamsBody.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换参数配置展开/收起
|
||||
function toggleParamsCollapse() {
|
||||
const content = document.getElementById('paramsContent');
|
||||
const toggleText = document.getElementById('paramsToggleText');
|
||||
const toggleIcon = document.getElementById('paramsToggleIcon');
|
||||
|
||||
if (content.classList.contains('hidden')) {
|
||||
content.classList.remove('hidden');
|
||||
toggleText.textContent = '收起';
|
||||
toggleIcon.className = 'fa fa-chevron-up ml-1 text-xs';
|
||||
} else {
|
||||
content.classList.add('hidden');
|
||||
toggleText.textContent = '展开';
|
||||
toggleIcon.className = 'fa fa-chevron-down ml-1 text-xs';
|
||||
}
|
||||
}
|
||||
|
||||
// 重置参数为默认值
|
||||
function resetParams() {
|
||||
const defaults = {
|
||||
'batch_size': 1,
|
||||
'learning_rate': 0.0001,
|
||||
'n_epochs': 1,
|
||||
'eval_steps': 100,
|
||||
'lr_scheduler_type': 'cosine',
|
||||
'max_length': 512,
|
||||
'warmup_ratio': 0.05,
|
||||
'weight_decay': 0.01,
|
||||
'lora_alpha': '32',
|
||||
'lora_dropout': 0.1,
|
||||
'lora_rank': '8'
|
||||
};
|
||||
|
||||
for (const [name, value] of Object.entries(defaults)) {
|
||||
const input = document.querySelector(`input[name="${name}"]`);
|
||||
if (input) {
|
||||
if (input.type === 'number') {
|
||||
input.value = value;
|
||||
} else if (input.type === 'select-one') {
|
||||
input.value = value;
|
||||
}
|
||||
}
|
||||
const select = document.querySelector(`select[name="${name}"]`);
|
||||
if (select) select.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染数据集单选列表
|
||||
function renderDatasetRadio(datasets, containerId, name, selectedId) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!datasets || datasets.length === 0) {
|
||||
container.innerHTML = '<span class="text-sm text-gray-400">暂无可用数据集</span>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = datasets.map(d => `
|
||||
<label class="flex items-center space-x-2 cursor-pointer">
|
||||
<input type="radio" name="${name}" value="${d.id}" ${d.id === selectedId ? 'checked' : ''} class="text-primary focus:ring-primary">
|
||||
<span class="text-sm text-gray-700">${d.name}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 加载数据集列表
|
||||
async function loadDatasets() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/dataset-manage`);
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
// 更新训练集下拉框
|
||||
const trainSelect = document.getElementById('trainDatasetSelect');
|
||||
if (trainSelect) {
|
||||
trainSelect.innerHTML = '<option value="">请选择训练数据集</option>' +
|
||||
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
|
||||
}
|
||||
// 更新验证集下拉框
|
||||
const validSelect = document.getElementById('validDatasetSelect');
|
||||
if (validSelect) {
|
||||
validSelect.innerHTML = '<option value="">请选择验证数据集</option>' +
|
||||
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载数据集失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载模型列表
|
||||
async function loadModels() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/model-manage`);
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
const modelSelect = document.getElementById('baseModelSelect');
|
||||
if (modelSelect) {
|
||||
modelSelect.innerHTML = '<option value="">请选择模型</option>' +
|
||||
result.data.map(m => `<option value="${m.id}">${m.name}</option>`).join('');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载模型失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function submitForm() {
|
||||
const form = document.getElementById('createForm');
|
||||
const formData = new FormData(form);
|
||||
const validSplit = formData.get('valid_split');
|
||||
|
||||
const data = {
|
||||
name: formData.get('name'),
|
||||
base_model: formData.get('base_model'),
|
||||
train_type: formData.get('train_type'),
|
||||
train_method: formData.get('train_method'),
|
||||
train_dataset_id: formData.get('train_dataset_id'),
|
||||
valid_split: validSplit,
|
||||
valid_ratio: parseInt(formData.get('valid_ratio')) || 10,
|
||||
valid_dataset_id: validSplit === 'custom' ? formData.get('valid_dataset_id') : null,
|
||||
output_model_name: formData.get('output_model_name'),
|
||||
status: 'pending',
|
||||
progress: 0
|
||||
};
|
||||
|
||||
if (!data.name) {
|
||||
alert('请输入任务名称');
|
||||
return;
|
||||
}
|
||||
if (!data.base_model) {
|
||||
alert('请选择基础模型');
|
||||
return;
|
||||
}
|
||||
if (!data.train_dataset_id) {
|
||||
alert('请选择训练集');
|
||||
return;
|
||||
}
|
||||
if (validSplit === 'custom' && !data.valid_dataset_id) {
|
||||
alert('请选择验证集');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/fine-tune`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
alert('创建成功!');
|
||||
window.location.href = 'main.html';
|
||||
} else {
|
||||
alert(result.message || '创建失败');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('创建失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user