修改了数据集上传,修改了模型配置页面
This commit is contained in:
@@ -60,7 +60,17 @@
|
||||
"Bash(grep:*)",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(./xrequest/Scripts/python.exe:*)",
|
||||
"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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
25
clear_cache.bat
Normal file
25
clear_cache.bat
Normal file
@@ -0,0 +1,25 @@
|
||||
@echo off
|
||||
echo 正在清除所有缓存文件...
|
||||
echo.
|
||||
|
||||
REM 切换到项目根目录
|
||||
cd /d "%~dp0"
|
||||
cd request
|
||||
|
||||
echo 1. 清除Python缓存文件...
|
||||
REM 删除所有__pycache__目录
|
||||
for /d /r . %%d in (__pycache__) do @if exist "%%d" rd /s /q "%%d" 2>nul
|
||||
REM 删除所有.pyc文件
|
||||
for /r . %%f in (*.pyc) do @if exist "%%f" del /q "%%f" 2>nul
|
||||
echo Python缓存清除完成
|
||||
|
||||
echo.
|
||||
echo 2. 清除浏览器缓存(请手动清除或按Ctrl+Shift+Delete)
|
||||
echo 请在浏览器中清除"缓存的图片和文件"
|
||||
echo.
|
||||
|
||||
echo 3. 重启服务提示
|
||||
echo 请按任意键打开启动命令窗口,然后运行启动命令
|
||||
echo 例如: python -m uvicorn src.core.app:app --host 0.0.0.0 --port 8000 --reload
|
||||
echo.
|
||||
pause
|
||||
@@ -1,104 +1,4 @@
|
||||
[
|
||||
{
|
||||
"id": "model-1",
|
||||
"name": "GPT-4 Turbo",
|
||||
"type": "gpt",
|
||||
"provider": "openai",
|
||||
"version": "gpt-4-turbo",
|
||||
"apiUrl": "https://api.openai.com/v1",
|
||||
"apiKey": "sk-test123456789",
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.7,
|
||||
"topP": 1.0,
|
||||
"topK": 50,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": "连接失败"
|
||||
},
|
||||
{
|
||||
"id": "model-2",
|
||||
"name": "Claude-3 Sonnet",
|
||||
"type": "claude",
|
||||
"provider": "anthropic",
|
||||
"version": "claude-3-sonnet-20240229",
|
||||
"apiUrl": "https://api.anthropic.com/v1",
|
||||
"apiKey": "",
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.5,
|
||||
"topP": 0.9,
|
||||
"topK": 40,
|
||||
"maxTokens": 4096,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": true,
|
||||
"logRequests": true,
|
||||
"status": "未测试"
|
||||
},
|
||||
{
|
||||
"id": "model-3",
|
||||
"name": "LLaMA-2 7B",
|
||||
"type": "llama",
|
||||
"provider": "local",
|
||||
"version": "llama-2-7b-chat",
|
||||
"apiUrl": "http://localhost:8080/v1",
|
||||
"apiKey": "",
|
||||
"timeout": 60,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.8,
|
||||
"topP": 0.95,
|
||||
"topK": 60,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": false,
|
||||
"status": "已测试"
|
||||
},
|
||||
{
|
||||
"id": "0c13f76a-6a40-48d2-86d3-2638fd2be652",
|
||||
"name": "Test Model",
|
||||
"type": "gpt",
|
||||
"provider": "openai",
|
||||
"version": "gpt-3.5-turbo",
|
||||
"apiUrl": "",
|
||||
"apiKey": "",
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.7,
|
||||
"topP": 1.0,
|
||||
"topK": 50,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": "未测试"
|
||||
},
|
||||
{
|
||||
"id": "9674adec-124c-4641-898b-7f7557e9e412",
|
||||
"name": "nova",
|
||||
"type": "gpt",
|
||||
"provider": "openai",
|
||||
"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": "已测试"
|
||||
},
|
||||
{
|
||||
"id": "e81c21e1-a4ce-4237-ba22-0922b741b9be",
|
||||
"name": "qwen3-flash",
|
||||
@@ -117,6 +17,26 @@
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": "已测试"
|
||||
"status": 1
|
||||
},
|
||||
{
|
||||
"id": "3e61f8de-831b-4439-9f0a-8b8ad753c5ca",
|
||||
"name": "Nova",
|
||||
"type": "custom",
|
||||
"provider": "custom",
|
||||
"version": "nova",
|
||||
"apiUrl": "http://10.10.10.122:1234/v1",
|
||||
"apiKey": "123",
|
||||
"timeout": 30,
|
||||
"maxRetries": 3,
|
||||
"temperature": 0.7,
|
||||
"topP": 1,
|
||||
"topK": 50,
|
||||
"maxTokens": 2048,
|
||||
"systemPrompt": "你是一个有用的AI助手。",
|
||||
"streaming": true,
|
||||
"functions": false,
|
||||
"logRequests": true,
|
||||
"status": 1
|
||||
}
|
||||
]
|
||||
39
request/_backend.bat
Normal file
39
request/_backend.bat
Normal file
@@ -0,0 +1,39 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
:: 读取配置
|
||||
cd /d "%~dp0"
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.host; } catch { Write-Host 'ERROR'; exit 1; }" > temp_host.txt
|
||||
set /p backend_host=<temp_host.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.port; } catch { Write-Host 'ERROR'; exit 1; }" > temp_port.txt
|
||||
set /p backend_port=<temp_port.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.python_path; } catch { Write-Host 'ERROR'; exit 1; }" > temp_python.txt
|
||||
set /p python_path=<temp_python.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.main_module; } catch { Write-Host 'ERROR'; exit 1; }" > temp_module.txt
|
||||
set /p main_module=<temp_module.txt
|
||||
|
||||
powershell -Command "try { $config = Get-Content ../config.json | ConvertFrom-Json; Write-Host $config.backend.log_level; } catch { Write-Host 'INFO'; exit 1; }" > temp_loglevel.txt
|
||||
set /p log_level=<temp_loglevel.txt
|
||||
|
||||
del temp_host.txt temp_port.txt temp_python.txt temp_module.txt temp_loglevel.txt 2>nul
|
||||
|
||||
echo [后端] 启动配置:
|
||||
echo [后端] 主机: !backend_host!
|
||||
echo [后端] 端口: !backend_port!
|
||||
echo [后端] 日志级别: !log_level!
|
||||
echo [后端] 主模块: !main_module!
|
||||
echo.
|
||||
|
||||
:: 设置环境变量
|
||||
set PYTHONPATH=src
|
||||
set LOGLEVEL=!log_level!
|
||||
|
||||
:: 启动后端服务
|
||||
echo [后端] 正在启动服务...
|
||||
!python_path! -m uvicorn !main_module! --host !backend_host! --port !backend_port!
|
||||
|
||||
pause
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 永远从脚本所在目录运行(避免在别的目录执行导致路径错误)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "🧹 X-Request 框架环境清理"
|
||||
echo "=========================="
|
||||
|
||||
# 函数:加载 .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
|
||||
}
|
||||
|
||||
# 加载环境配置
|
||||
load_env_file
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d "xrequest" ]; then
|
||||
echo "⚠️ 虚拟环境不存在,无需清理"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "📋 检测到虚拟环境: xrequest"
|
||||
|
||||
# 询问用户确认
|
||||
read -p "确定要删除虚拟环境吗?(y/N): " confirm
|
||||
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||
echo "❌ 操作已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 删除虚拟环境
|
||||
echo "🗑️ 正在删除虚拟环境..."
|
||||
rm -rf xrequest
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ 虚拟环境已删除"
|
||||
else
|
||||
echo "❌ 虚拟环境删除失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 询问是否清理日志
|
||||
if [ -d "${LOGS_DIR:-logs}" ]; then
|
||||
echo ""
|
||||
read -p "是否也要清理日志目录?(y/N): " clean_logs
|
||||
if [[ "$clean_logs" =~ ^[Yy]$ ]]; then
|
||||
echo "🗑️ 正在清理日志目录..."
|
||||
rm -rf "${LOGS_DIR:-logs}"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ 日志目录已清理"
|
||||
else
|
||||
echo "⚠️ 日志目录清理失败"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 询问是否清理 __pycache__ 和 .pyc 文件
|
||||
echo ""
|
||||
read -p "是否清理 Python 缓存文件 (__pycache__, *.pyc)?(y/N): " clean_cache
|
||||
if [[ "$clean_cache" =~ ^[Yy]$ ]]; then
|
||||
echo "🗑️ 正在清理 Python 缓存..."
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
echo "✅ Python 缓存已清理"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎉 清理完成!"
|
||||
echo ""
|
||||
echo "📝 如需重新设置环境,请运行:"
|
||||
echo " ./setup.sh"
|
||||
echo ""
|
||||
@@ -1 +1 @@
|
||||
3807
|
||||
2916
|
||||
|
||||
@@ -111,168 +111,64 @@ class DatasetAPI(BaseAPI):
|
||||
return StandardResponse.error(f"上传失败: {str(e)}")
|
||||
|
||||
@get("", response_model=StandardResponse)
|
||||
async def list_datasets(self, list_all: bool = False):
|
||||
async def list_datasets(self):
|
||||
"""
|
||||
获取所有数据集列表
|
||||
|
||||
Args:
|
||||
list_all: 是否列出data目录下的所有文件(物理文件),默认False(只列出API上传的文件)
|
||||
获取数据集列表(只返回filename_mapping.json中记录的文件)
|
||||
|
||||
Returns:
|
||||
StandardResponse: 包含数据集列表的标准响应
|
||||
"""
|
||||
# 添加调试日志
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"list_datasets called with list_all={list_all}")
|
||||
|
||||
try:
|
||||
if list_all:
|
||||
# 列出data目录下的所有文件(物理文件)
|
||||
import json
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
data_dir = file_upload_service.upload_dir
|
||||
mapping_file = data_dir / "filename_mapping.json"
|
||||
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 = {}
|
||||
# 读取文件名映射
|
||||
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", {})
|
||||
|
||||
# 获取data目录下的所有JSON文件
|
||||
datasets = []
|
||||
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
|
||||
# 根据映射文件构建数据集列表
|
||||
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
|
||||
|
||||
# 从映射文件获取真实文件名
|
||||
mapping_info = mappings.get(file_id, {})
|
||||
original_filename = mapping_info.get("original_filename", file_path.name)
|
||||
uploaded_at = mapping_info.get("uploaded_at", "")
|
||||
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)
|
||||
|
||||
# 获取文件大小
|
||||
file_size = file_path.stat().st_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
|
||||
})
|
||||
|
||||
# 格式化文件大小
|
||||
size_mb = round(file_size / 1024 / 1024, 2)
|
||||
size_display = format_file_size(file_size)
|
||||
# 按上传时间排序
|
||||
datasets.sort(key=lambda x: x["uploaded_at"], reverse=True)
|
||||
|
||||
datasets.append({
|
||||
"file_id": file_id,
|
||||
"name": original_filename,
|
||||
"size": file_size,
|
||||
"size_mb": size_mb,
|
||||
"size_display": size_display,
|
||||
"status": "已处理",
|
||||
"description": mapping_info.get("original_filename", "") if mapping_info else "",
|
||||
"uploaded_at": uploaded_at,
|
||||
"download_count": 0,
|
||||
"is_physical_file": True
|
||||
})
|
||||
|
||||
# 按文件名排序
|
||||
datasets.sort(key=lambda x: x["name"])
|
||||
|
||||
return StandardResponse.success({
|
||||
"datasets": datasets,
|
||||
"total": len(datasets),
|
||||
"source": "physical_files"
|
||||
})
|
||||
else:
|
||||
# 获取所有文件(API上传的文件)
|
||||
all_files = file_upload_service.get_all_files()
|
||||
|
||||
# 转换为前端期望的格式
|
||||
datasets = []
|
||||
for uploaded_file in all_files:
|
||||
# 只返回JSON/JSONL文件(数据集文件)
|
||||
file_ext = uploaded_file.original_filename.lower().split('.')[-1] if '.' in uploaded_file.original_filename else ''
|
||||
if file_ext in ['json', 'jsonl']:
|
||||
# 获取文件名映射(显示真实文件名)
|
||||
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)
|
||||
|
||||
datasets.append({
|
||||
"file_id": uploaded_file.file_id,
|
||||
"name": display_name,
|
||||
"size": uploaded_file.file_size,
|
||||
"size_mb": size_mb,
|
||||
"size_display": size_display,
|
||||
"status": "已处理",
|
||||
"description": uploaded_file.description or "",
|
||||
"uploaded_at": uploaded_file.uploaded_at,
|
||||
"download_count": uploaded_file.download_count,
|
||||
"is_physical_file": False
|
||||
})
|
||||
|
||||
return StandardResponse.success({
|
||||
"datasets": datasets,
|
||||
"total": len(datasets),
|
||||
"source": "api_uploaded"
|
||||
})
|
||||
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}", response_model=StandardResponse)
|
||||
async def get_dataset(self, file_id: str):
|
||||
"""
|
||||
|
||||
@@ -7,10 +7,8 @@ import os
|
||||
import json
|
||||
import uuid
|
||||
import httpx
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import HTTPException, Body, Response
|
||||
from fastapi.responses import StreamingResponse
|
||||
import logging
|
||||
|
||||
# 导入基类
|
||||
@@ -27,81 +25,6 @@ MODELS_CONFIG_PATH = os.path.join(BASE_DIR, "..", "..", "models", "models.json")
|
||||
MODELS_CONFIG_PATH = os.path.normpath(MODELS_CONFIG_PATH)
|
||||
|
||||
|
||||
async def handle_streaming_response(response: httpx.Response, model: Dict[str, Any], attempt: int, max_attempts: int) -> Dict[str, Any]:
|
||||
"""处理流式响应"""
|
||||
try:
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API调用失败 (状态码: {response.status_code})"
|
||||
try:
|
||||
error_detail = response.json()
|
||||
error_msg += f": {error_detail}"
|
||||
except:
|
||||
error_msg += f": {response.text}"
|
||||
return {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'status_code': response.status_code
|
||||
}
|
||||
|
||||
full_content = ""
|
||||
chunk_count = 0
|
||||
usage = {}
|
||||
|
||||
# 处理流式数据
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith('data: '):
|
||||
data_str = line[6:] # 移除 'data: ' 前缀
|
||||
|
||||
if data_str.strip() == '[DONE]':
|
||||
break
|
||||
|
||||
try:
|
||||
chunk = json.loads(data_str)
|
||||
chunk_count += 1
|
||||
|
||||
# 提取内容
|
||||
if 'choices' in chunk and len(chunk['choices']) > 0:
|
||||
delta = chunk['choices'][0].get('delta', {})
|
||||
if 'content' in delta:
|
||||
content = delta['content']
|
||||
full_content += content
|
||||
|
||||
# 提取使用统计
|
||||
if 'usage' in chunk:
|
||||
usage = chunk['usage']
|
||||
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
logger.info(f"流式响应完成 (尝试 {attempt}/{max_attempts}) - 接收 {chunk_count} 个数据块")
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'model': model['name'],
|
||||
'content': full_content,
|
||||
'usage': usage,
|
||||
'streaming': True,
|
||||
'chunks_received': chunk_count,
|
||||
'raw_response': {
|
||||
'object': 'chat.completion',
|
||||
'model': model.get('version', 'unknown'),
|
||||
'choices': [{
|
||||
'message': {'role': 'assistant', 'content': full_content},
|
||||
'finish_reason': 'stop'
|
||||
}],
|
||||
'usage': usage
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"流式响应处理错误: {str(e)}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'流式响应处理失败: {str(e)}',
|
||||
'error_type': type(e).__name__
|
||||
}
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""模型配置管理器"""
|
||||
|
||||
@@ -227,245 +150,124 @@ class ModelManager:
|
||||
|
||||
@staticmethod
|
||||
async def call_model(model_id: str, prompt: str, system_prompt: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""真实调用模型API"""
|
||||
"""真实调用模型API - 简化版本"""
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if not model:
|
||||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
return {'success': False, 'error': '模型不存在'}
|
||||
|
||||
# 检查API配置
|
||||
api_url = model.get('apiUrl', '').strip()
|
||||
api_key = model.get('apiKey', '').strip()
|
||||
version = model.get('version', '').strip()
|
||||
provider = model.get('provider', '').strip()
|
||||
|
||||
# 调试日志
|
||||
logger.info(f"Model {model.get('name')} config", extra={
|
||||
'provider': provider,
|
||||
'api_url': api_url,
|
||||
'version': version,
|
||||
'has_api_key': bool(api_key),
|
||||
'api_key_length': len(api_key) if api_key else 0
|
||||
})
|
||||
# 记录调试信息
|
||||
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:
|
||||
raise HTTPException(status_code=400, detail="模型API地址未配置")
|
||||
return {'success': False, 'error': 'API地址未配置'}
|
||||
|
||||
if not version:
|
||||
raise HTTPException(status_code=400, detail="模型版本未配置")
|
||||
return {'success': False, 'error': '模型版本未配置'}
|
||||
|
||||
# 准备请求数据
|
||||
request_data = {
|
||||
"model": version,
|
||||
"messages": [],
|
||||
"temperature": model.get('temperature', 0.7),
|
||||
"top_p": model.get('topP', 1.0),
|
||||
"max_tokens": model.get('maxTokens', 2048),
|
||||
"stream": model.get('streaming', False)
|
||||
}
|
||||
# 准备请求数据 - 标准的OpenAI格式
|
||||
messages = []
|
||||
|
||||
# 添加系统提示词
|
||||
if system_prompt:
|
||||
request_data["messages"].append({
|
||||
"role": "system",
|
||||
"content": system_prompt
|
||||
})
|
||||
messages.append({"role": "system", "content": system_prompt})
|
||||
elif model.get('systemPrompt'):
|
||||
request_data["messages"].append({
|
||||
"role": "system",
|
||||
"content": model.get('systemPrompt')
|
||||
})
|
||||
system_prompt_content = model.get('systemPrompt')
|
||||
if system_prompt_content:
|
||||
messages.append({"role": "system", "content": system_prompt_content})
|
||||
|
||||
# 添加用户提示词
|
||||
request_data["messages"].append({
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
})
|
||||
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密钥(如果提供)
|
||||
# 只有在有API密钥时才添加认证头
|
||||
if api_key:
|
||||
provider_lower = provider.lower()
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# 根据提供商类型设置认证头
|
||||
if provider_lower == 'anthropic':
|
||||
# Anthropic API
|
||||
headers["x-api-key"] = api_key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
logger.info(f"设置Anthropic认证头,x-api-key长度: {len(api_key)}")
|
||||
else:
|
||||
# OpenAI 和其他兼容的API(包括自定义)
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
logger.info(f"设置Bearer认证头,key长度: {len(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}")
|
||||
|
||||
# 记录最终使用的请求头(隐藏API key)
|
||||
logger.info(f"最终请求头", extra={
|
||||
'headers': {k: v if k != 'Authorization' and k != 'x-api-key' else '***HIDDEN***' for k, v in headers.items()},
|
||||
'api_key_masked': f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else '***'
|
||||
})
|
||||
else:
|
||||
logger.warning(f"未提供API key for model {model.get('name')}")
|
||||
# 记录最终使用的请求头
|
||||
logger.info(f"最终请求头", extra={
|
||||
'headers': headers,
|
||||
'api_key': 'NONE'
|
||||
})
|
||||
response = await client.post(
|
||||
f"{api_url.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json=request_data
|
||||
)
|
||||
|
||||
# 获取重试配置
|
||||
max_retries = model.get('maxRetries', 3)
|
||||
retry_delay = 2 # 重试间隔2秒
|
||||
# 检查响应
|
||||
logger.info(f"响应状态码: {response.status_code}")
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info(f"响应内容: {result}")
|
||||
|
||||
last_error = None
|
||||
# 解析响应
|
||||
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)}")
|
||||
|
||||
# 重试循环
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
# 发送API请求
|
||||
timeout = httpx.Timeout(model.get('timeout', 30))
|
||||
provider = model.get('provider', '').lower()
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
# 根据提供商类型调用不同的API端点
|
||||
if provider_lower == 'anthropic':
|
||||
# Anthropic API格式
|
||||
anthropic_request = {
|
||||
"model": version,
|
||||
"max_tokens": request_data["max_tokens"],
|
||||
"messages": request_data["messages"][1:] # 移除system message,Anthropic使用system参数
|
||||
}
|
||||
if system_prompt or model.get('systemPrompt'):
|
||||
anthropic_request["system"] = system_prompt or model.get('systemPrompt')
|
||||
|
||||
logger.info(f"发送Anthropic API请求 (尝试 {attempt + 1}/{max_retries + 1})", extra={
|
||||
'url': f"{api_url.rstrip('/')}/messages",
|
||||
'headers': {k: v if k != 'x-api-key' else '***HIDDEN***' for k, v in headers.items()},
|
||||
'request_body': anthropic_request
|
||||
})
|
||||
|
||||
response = await client.post(
|
||||
f"{api_url.rstrip('/')}/messages",
|
||||
headers=headers,
|
||||
json=anthropic_request
|
||||
)
|
||||
else:
|
||||
# OpenAI 和其他兼容的API(包括自定义、本地部署等)
|
||||
|
||||
logger.info(f"发送OpenAI兼容API请求 (尝试 {attempt + 1}/{max_retries + 1})", extra={
|
||||
'url': f"{api_url.rstrip('/')}/chat/completions",
|
||||
'headers': {k: v if k != 'Authorization' else '***HIDDEN***' for k, v in headers.items()},
|
||||
'request_body': request_data
|
||||
})
|
||||
|
||||
response = await client.post(
|
||||
f"{api_url.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json=request_data
|
||||
)
|
||||
|
||||
# 处理流式响应
|
||||
if request_data.get('stream', False):
|
||||
return await handle_streaming_response(response, model, attempt + 1, max_retries + 1)
|
||||
|
||||
# 检查响应状态(非流式)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
|
||||
# 解析响应 - 根据提供商类型
|
||||
provider = model.get('provider', '').lower()
|
||||
|
||||
if provider == 'anthropic' and "content" in result and len(result["content"]) > 0:
|
||||
# Anthropic格式响应
|
||||
content = result["content"][0]["text"]
|
||||
logger.info(f"API调用成功 (尝试 {attempt + 1})")
|
||||
return {
|
||||
'success': True,
|
||||
'model': model['name'],
|
||||
'content': content,
|
||||
'usage': result.get('usage', {}),
|
||||
'raw_response': result
|
||||
}
|
||||
elif "choices" in result and len(result["choices"]) > 0:
|
||||
# OpenAI和其他兼容API格式响应
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
logger.info(f"API调用成功 (尝试 {attempt + 1})")
|
||||
return {
|
||||
'success': True,
|
||||
'model': model['name'],
|
||||
'content': content,
|
||||
'usage': result.get('usage', {}),
|
||||
'raw_response': result
|
||||
}
|
||||
# 如果内容为空,记录警告但仍然返回成功
|
||||
if not content:
|
||||
logger.warning("API返回的内容为空")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': '无法解析API响应',
|
||||
'success': True,
|
||||
'model': str(model.get('name', 'unknown')),
|
||||
'content': content,
|
||||
'usage': result.get('usage', {}),
|
||||
'raw_response': result
|
||||
}
|
||||
else:
|
||||
error_msg = f"API调用失败 (状态码: {response.status_code})"
|
||||
try:
|
||||
error_detail = response.json()
|
||||
error_msg += f": {error_detail}"
|
||||
except:
|
||||
error_msg += f": {response.text}"
|
||||
|
||||
last_error = {
|
||||
'success': False,
|
||||
'error': error_msg,
|
||||
'status_code': response.status_code
|
||||
}
|
||||
|
||||
# 如果不是最后一次尝试,等待后重试
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"API调用失败,{retry_delay}秒后重试 (尝试 {attempt + 1}/{max_retries + 1}): {error_msg}")
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
else:
|
||||
# 最后一次尝试失败,返回错误
|
||||
return last_error
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
last_error = {
|
||||
'success': False,
|
||||
'error': 'API调用超时',
|
||||
'timeout': model.get('timeout', 30)
|
||||
}
|
||||
logger.warning(f"API调用超时 (尝试 {attempt + 1}/{max_retries + 1})")
|
||||
# 如果不是最后一次尝试,等待后重试
|
||||
if attempt < max_retries:
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
logger.error(f"API响应格式异常: {result}")
|
||||
return {'success': False, 'error': 'API响应格式异常'}
|
||||
else:
|
||||
return last_error
|
||||
# 获取错误信息
|
||||
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}")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
last_error = {
|
||||
'success': False,
|
||||
'error': f'网络请求错误: {str(e)}'
|
||||
}
|
||||
logger.warning(f"网络请求错误 (尝试 {attempt + 1}/{max_retries + 1}): {str(e)}")
|
||||
# 如果不是最后一次尝试,等待后重试
|
||||
if attempt < max_retries:
|
||||
await asyncio.sleep(retry_delay)
|
||||
continue
|
||||
else:
|
||||
return last_error
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'API调用失败 ({response.status_code}): {error_detail_str}'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
last_error = {
|
||||
'success': False,
|
||||
'error': f'调用失败: {str(e)}',
|
||||
'error_type': type(e).__name__
|
||||
}
|
||||
logger.error(f"未知错误 (尝试 {attempt + 1}/{max_retries + 1}): {str(e)}")
|
||||
# 其他错误不重试,直接返回
|
||||
return last_error
|
||||
|
||||
# 如果所有重试都失败,返回最后一次错误
|
||||
return last_error
|
||||
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]:
|
||||
@@ -530,7 +332,8 @@ class ModelManager:
|
||||
message = '连接测试成功'
|
||||
else:
|
||||
model['status'] = '连接失败'
|
||||
message = f'连接测试失败: {test_result.get("error", "未知错误")}'
|
||||
error = test_result.get("error")
|
||||
message = f'连接测试失败: {str(error) if error else "未知错误"}'
|
||||
|
||||
# 保存更新
|
||||
ModelManager.update_model(model_id, {'status': model['status']})
|
||||
@@ -656,7 +459,8 @@ class ModelsAPI(BaseAPI):
|
||||
message = '连接测试成功'
|
||||
else:
|
||||
model['status'] = '连接失败'
|
||||
message = f'连接测试失败: {result.get("error", "未知错误")}'
|
||||
error = result.get("error")
|
||||
message = f'连接测试失败: {str(error) if error else "未知错误"}'
|
||||
|
||||
# 保存更新
|
||||
ModelManager.update_model(model_id, {'status': model['status']})
|
||||
@@ -678,157 +482,28 @@ class ModelsAPI(BaseAPI):
|
||||
|
||||
@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)
|
||||
stream_enabled = request_data.get('stream', False)
|
||||
|
||||
if not prompt:
|
||||
raise HTTPException(status_code=400, detail="提示词不能为空")
|
||||
|
||||
# 如果启用流式,返回流式响应
|
||||
if stream_enabled:
|
||||
return StreamingResponse(
|
||||
self._stream_data(model_id, prompt, system_prompt),
|
||||
media_type="text/plain",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Transfer-Encoding": "chunked"
|
||||
}
|
||||
)
|
||||
else:
|
||||
# 普通非流式调用
|
||||
result = await ModelManager.call_model(
|
||||
model_id=model_id,
|
||||
prompt=prompt,
|
||||
system_prompt=system_prompt
|
||||
)
|
||||
return self.success(result, "模型调用成功" if result.get('success') else "模型调用失败")
|
||||
# 强制使用非流式调用
|
||||
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))
|
||||
|
||||
async def _stream_data(self, model_id: str, prompt: str, system_prompt: Optional[str] = None):
|
||||
"""真正的流式输出数据"""
|
||||
try:
|
||||
# 重新调用模型,获取流式响应
|
||||
model = ModelManager.get_model_by_id(model_id)
|
||||
if not model:
|
||||
yield "data: " + json.dumps({"error": "模型不存在", "done": True}) + "\n\n"
|
||||
return
|
||||
|
||||
# 设置流式请求
|
||||
api_url = model.get('apiUrl', '').strip()
|
||||
api_key = model.get('apiKey', '').strip()
|
||||
version = model.get('version', '').strip()
|
||||
provider = model.get('provider', '').strip()
|
||||
|
||||
# 准备请求数据
|
||||
request_data = {
|
||||
"model": version,
|
||||
"messages": [],
|
||||
"temperature": model.get('temperature', 0.7),
|
||||
"top_p": model.get('topP', 1.0),
|
||||
"max_tokens": model.get('maxTokens', 2048),
|
||||
"stream": True # 启用流式
|
||||
}
|
||||
|
||||
# 添加系统提示词
|
||||
if system_prompt:
|
||||
request_data["messages"].append({
|
||||
"role": "system",
|
||||
"content": system_prompt
|
||||
})
|
||||
elif model.get('systemPrompt'):
|
||||
request_data["messages"].append({
|
||||
"role": "system",
|
||||
"content": model.get('systemPrompt')
|
||||
})
|
||||
|
||||
# 添加用户提示词
|
||||
request_data["messages"].append({
|
||||
"role": "user",
|
||||
"content": prompt
|
||||
})
|
||||
|
||||
# 设置请求头
|
||||
headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
if api_key:
|
||||
if provider.lower() == 'anthropic':
|
||||
headers["x-api-key"] = api_key
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# 发送流式请求
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
if provider.lower() == 'anthropic':
|
||||
# Anthropic流式请求
|
||||
anthropic_request = {
|
||||
"model": version,
|
||||
"max_tokens": request_data["max_tokens"],
|
||||
"messages": request_data["messages"][1:],
|
||||
"stream": True
|
||||
}
|
||||
if system_prompt or model.get('systemPrompt'):
|
||||
anthropic_request["system"] = system_prompt or model.get('systemPrompt')
|
||||
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{api_url.rstrip('/')}/messages",
|
||||
headers=headers,
|
||||
json=anthropic_request
|
||||
) as response:
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith('data: '):
|
||||
data_str = line[6:]
|
||||
if data_str.strip() == '[DONE]':
|
||||
yield "data: " + json.dumps({"content": "", "done": True}) + "\n\n"
|
||||
break
|
||||
|
||||
try:
|
||||
chunk_data = json.loads(data_str)
|
||||
if "content" in chunk_data and len(chunk_data["content"]) > 0:
|
||||
content = chunk_data["content"][0]["text"]
|
||||
yield f"data: {json.dumps({'content': content, 'done': False})}\n\n"
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
# OpenAI兼容流式请求
|
||||
async with client.stream(
|
||||
"POST",
|
||||
f"{api_url.rstrip('/')}/chat/completions",
|
||||
headers=headers,
|
||||
json=request_data
|
||||
) as response:
|
||||
async for line in response.aiter_lines():
|
||||
if line.startswith('data: '):
|
||||
data_str = line[6:]
|
||||
if data_str.strip() == '[DONE]':
|
||||
yield "data: " + json.dumps({"content": "", "done": True}) + "\n\n"
|
||||
break
|
||||
|
||||
try:
|
||||
chunk_data = json.loads(data_str)
|
||||
if "choices" in chunk_data and len(chunk_data["choices"]) > 0:
|
||||
delta = chunk_data["choices"][0].get("delta", {})
|
||||
if "content" in delta and delta["content"]:
|
||||
content = delta["content"]
|
||||
yield f"data: {json.dumps({'content': content, 'done': False})}\n\n"
|
||||
except:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
yield "data: " + json.dumps({"error": str(e), "done": True}) + "\n\n"
|
||||
|
||||
@post("/batch-delete")
|
||||
async def batch_delete_models(self, model_ids: List[str] = Body(...)):
|
||||
"""批量删除模型"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -12,21 +12,21 @@ class Settings(BaseSettings):
|
||||
|
||||
# 服务器配置
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 1112
|
||||
port: int = 3000 # 默认使用config.json中的端口
|
||||
workers: int = 1
|
||||
|
||||
# 日志配置
|
||||
log_level: str = "INFO"
|
||||
log_file: Optional[str] = "logs/app.log"
|
||||
log_format: str = "json"
|
||||
log_to_console: bool = False
|
||||
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
|
||||
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
|
||||
@@ -39,10 +39,35 @@ class Settings(BaseSettings):
|
||||
cors_headers: list[str] = ["*"]
|
||||
|
||||
class Config:
|
||||
env_file = str(Path(__file__).parent.parent.parent / ".env")
|
||||
env_file_encoding = "utf-8"
|
||||
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()
|
||||
@@ -224,7 +224,7 @@ def setup_routes(app: FastAPI):
|
||||
@app.get("/routes")
|
||||
async def get_routes_info():
|
||||
"""获取所有路由信息"""
|
||||
from ..api.discovery import get_registered_modules_info
|
||||
from ..api.internal.discovery import get_registered_modules_info
|
||||
|
||||
response_data = {
|
||||
"app_routes": [
|
||||
@@ -246,6 +246,64 @@ def setup_routes(app: FastAPI):
|
||||
|
||||
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
|
||||
|
||||
@@ -288,4 +346,7 @@ def get_app() -> FastAPI:
|
||||
with _app_lock:
|
||||
if _app_instance is None:
|
||||
_app_instance = create_app()
|
||||
return _app_instance
|
||||
return _app_instance
|
||||
|
||||
# 导出应用实例
|
||||
app = get_app()
|
||||
@@ -94,14 +94,88 @@ class FileUploadService:
|
||||
|
||||
# 文件信息存储(内存存储,可扩展为数据库)
|
||||
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:
|
||||
"""生成存储文件名"""
|
||||
|
||||
10
request/start-backend.bat
Normal file
10
request/start-backend.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
@echo off
|
||||
echo Starting Backend API Server on Port 3000...
|
||||
echo.
|
||||
echo Backend API: http://localhost:3000/models/
|
||||
echo Press Ctrl+C to stop the server
|
||||
echo.
|
||||
cd /d "%~dp0"
|
||||
set PYTHONPATH=src
|
||||
/d/Softwares/Anaconda/python -m uvicorn src.core.app:app --host 0.0.0.0 --port 3000
|
||||
pause
|
||||
@@ -1 +1 @@
|
||||
0bbf8a1a0807bf0cee9cf3cc5634f318
|
||||
58852b1a007ac2ceb1f790a423c070f2
|
||||
@@ -854,20 +854,8 @@
|
||||
"Datasets"
|
||||
],
|
||||
"summary": "list_datasets",
|
||||
"description": "获取所有数据集列表\n\nArgs:\n list_all: 是否列出data目录下的所有文件(物理文件),默认False(只列出API上传的文件)\n\nReturns:\n StandardResponse: 包含数据集列表的标准响应",
|
||||
"description": "获取数据集列表(只返回filename_mapping.json中记录的文件)\n\nReturns:\n StandardResponse: 包含数据集列表的标准响应",
|
||||
"operationId": "list_datasets_datasets_get",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "list_all",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "List All"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
@@ -878,16 +866,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,7 +962,7 @@
|
||||
"Models"
|
||||
],
|
||||
"summary": "call_model_api",
|
||||
"description": "调用模型进行对话(支持流式输出)",
|
||||
"description": "调用模型进行对话(强制使用非流式响应)",
|
||||
"operationId": "call_model_api_models__model_id__call_post",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1285,7 +1263,7 @@
|
||||
"Models"
|
||||
],
|
||||
"summary": "test_model_connection",
|
||||
"description": "测试模型连接",
|
||||
"description": "测试模型连接 - 禁用流式响应",
|
||||
"operationId": "test_model_connection_models__model_id__test_post",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1319,6 +1297,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sse": {
|
||||
"get": {
|
||||
"summary": "Sse Demo",
|
||||
"description": "SSE模拟大模型流式输出演示",
|
||||
"operationId": "sse_demo_sse_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
||||
98
request/static/sse-test.html
Normal file
98
request/static/sse-test.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSE流式输出演示</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#messages {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
font-size: 16px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.done-indicator {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🚀 大模型流式输出演示</h1>
|
||||
<div id="messages">正在连接服务器...</div>
|
||||
|
||||
<script>
|
||||
const messageBox = document.getElementById('messages');
|
||||
let currentContent = '';
|
||||
|
||||
// 使用相对路径,避免跨域问题
|
||||
const source = new EventSource('/sse');
|
||||
|
||||
source.onopen = function() {
|
||||
console.log('SSE连接已建立');
|
||||
messageBox.innerHTML = '正在生成内容...\n\n';
|
||||
};
|
||||
|
||||
source.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
if (data.error) {
|
||||
messageBox.innerHTML += '<div style="color: red;">错误: ' + data.error + '</div>';
|
||||
source.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.content !== undefined && data.content !== null) {
|
||||
// 追加内容
|
||||
currentContent += data.content;
|
||||
messageBox.innerHTML = currentContent;
|
||||
|
||||
// 自动滚动到底部
|
||||
messageBox.scrollTop = messageBox.scrollHeight;
|
||||
}
|
||||
|
||||
if (data.done) {
|
||||
console.log('流式输出完成');
|
||||
messageBox.innerHTML += '\n\n<div class="done-indicator">✅ 内容生成完成!</div>';
|
||||
source.close();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析数据失败:', e);
|
||||
console.log('原始数据:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
source.onerror = function(error) {
|
||||
console.error('SSE连接出错:', error);
|
||||
messageBox.innerHTML += '<div style="color: red; margin-top: 20px;">❌ 连接失败</div>';
|
||||
source.close();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
396
streaming-test.html
Normal file
396
streaming-test.html
Normal file
@@ -0,0 +1,396 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>流式输出测试页面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #5a67d8;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #a0aec0;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.stream-output {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: #2d3748;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-track {
|
||||
background: #edf2f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stream-output::-webkit-scrollbar-thumb:hover {
|
||||
background: #a0aec0;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #edf2f7;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #4a5568;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.loading-dots {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.loading-dots::after {
|
||||
content: '.';
|
||||
animation: dots 1s steps(5, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% { content: '.'; }
|
||||
40% { content: '..'; }
|
||||
60% { content: '...'; }
|
||||
80%, 100% { content: '.'; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✨ 流式输出测试</h1>
|
||||
|
||||
<div class="controls">
|
||||
<button id="startBtn">开始流式输出</button>
|
||||
<button id="stopBtn" disabled>停止输出</button>
|
||||
<button id="clearBtn">清空内容</button>
|
||||
</div>
|
||||
|
||||
<div id="output" class="stream-output">
|
||||
<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stats-item">
|
||||
<span>状态:</span>
|
||||
<span id="status" style="font-weight: 600;">就绪</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span>字符数:</span>
|
||||
<span id="charCount">0</span>
|
||||
</div>
|
||||
<div class="stats-item">
|
||||
<span>耗时:</span>
|
||||
<span id="timeElapsed">0.00s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 页面元素
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const clearBtn = document.getElementById('clearBtn');
|
||||
const output = document.getElementById('output');
|
||||
const statusEl = document.getElementById('status');
|
||||
const charCountEl = document.getElementById('charCount');
|
||||
const timeElapsedEl = document.getElementById('timeElapsed');
|
||||
|
||||
// 状态变量
|
||||
let controller = null;
|
||||
let startTime = 0;
|
||||
let charCount = 0;
|
||||
let timer = null;
|
||||
|
||||
// 模拟大模型输出的文本内容
|
||||
const sampleText = `欢迎使用流式输出测试页面!
|
||||
|
||||
这是一个模拟大语言模型生成内容的演示。
|
||||
|
||||
流式输出是一种将生成的内容逐字逐句发送给用户的技术,
|
||||
它可以显著提升用户体验,让用户感觉内容是实时生成的。
|
||||
|
||||
在实际应用中,大语言模型会在生成每个token后立即发送给客户端,
|
||||
而不需要等待整个响应完成。
|
||||
|
||||
这种技术特别适合:
|
||||
- 长文本生成
|
||||
- 对话系统
|
||||
- 代码生成
|
||||
- 实时翻译
|
||||
|
||||
流式输出的优势包括:
|
||||
1. 更快的感知响应速度
|
||||
2. 更好的用户体验
|
||||
3. 更低的内存占用
|
||||
4. 支持更长的内容生成
|
||||
|
||||
在这个测试中,我们将模拟每秒生成约50个字符的速度,
|
||||
您可以看到文本是如何逐字逐句显示的。
|
||||
|
||||
您可以随时点击"停止输出"按钮来中断流式传输,
|
||||
也可以点击"清空内容"按钮重新开始。
|
||||
|
||||
让我们继续探索流式输出的更多应用场景...
|
||||
|
||||
在Web应用中,流式输出通常使用以下技术实现:
|
||||
- Server-Sent Events (SSE)
|
||||
- WebSockets
|
||||
- HTTP/2 Server Push
|
||||
|
||||
每种技术都有其优缺点,选择合适的技术取决于具体的应用场景。
|
||||
|
||||
例如,SSE适合单向的服务器到客户端通信,
|
||||
而WebSockets适合双向通信。
|
||||
|
||||
流式输出不仅提升了用户体验,
|
||||
也为开发者提供了更多的灵活性和控制能力。
|
||||
|
||||
感谢您使用这个测试页面!
|
||||
|
||||
希望您对流式输出技术有了更深入的了解。
|
||||
|
||||
如果您有任何问题或建议,欢迎随时提出。
|
||||
|
||||
祝您使用愉快!`;
|
||||
|
||||
// 更新统计信息
|
||||
function updateStats() {
|
||||
charCountEl.textContent = charCount;
|
||||
|
||||
if (startTime > 0) {
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
timeElapsedEl.textContent = elapsed.toFixed(2) + 's';
|
||||
}
|
||||
}
|
||||
|
||||
// 清空输出
|
||||
function clearOutput() {
|
||||
output.innerHTML = '<div style="color: #718096;">点击上方按钮开始测试流式输出...</div>';
|
||||
charCount = 0;
|
||||
updateStats();
|
||||
statusEl.textContent = '就绪';
|
||||
}
|
||||
|
||||
// 模拟流式输出
|
||||
async function simulateStreaming() {
|
||||
startBtn.disabled = true;
|
||||
stopBtn.disabled = false;
|
||||
clearOutput();
|
||||
|
||||
output.textContent = '';
|
||||
statusEl.innerHTML = '生成中 <span class="loading-dots"></span>';
|
||||
|
||||
startTime = Date.now();
|
||||
charCount = 0;
|
||||
|
||||
// 创建AbortController用于取消操作
|
||||
controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
try {
|
||||
// 使用ReadableStream模拟流式输出
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
let index = 0;
|
||||
|
||||
while (index < sampleText.length && !signal.aborted) {
|
||||
// 随机延迟,模拟真实生成速度
|
||||
const delay = Math.random() * 80 + 40; // 40-120ms
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// 每次发送1-5个字符
|
||||
const chunkSize = Math.floor(Math.random() * 5) + 1;
|
||||
const chunk = sampleText.slice(index, index + chunkSize);
|
||||
|
||||
controller.enqueue(new TextEncoder().encode(chunk));
|
||||
index += chunkSize;
|
||||
|
||||
// 更新统计信息
|
||||
charCount += chunk.length;
|
||||
updateStats();
|
||||
}
|
||||
|
||||
controller.close();
|
||||
},
|
||||
cancel() {
|
||||
console.log('流式输出已取消');
|
||||
}
|
||||
});
|
||||
|
||||
// 读取并显示流内容
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// 追加内容到输出
|
||||
output.textContent += decoder.decode(value, { stream: true });
|
||||
|
||||
// 自动滚动到底部
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
// 完成
|
||||
statusEl.textContent = '完成';
|
||||
output.style.borderColor = '#48bb78';
|
||||
|
||||
// 添加完成标记
|
||||
const doneMarker = document.createElement('div');
|
||||
doneMarker.style.cssText = `
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #48bb78;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
`;
|
||||
doneMarker.textContent = '✅ 流式输出完成!';
|
||||
output.appendChild(doneMarker);
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
statusEl.textContent = '已停止';
|
||||
output.style.borderColor = '#ed8936';
|
||||
|
||||
const stopMarker = document.createElement('div');
|
||||
stopMarker.style.cssText = `
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #ed8936;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
`;
|
||||
stopMarker.textContent = '⏸️ 流式输出已停止';
|
||||
output.appendChild(stopMarker);
|
||||
} else {
|
||||
statusEl.textContent = '错误';
|
||||
output.innerHTML += `\n\n❌ 发生错误: ${error.message}`;
|
||||
output.style.borderColor = '#f56565';
|
||||
}
|
||||
} finally {
|
||||
// 重置状态
|
||||
startBtn.disabled = false;
|
||||
stopBtn.disabled = true;
|
||||
controller = null;
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 停止流式输出
|
||||
function stopStreaming() {
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
controller = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 事件监听器
|
||||
startBtn.addEventListener('click', simulateStreaming);
|
||||
stopBtn.addEventListener('click', stopStreaming);
|
||||
clearBtn.addEventListener('click', clearOutput);
|
||||
|
||||
// 页面加载完成
|
||||
window.addEventListener('load', () => {
|
||||
console.log('流式输出测试页面已加载');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -34,85 +34,6 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- 网格布局样式 -->
|
||||
<style>
|
||||
/* 2x2网格布局 - 通用 */
|
||||
.grid-2x2 {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: repeat(2, auto) !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* 1x4网格布局 - 用于模型选择区域 */
|
||||
.grid-1x4 {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
grid-template-rows: 1 !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* 结果展示区域的2x2布局 */
|
||||
#comparisonResults .grid-2x2 {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: repeat(2, auto) !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* 模型选择区域的1x4布局 */
|
||||
#compare-config-section .grid-1x4 {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
grid-template-rows: 1 !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* 确保结果展示的卡片是网格项 */
|
||||
#comparisonResults .grid-2x2 > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 确保模型输出卡片正确排列 */
|
||||
#comparisonResults .grid-2x2 > div:nth-child(1) { grid-row: 1; grid-column: 1; } /* 模型A - 左上 */
|
||||
#comparisonResults .grid-2x2 > div:nth-child(2) { grid-row: 1; grid-column: 2; } /* 模型B - 右上 */
|
||||
#comparisonResults .grid-2x2 > div:nth-child(3) { grid-row: 2; grid-column: 1; } /* 模型C - 左下 */
|
||||
#comparisonResults .grid-2x2 > div:nth-child(4) { grid-row: 2; grid-column: 2; } /* 模型D - 右下 */
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
#compare-config-section .grid-1x4 {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: repeat(2, auto) !important;
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#comparisonResults .grid-2x2 {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
grid-template-rows: repeat(2, auto) !important;
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#compare-config-section .grid-1x4 {
|
||||
grid-template-columns: repeat(1, 1fr) !important;
|
||||
grid-template-rows: repeat(4, auto) !important;
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
#comparisonResults .grid-2x2 {
|
||||
grid-template-columns: repeat(1, 1fr) !important;
|
||||
grid-template-rows: repeat(4, auto) !important;
|
||||
gap: 0.75rem !important;
|
||||
}
|
||||
#comparisonResults .grid-2x2 > div:nth-child(1) { grid-row: 1; grid-column: 1; }
|
||||
#comparisonResults .grid-2x2 > div:nth-child(2) { grid-row: 2; grid-column: 1; }
|
||||
#comparisonResults .grid-2x2 > div:nth-child(3) { grid-row: 3; grid-column: 1; }
|
||||
#comparisonResults .grid-2x2 > div:nth-child(4) { grid-row: 4; grid-column: 1; }
|
||||
}
|
||||
</style>
|
||||
<style type="text/tailwindcss">
|
||||
@layer utilities {
|
||||
.gradient-blue {
|
||||
@@ -515,7 +436,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 模型选择区 -->
|
||||
<div class="grid-1x4 mb-4">
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<!-- 模型A -->
|
||||
<div class="bg-gray-50 rounded-xl p-3 border-2 border-dashboard-primary/20">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
|
||||
@@ -559,50 +480,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型C -->
|
||||
<div class="bg-gray-50 rounded-xl p-3 border-2 border-gray-300 opacity-50 transition-opacity hover:opacity-100">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-500 text-white flex items-center justify-center text-sm font-bold mr-2">C</span>
|
||||
模型 C
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-dashboard-text mb-2">选择模型</label>
|
||||
<select id="modelCSelect" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-dashboard-primary/20">
|
||||
<option value="">请选择模型C (可选)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-dashboard-text mb-2">模型信息</label>
|
||||
<div id="modelCInfo" class="text-sm text-dashboard-textLight bg-white p-3 rounded-lg border border-gray-200">
|
||||
请先选择模型
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型D -->
|
||||
<div class="bg-gray-50 rounded-xl p-3 border-2 border-gray-300 opacity-50 transition-opacity hover:opacity-100">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-500 text-white flex items-center justify-center text-sm font-bold mr-2">D</span>
|
||||
模型 D
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-dashboard-text mb-2">选择模型</label>
|
||||
<select id="modelDSelect" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-dashboard-primary/20">
|
||||
<option value="">请选择模型D (可选)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-dashboard-text mb-2">模型信息</label>
|
||||
<div id="modelDInfo" class="text-sm text-dashboard-textLight bg-white p-3 rounded-lg border border-gray-200">
|
||||
请先选择模型
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试输入区 -->
|
||||
@@ -653,7 +530,7 @@
|
||||
<span class="font-semibold">显示配置</span>
|
||||
</button>
|
||||
<div id="comparisonResults" class="hidden">
|
||||
<div class="grid-2x2">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- 模型A输出 -->
|
||||
<div class="bg-white rounded-xl p-4 border border-gray-200">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -707,59 +584,18 @@
|
||||
<div>Token数: <span id="modelBTokens">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型C输出 -->
|
||||
<div class="bg-white rounded-xl p-4 border border-gray-200 hidden" id="modelCOutputCard">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-500 text-white flex items-center justify-center text-sm font-bold mr-2">C</span>
|
||||
<span id="modelCName">模型C</span>
|
||||
</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="copyToClipboard('modelCOutput')" class="text-dashboard-textLight hover:text-dashboard-primary">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-dashboard-textLight mb-2">输出结果</div>
|
||||
<div id="modelCOutput" class="bg-gray-50 p-4 rounded-lg border border-gray-200" style="min-height: 300px; max-height: 600px; overflow-y: auto;">
|
||||
<div class="text-dashboard-textLight">等待对比结果...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-dashboard-textLight">
|
||||
<div>响应时间: <span id="modelCTime">-</span></div>
|
||||
<div>Token数: <span id="modelCTokens">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 模型D输出 -->
|
||||
<div class="bg-white rounded-xl p-4 border border-gray-200 hidden" id="modelDOutputCard">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text flex items-center">
|
||||
<span class="w-6 h-6 rounded-full bg-gray-500 text-white flex items-center justify-center text-sm font-bold mr-2">D</span>
|
||||
<span id="modelDName">模型D</span>
|
||||
</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="copyToClipboard('modelDOutput')" class="text-dashboard-textLight hover:text-dashboard-primary">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="text-xs text-dashboard-textLight mb-2">输出结果</div>
|
||||
<div id="modelDOutput" class="bg-gray-50 p-4 rounded-lg border border-gray-200" style="min-height: 300px; max-height: 600px; overflow-y: auto;">
|
||||
<div class="text-dashboard-textLight">等待对比结果...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-dashboard-textLight">
|
||||
<div>响应时间: <span id="modelDTime">-</span></div>
|
||||
<div>Token数: <span id="modelDTokens">-</span></div>
|
||||
</div>
|
||||
<!-- 对比分析 -->
|
||||
<div class="mt-4 bg-white rounded-xl p-4 border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
||||
</svg>
|
||||
对比分析
|
||||
</h3>
|
||||
<div id="comparisonAnalysis" class="space-y-3">
|
||||
<div class="text-dashboard-textLight">对比结果将在这里显示...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2046,9 +1882,7 @@
|
||||
|
||||
let comparisonModels = {
|
||||
modelA: null,
|
||||
modelB: null,
|
||||
modelC: null,
|
||||
modelD: null
|
||||
modelB: null
|
||||
};
|
||||
|
||||
// 初始化模型对比页面
|
||||
@@ -2087,16 +1921,12 @@
|
||||
function loadComparisonModelList() {
|
||||
const modelASelect = document.getElementById('modelASelect');
|
||||
const modelBSelect = document.getElementById('modelBSelect');
|
||||
const modelCSelect = document.getElementById('modelCSelect');
|
||||
const modelDSelect = document.getElementById('modelDSelect');
|
||||
|
||||
if (!modelASelect || !modelBSelect) return;
|
||||
|
||||
// 清空现有选项
|
||||
modelASelect.innerHTML = '<option value="">请选择模型A</option>';
|
||||
modelBSelect.innerHTML = '<option value="">请选择模型B</option>';
|
||||
if (modelCSelect) modelCSelect.innerHTML = '<option value="">请选择模型C (可选)</option>';
|
||||
if (modelDSelect) modelDSelect.innerHTML = '<option value="">请选择模型D (可选)</option>';
|
||||
|
||||
// 添加模型选项
|
||||
modelConfigs.forEach((model, index) => {
|
||||
@@ -2109,20 +1939,6 @@
|
||||
optionB.value = index;
|
||||
optionB.textContent = `${model.name} (${getProviderName(model.provider)})`;
|
||||
modelBSelect.appendChild(optionB);
|
||||
|
||||
if (modelCSelect) {
|
||||
const optionC = document.createElement('option');
|
||||
optionC.value = index;
|
||||
optionC.textContent = `${model.name} (${getProviderName(model.provider)})`;
|
||||
modelCSelect.appendChild(optionC);
|
||||
}
|
||||
|
||||
if (modelDSelect) {
|
||||
const optionD = document.createElement('option');
|
||||
optionD.value = index;
|
||||
optionD.textContent = `${model.name} (${getProviderName(model.provider)})`;
|
||||
modelDSelect.appendChild(optionD);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2130,8 +1946,6 @@
|
||||
function setupComparisonEventListeners() {
|
||||
const modelASelect = document.getElementById('modelASelect');
|
||||
const modelBSelect = document.getElementById('modelBSelect');
|
||||
const modelCSelect = document.getElementById('modelCSelect');
|
||||
const modelDSelect = document.getElementById('modelDSelect');
|
||||
const testTemperature = document.getElementById('testTemperature');
|
||||
|
||||
if (modelASelect) {
|
||||
@@ -2142,14 +1956,6 @@
|
||||
modelBSelect.onchange = () => handleModelSelection('B', modelBSelect.value);
|
||||
}
|
||||
|
||||
if (modelCSelect) {
|
||||
modelCSelect.onchange = () => handleModelSelection('C', modelCSelect.value);
|
||||
}
|
||||
|
||||
if (modelDSelect) {
|
||||
modelDSelect.onchange = () => handleModelSelection('D', modelDSelect.value);
|
||||
}
|
||||
|
||||
if (testTemperature) {
|
||||
testTemperature.oninput = (e) => {
|
||||
document.getElementById('tempDisplay').textContent = e.target.value;
|
||||
@@ -2202,27 +2008,22 @@
|
||||
function checkModelSelection() {
|
||||
const modelASelect = document.getElementById('modelASelect');
|
||||
const modelBSelect = document.getElementById('modelBSelect');
|
||||
const modelCSelect = document.getElementById('modelCSelect');
|
||||
const modelDSelect = document.getElementById('modelDSelect');
|
||||
|
||||
// 获取所有已选择的模型索引
|
||||
const selected = [];
|
||||
if (modelASelect && modelASelect.value) selected.push(modelASelect.value);
|
||||
if (modelBSelect && modelBSelect.value) selected.push(modelBSelect.value);
|
||||
if (modelCSelect && modelCSelect.value) selected.push(modelCSelect.value);
|
||||
if (modelDSelect && modelDSelect.value) selected.push(modelDSelect.value);
|
||||
if (modelASelect && modelBSelect) {
|
||||
// 禁用已选择的模型
|
||||
const selectedA = modelASelect.value;
|
||||
const selectedB = modelBSelect.value;
|
||||
|
||||
// 为每个下拉框禁用已选择的模型
|
||||
const selects = [modelASelect, modelBSelect, modelCSelect, modelDSelect];
|
||||
selects.forEach(select => {
|
||||
if (!select) return;
|
||||
Array.from(select.options).forEach((option, index) => {
|
||||
Array.from(modelASelect.options).forEach((option, index) => {
|
||||
if (index === 0) return; // 跳过第一个选项
|
||||
const optionValue = index.toString();
|
||||
// 如果该选项在其他下拉框中被选中,则禁用
|
||||
option.disabled = selected.includes(optionValue);
|
||||
option.disabled = index.toString() === selectedB;
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(modelBSelect.options).forEach((option, index) => {
|
||||
if (index === 0) return; // 跳过第一个选项
|
||||
option.disabled = index.toString() === selectedA;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 折叠配置区
|
||||
@@ -2322,22 +2123,15 @@
|
||||
|
||||
// 运行模型对比
|
||||
async function runModelComparison() {
|
||||
const modelA = comparisonModels.modelA;
|
||||
const modelB = comparisonModels.modelB;
|
||||
const prompt = document.getElementById('testPrompt').value.trim();
|
||||
const temperature = parseFloat(document.getElementById('testTemperature').value);
|
||||
const maxTokens = parseInt(document.getElementById('testMaxTokens').value);
|
||||
|
||||
// 获取所有选择的模型
|
||||
const selectedModels = [];
|
||||
for (let modelType of ['A', 'B', 'C', 'D']) {
|
||||
const model = comparisonModels[`model${modelType}`];
|
||||
if (model) {
|
||||
selectedModels.push({ model, modelType });
|
||||
}
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if (selectedModels.length < 2) {
|
||||
alert('请至少选择两个模型进行对比');
|
||||
if (!modelA || !modelB) {
|
||||
alert('请选择两个模型进行对比');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2353,37 +2147,22 @@
|
||||
// 折叠配置区
|
||||
collapseConfigSection();
|
||||
|
||||
// 更新模型名称
|
||||
document.getElementById('modelAName').textContent = modelA.name;
|
||||
document.getElementById('modelBName').textContent = modelB.name;
|
||||
|
||||
// 重置输出区域
|
||||
resetOutputAreas();
|
||||
|
||||
// 显示已选择模型的流式输出状态,并显示/隐藏对应的卡片
|
||||
selectedModels.forEach(({ model, modelType }) => {
|
||||
// 更新模型名称
|
||||
const nameElement = document.getElementById(`model${modelType}Name`);
|
||||
if (nameElement) {
|
||||
nameElement.textContent = model.name;
|
||||
}
|
||||
// 显示流式输出状态
|
||||
const modelAOutput = document.getElementById('modelAOutput');
|
||||
const modelBOutput = document.getElementById('modelBOutput');
|
||||
|
||||
// 显示对应的输出卡片
|
||||
const outputCard = document.getElementById(`model${modelType}OutputCard`);
|
||||
if (outputCard) {
|
||||
outputCard.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 显示流式输出状态
|
||||
const outputElement = document.getElementById(`model${modelType}Output`);
|
||||
if (outputElement) {
|
||||
outputElement.innerHTML = '<div class="text-dashboard-text"><span class="animate-pulse">正在生成中...</span></div>';
|
||||
}
|
||||
});
|
||||
|
||||
// 隐藏未选择模型的输出卡片
|
||||
for (let modelType of ['C', 'D']) {
|
||||
const model = comparisonModels[`model${modelType}`];
|
||||
const outputCard = document.getElementById(`model${modelType}OutputCard`);
|
||||
if (outputCard && !model) {
|
||||
outputCard.classList.add('hidden');
|
||||
}
|
||||
if (modelAOutput) {
|
||||
modelAOutput.innerHTML = '<div class="text-dashboard-text"><span class="animate-pulse">正在生成中...</span></div>';
|
||||
}
|
||||
if (modelBOutput) {
|
||||
modelBOutput.innerHTML = '<div class="text-dashboard-text"><span class="animate-pulse">正在生成中...</span></div>';
|
||||
}
|
||||
|
||||
// 开始对比
|
||||
@@ -2393,44 +2172,51 @@
|
||||
|
||||
try {
|
||||
// 创建流式显示回调
|
||||
const streamingCache = {};
|
||||
const onChunkCallbacks = {};
|
||||
const modelPromises = [];
|
||||
const streamingCache = {
|
||||
'A': { output: '', responseTime: 0, streaming: false },
|
||||
'B': { output: '', responseTime: 0, streaming: false }
|
||||
};
|
||||
|
||||
// 为每个选择的模型创建回调和Promise
|
||||
selectedModels.forEach(({ model, modelType }) => {
|
||||
streamingCache[modelType] = { output: '', responseTime: 0, streaming: false };
|
||||
const onChunkA = (chunk) => {
|
||||
streamingCache['A'].output = chunk.content;
|
||||
streamingCache['A'].responseTime = Date.now();
|
||||
streamingCache['A'].streaming = true;
|
||||
displayStreamingModelResult('A', streamingCache['A']);
|
||||
};
|
||||
|
||||
onChunkCallbacks[modelType] = (chunk) => {
|
||||
streamingCache[modelType].output = chunk.content;
|
||||
streamingCache[modelType].responseTime = Date.now();
|
||||
streamingCache[modelType].streaming = true;
|
||||
displayStreamingModelResult(modelType, streamingCache[modelType]);
|
||||
};
|
||||
const onChunkB = (chunk) => {
|
||||
streamingCache['B'].output = chunk.content;
|
||||
streamingCache['B'].responseTime = Date.now();
|
||||
streamingCache['B'].streaming = true;
|
||||
displayStreamingModelResult('B', streamingCache['B']);
|
||||
};
|
||||
|
||||
// 创建模型调用Promise
|
||||
const promise = withTimeout(
|
||||
callRealModel(model, prompt, temperature, maxTokens, model.streaming ? onChunkCallbacks[modelType] : null),
|
||||
60000
|
||||
);
|
||||
modelPromises.push({ promise, modelType });
|
||||
});
|
||||
// 并行运行两个模型(带超时控制)
|
||||
const [resultA, resultB] = await Promise.allSettled([
|
||||
withTimeout(callRealModel(modelA, prompt, temperature, maxTokens, modelA.streaming ? onChunkA : null), 60000), // 60秒超时
|
||||
withTimeout(callRealModel(modelB, prompt, temperature, maxTokens, modelB.streaming ? onChunkB : null), 60000) // 60秒超时
|
||||
]);
|
||||
|
||||
// 并行运行所有模型
|
||||
const results = await Promise.allSettled(modelPromises.map(item => item.promise));
|
||||
|
||||
// 处理所有模型结果
|
||||
results.forEach((result, index) => {
|
||||
const modelType = modelPromises[index].modelType;
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
if (!result.value.streaming) {
|
||||
displayModelResult(modelType, result.value);
|
||||
}
|
||||
} else {
|
||||
displayModelError(modelType, result.reason);
|
||||
// 处理模型A结果
|
||||
if (resultA.status === 'fulfilled') {
|
||||
if (!resultA.value.streaming) {
|
||||
displayModelResult('A', resultA.value);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
displayModelError('A', resultA.reason);
|
||||
}
|
||||
|
||||
// 处理模型B结果
|
||||
if (resultB.status === 'fulfilled') {
|
||||
if (!resultB.value.streaming) {
|
||||
displayModelResult('B', resultB.value);
|
||||
}
|
||||
} else {
|
||||
displayModelError('B', resultB.reason);
|
||||
}
|
||||
|
||||
// 生成对比分析
|
||||
generateComparisonAnalysis(modelA, modelB);
|
||||
|
||||
} catch (error) {
|
||||
console.error('对比过程中出错:', error);
|
||||
@@ -2474,23 +2260,18 @@
|
||||
// 检查是否启用流式输出
|
||||
const contentType = response.headers.get('content-type');
|
||||
const isStreaming = contentType?.includes('text/plain');
|
||||
console.log('[流式调试] Content-Type:', contentType);
|
||||
console.log('[流式调试] isStreaming:', isStreaming);
|
||||
console.log('[流式调试] onChunk:', onChunk);
|
||||
|
||||
if (isStreaming) {
|
||||
// 流式处理
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullContent = '';
|
||||
let chunkCount = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
console.log('[流式调试] 收到数据块:', chunk);
|
||||
const lines = chunk.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -2500,14 +2281,11 @@
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
console.log('[流式调试] 解析数据:', parsed);
|
||||
if (parsed.content !== undefined) {
|
||||
if (parsed.content) {
|
||||
fullContent += parsed.content;
|
||||
chunkCount++;
|
||||
// 调用回调函数更新UI
|
||||
if (onChunk) {
|
||||
console.log('[流式调试] 调用回调函数, 当前内容长度:', fullContent.length);
|
||||
onChunk({
|
||||
content: fullContent,
|
||||
delta: parsed.content,
|
||||
@@ -2516,12 +2294,11 @@
|
||||
}
|
||||
}
|
||||
if (parsed.done) {
|
||||
console.log('[流式调试] 流式输出完成, 总块数:', chunkCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[流式调试] 解析错误:', e);
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2654,8 +2431,53 @@
|
||||
tokensElement.textContent = '-';
|
||||
}
|
||||
});
|
||||
|
||||
const analysisElement = document.getElementById('comparisonAnalysis');
|
||||
if (analysisElement) {
|
||||
analysisElement.innerHTML = '<div class="text-dashboard-textLight">正在分析对比结果...</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 生成对比分析
|
||||
function generateComparisonAnalysis(modelA, modelB) {
|
||||
const analysisElement = document.getElementById('comparisonAnalysis');
|
||||
if (!analysisElement) return;
|
||||
|
||||
const analysis = [
|
||||
{
|
||||
title: '响应速度',
|
||||
content: '两个模型的响应时间对比分析'
|
||||
},
|
||||
{
|
||||
title: '回答质量',
|
||||
content: '基于内容完整性、准确性和有用性的评估'
|
||||
},
|
||||
{
|
||||
title: '语言风格',
|
||||
content: '分析两个模型的语言表达方式和风格特点'
|
||||
},
|
||||
{
|
||||
title: '详细程度',
|
||||
content: '比较回答的详细程度和信息密度'
|
||||
},
|
||||
{
|
||||
title: '总体评价',
|
||||
content: '综合两个模型在本次测试中的表现'
|
||||
}
|
||||
];
|
||||
|
||||
let analysisHTML = '';
|
||||
analysis.forEach(item => {
|
||||
analysisHTML += `
|
||||
<div class="border-l-4 border-dashboard-primary pl-4">
|
||||
<h4 class="font-semibold text-dashboard-text mb-1">${item.title}</h4>
|
||||
<p class="text-sm text-dashboard-textLight">${item.content}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
analysisElement.innerHTML = analysisHTML;
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
function copyToClipboard(elementId) {
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 启动 HTTP 服务器"
|
||||
echo "===================="
|
||||
echo ""
|
||||
|
||||
# 获取脚本所在目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "📂 当前目录: $SCRIPT_DIR"
|
||||
echo ""
|
||||
|
||||
# 检测操作系统类型
|
||||
echo "🔍 检测操作系统..."
|
||||
OSTYPE_LOWER=$(echo "$OSTYPE" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# 检测是否在 WSL 中
|
||||
IS_WSL=0
|
||||
if [[ -n "$WSLENV" ]] || grep -qi microsoft /proc/version 2>/dev/null; then
|
||||
IS_WSL=1
|
||||
OS_TYPE="WSL (Windows Subsystem for Linux)"
|
||||
echo "📟 检测到 WSL 环境"
|
||||
elif [[ "$OSTYPE_LOWER" == "linux-gnu"* ]]; then
|
||||
OS_TYPE="Linux"
|
||||
echo "🐧 检测到 Linux 环境"
|
||||
elif [[ "$OSTYPE_LOWER" == "darwin"* ]]; then
|
||||
OS_TYPE="macOS"
|
||||
echo "🍎 检测到 macOS 环境"
|
||||
elif [[ "$OSTYPE_LOWER" == "msys" ]] || [[ "$OSTYPE_LOWER" == "cygwin" ]]; then
|
||||
OS_TYPE="Windows (MSYS/Cygwin)"
|
||||
echo "🪟 检测到 Windows (MSYS/Cygwin) 环境"
|
||||
else
|
||||
OS_TYPE="Unknown"
|
||||
echo "⚠️ 未识别的操作系统: $OSTYPE"
|
||||
fi
|
||||
|
||||
echo "✅ 操作系统类型: $OS_TYPE"
|
||||
echo ""
|
||||
|
||||
# 根据操作系统选择 Python 命令
|
||||
PYTHON_CMD=""
|
||||
PYTHON_VERSION=""
|
||||
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_CMD="python3"
|
||||
PYTHON_VERSION=$(python3 --version 2>&1)
|
||||
elif command -v python &> /dev/null; then
|
||||
PYTHON_CMD="python"
|
||||
PYTHON_VERSION=$(python --version 2>&1)
|
||||
else
|
||||
echo "❌ 错误: 未找到 Python"
|
||||
echo ""
|
||||
echo "💡 解决方案:"
|
||||
echo " 1. 安装 Python 3.x"
|
||||
echo " 2. 确保 python 或 python3 命令可用"
|
||||
echo " 3. Windows 用户可以使用: start.bat"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 使用 Python 命令: $PYTHON_CMD"
|
||||
echo "✅ Python 版本: $PYTHON_VERSION"
|
||||
echo ""
|
||||
|
||||
# 获取本机IP地址
|
||||
SERVER_IP=""
|
||||
case "$OS_TYPE" in
|
||||
"Linux"|"macOS")
|
||||
# Linux/macOS 使用 hostname -I
|
||||
if command -v hostname &> /dev/null; then
|
||||
SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}' | head -n1)
|
||||
if [ -n "$SERVER_IP" ] && [ "$SERVER_IP" != "localhost" ]; then
|
||||
echo "🌐 检测到 IP 地址: $SERVER_IP"
|
||||
else
|
||||
echo "🌐 未检测到有效 IP,将使用 localhost"
|
||||
SERVER_IP="localhost"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
"WSL (Windows Subsystem for Linux)")
|
||||
echo "💡 WSL 环境提示:"
|
||||
echo " - 如果无法访问 localhost,请检查 WSL 网络配置"
|
||||
echo " - 建议在 Windows 浏览器中访问服务"
|
||||
echo ""
|
||||
echo "🌐 建议使用: http://localhost:8000 访问"
|
||||
SERVER_IP="localhost"
|
||||
;;
|
||||
"Windows (MSYS/Cygwin)"|*)
|
||||
# Windows 下使用 localhost 更可靠
|
||||
echo "🌐 Windows 环境使用 localhost 访问"
|
||||
SERVER_IP="localhost"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$SERVER_IP" ]; then
|
||||
SERVER_IP="localhost"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📱 访问地址:"
|
||||
echo " 主页: http://$SERVER_IP:8000/pages/main.html"
|
||||
echo " 登录: http://$SERVER_IP:8000/pages/login.html"
|
||||
echo ""
|
||||
echo "⚠️ 服务器将在端口 8000 启动"
|
||||
echo "按 Ctrl+C 停止服务器"
|
||||
echo ""
|
||||
|
||||
# 启动 HTTP 服务器
|
||||
echo "🔧 启动中..."
|
||||
echo ""
|
||||
|
||||
# WSL 环境特殊处理
|
||||
if [ $IS_WSL -eq 1 ]; then
|
||||
echo "💡 WSL 启动提示:"
|
||||
echo " - 服务器在 WSL 中启动"
|
||||
echo " - 如果 Windows 浏览器无法访问,可能需要配置 WSL 网络"
|
||||
echo " - 或使用 Windows 的 Python 启动: python -m http.server 8000"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# 启动服务器
|
||||
$PYTHON_CMD -m http.server 8000
|
||||
|
||||
EXIT_CODE=$?
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ 服务器启动失败 (退出码: $EXIT_CODE)"
|
||||
echo ""
|
||||
echo "💡 故障排除:"
|
||||
echo " 1. 检查端口 8000 是否被占用"
|
||||
echo " 2. 确认 Python 3 已正确安装"
|
||||
echo " 3. 尝试使用其他端口: $PYTHON_CMD -m http.server 9000"
|
||||
echo ""
|
||||
fi
|
||||
Reference in New Issue
Block a user