Compare commits
6 Commits
main
...
v1_interfa
| Author | SHA1 | Date | |
|---|---|---|---|
| 373f5244e5 | |||
| cde34e4919 | |||
| 1ae1139f17 | |||
| 7250d5840a | |||
| ea8d5a28dd | |||
| b2f19d9583 |
@@ -33,47 +33,7 @@
|
|||||||
"Bash(./test_upload.sh:*)",
|
"Bash(./test_upload.sh:*)",
|
||||||
"Bash(./test_all.sh)",
|
"Bash(./test_all.sh)",
|
||||||
"Bash(/data/code/FT_Platform/YG_FT_Platform/test_data_dir.sh:*)",
|
"Bash(/data/code/FT_Platform/YG_FT_Platform/test_data_dir.sh:*)",
|
||||||
"Bash(ls:*)",
|
"Bash(grep:*)"
|
||||||
"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,17 +174,3 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.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/
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
@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
Normal file
17
config.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 数据库配置
|
||||||
|
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
3
data/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
# 忽略data目录下的所有文件
|
|
||||||
*
|
|
||||||
!.gitignore
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
@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
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/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 +0,0 @@
|
|||||||
2916
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/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
300
request/setup.sh
@@ -1,300 +0,0 @@
|
|||||||
#!/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 ""
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
"""
|
|
||||||
X-Request 高性能FastAPI框架
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
|
||||||
__author__ = "X-Request Team"
|
|
||||||
__description__ = "高性能、高并发的请求框架,具有全面的日志系统"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"""
|
|
||||||
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"]
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
"""
|
|
||||||
框架内部模块
|
|
||||||
|
|
||||||
此目录包含框架的核心组件,不建议用户修改:
|
|
||||||
- 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']
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
"""
|
|
||||||
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()
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"""
|
|
||||||
日志管理API模块
|
|
||||||
|
|
||||||
提供日志文件的管理、搜索、下载等功能
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .routes import router
|
|
||||||
|
|
||||||
__all__ = ["router"]
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
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")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from fastapi import APIRouter
|
|
||||||
from .management import router as management_router
|
|
||||||
|
|
||||||
# 创建主路由器
|
|
||||||
router = APIRouter(prefix="/logs", tags=["日志管理"])
|
|
||||||
|
|
||||||
# 包含所有子路由
|
|
||||||
router.include_router(management_router)
|
|
||||||
|
|
||||||
# 导出路由器
|
|
||||||
__all__ = ["router"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
"""
|
|
||||||
业务模块目录
|
|
||||||
|
|
||||||
此目录用于存放用户自定义的业务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()
|
|
||||||
"""
|
|
||||||
@@ -1,381 +0,0 @@
|
|||||||
"""
|
|
||||||
数据集管理 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()
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
"""
|
|
||||||
模型配置管理模块
|
|
||||||
支持模型的增删改查操作
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .settings import settings
|
|
||||||
|
|
||||||
__all__ = ["settings"]
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .app import create_app, get_app
|
|
||||||
|
|
||||||
__all__ = ["create_app", "get_app"]
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from .logging import LoggingMiddleware
|
|
||||||
from .error_handling import ErrorHandlingMiddleware
|
|
||||||
from .performance import PerformanceMiddleware
|
|
||||||
|
|
||||||
__all__ = ["LoggingMiddleware", "ErrorHandlingMiddleware", "PerformanceMiddleware"]
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
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")
|
|
||||||
)
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""
|
|
||||||
统一数据模型模块
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 基础模型
|
|
||||||
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",
|
|
||||||
]
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""
|
|
||||||
基础数据模型
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"""
|
|
||||||
业务模块数据模型
|
|
||||||
"""
|
|
||||||
|
|
||||||
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",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
场景注册 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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""
|
|
||||||
用户相关数据模型
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
"""
|
|
||||||
响应相关模型模块
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .standard import (
|
|
||||||
StandardResponse,
|
|
||||||
RequestInfo,
|
|
||||||
ResponseInfo
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"StandardResponse",
|
|
||||||
"RequestInfo",
|
|
||||||
"ResponseInfo",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
# 业务服务模块
|
|
||||||
@@ -1,546 +0,0 @@
|
|||||||
"""
|
|
||||||
文件上传服务
|
|
||||||
提供文件上传、存储、管理等功能
|
|
||||||
"""
|
|
||||||
|
|
||||||
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()
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
"""
|
|
||||||
微调任务管理服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
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, "微调任务已完成")
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# 统一日志系统 - 推荐使用
|
|
||||||
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",
|
|
||||||
]
|
|
||||||
@@ -1,435 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,321 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
"""
|
|
||||||
高级日志系统使用示例
|
|
||||||
|
|
||||||
演示如何在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} - 下载指定日志文件")
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
"""
|
|
||||||
统一日志管理器 - 整合基础日志和高级日志系统
|
|
||||||
提供简单易用的统一接口,同时保持向后兼容
|
|
||||||
"""
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
@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
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/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 +0,0 @@
|
|||||||
58852b1a007ac2ceb1f790a423c070f2
|
|
||||||
File diff suppressed because it is too large
Load Diff
4
request/static/font-awesome.min.css
vendored
4
request/static/font-awesome.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,697 +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>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>
|
|
||||||
@@ -1,98 +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>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>
|
|
||||||
@@ -1,474 +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>系统状态 - 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>
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
140
request/static/vendor/icons.js
vendored
140
request/static/vendor/icons.js
vendored
@@ -1,140 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
3
request/static/vendor/swagger-ui-bundle.js
vendored
File diff suppressed because one or more lines are too long
3
request/static/vendor/swagger-ui.css
vendored
3
request/static/vendor/swagger-ui.css
vendored
File diff suppressed because one or more lines are too long
65
request/static/vendor/tailwind.min.js
vendored
65
request/static/vendor/tailwind.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,83 +0,0 @@
|
|||||||
#!/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)"
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
<#
|
|
||||||
.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"
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
@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=
|
|
||||||
)
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
@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
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
|||||||
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
|
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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
Normal file
506
src/main.py
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
"""
|
||||||
|
远光软件微调平台 - 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
Normal file
14
src/run.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/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
|
||||||
@@ -1,396 +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>
|
|
||||||
<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>
|
|
||||||
95
web/README.md
Normal file
95
web/README.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# 🔥 大模型微调平台
|
||||||
|
|
||||||
|
> **静态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是否安装
|
||||||
@@ -1,57 +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>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
2683
web/main.html
File diff suppressed because it is too large
Load Diff
455
web/pages/custom-tool-create.html
Normal file
455
web/pages/custom-tool-create.html
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
<!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>
|
||||||
440
web/pages/dataset-create.html
Normal file
440
web/pages/dataset-create.html
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
<!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>
|
||||||
787
web/pages/fine-tune-create.html
Normal file
787
web/pages/fine-tune-create.html
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
<!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