修改了数据集上传,修改了模型配置页面

This commit is contained in:
2026-01-14 16:35:22 +08:00
parent e1ab76a9a1
commit 43b6018a3e
19 changed files with 1058 additions and 1231 deletions

View File

@@ -60,7 +60,17 @@
"Bash(grep:*)", "Bash(grep:*)",
"Bash(tasklist:*)", "Bash(tasklist:*)",
"Bash(./xrequest/Scripts/python.exe:*)", "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
View 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

View File

@@ -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", "id": "e81c21e1-a4ce-4237-ba22-0922b741b9be",
"name": "qwen3-flash", "name": "qwen3-flash",
@@ -117,6 +17,26 @@
"streaming": true, "streaming": true,
"functions": false, "functions": false,
"logRequests": true, "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
View 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

View File

@@ -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 ""

View File

@@ -1 +1 @@
3807 2916

View File

@@ -111,118 +111,60 @@ class DatasetAPI(BaseAPI):
return StandardResponse.error(f"上传失败: {str(e)}") return StandardResponse.error(f"上传失败: {str(e)}")
@get("", response_model=StandardResponse) @get("", response_model=StandardResponse)
async def list_datasets(self, list_all: bool = False): async def list_datasets(self):
""" """
获取所有数据集列表 获取数据集列表只返回filename_mapping.json中记录的文件
Args:
list_all: 是否列出data目录下的所有文件物理文件默认False只列出API上传的文件
Returns: Returns:
StandardResponse: 包含数据集列表的标准响应 StandardResponse: 包含数据集列表的标准响应
""" """
# 添加调试日志
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info(f"list_datasets called with list_all={list_all}")
try: try:
if list_all: import json
# 列出data目录下的所有文件物理文件
import json
from pathlib import Path
data_dir = file_upload_service.upload_dir data_dir = file_upload_service.upload_dir
mapping_file = data_dir / "filename_mapping.json" mapping_file = data_dir / "filename_mapping.json"
# 读取文件名映射 # 读取文件名映射
mappings = {} mappings = {}
if mapping_file.exists(): if mapping_file.exists():
try: with open(mapping_file, 'r', encoding='utf-8') as f:
with open(mapping_file, 'r', encoding='utf-8') as f: mapping_data = json.load(f)
mapping_data = json.load(f) mappings = mapping_data.get("mappings", {})
mappings = mapping_data.get("mappings", {})
except Exception:
mappings = {}
# 获取data目录下的所有JSON文件 # 根据映射文件构建数据集列表
datasets = [] datasets = []
if data_dir.exists(): for file_id, mapping_info in mappings.items():
for file_path in data_dir.iterdir(): # 获取物理文件信息
# 跳过目录和映射文件本身 storage_filename = mapping_info.get("storage_filename", f"{file_id}.json")
if file_path.is_file() and file_path.name != "filename_mapping.json": file_path = data_dir / storage_filename
file_id = file_path.stem # 去掉.json后缀得到file_id
# 从映射文件获取真实文件名 if file_path.exists():
mapping_info = mappings.get(file_id, {}) # 获取文件大小
original_filename = mapping_info.get("original_filename", file_path.name) file_size = file_path.stat().st_size
uploaded_at = mapping_info.get("uploaded_at", "") size_mb = round(file_size / 1024 / 1024, 2)
size_display = format_file_size(file_size)
# 获取文件大小 datasets.append({
file_size = file_path.stat().st_size "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) datasets.sort(key=lambda x: x["uploaded_at"], reverse=True)
size_display = format_file_size(file_size)
datasets.append({ return StandardResponse.success({
"file_id": file_id, "datasets": datasets,
"name": original_filename, "total": len(datasets),
"size": file_size, "source": "filename_mapping"
"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"
})
except Exception as e: except Exception as e:
return StandardResponse.error(f"获取数据集列表失败: {str(e)}") return StandardResponse.error(f"获取数据集列表失败: {str(e)}")
@@ -273,52 +215,6 @@ class DatasetAPI(BaseAPI):
except Exception as e: except Exception as e:
return StandardResponse.error(f"获取数据集详情失败: {str(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) @get("/{file_id}/content", response_model=StandardResponse)
async def get_dataset_content(self, file_id: str, limit: int = 5): async def get_dataset_content(self, file_id: str, limit: int = 5):
""" """

View File

@@ -7,10 +7,8 @@ import os
import json import json
import uuid import uuid
import httpx import httpx
import asyncio
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from fastapi import HTTPException, Body, Response from fastapi import HTTPException, Body, Response
from fastapi.responses import StreamingResponse
import logging 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) 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: class ModelManager:
"""模型配置管理器""" """模型配置管理器"""
@@ -227,245 +150,124 @@ class ModelManager:
@staticmethod @staticmethod
async def call_model(model_id: str, prompt: str, system_prompt: Optional[str] = None) -> Dict[str, Any]: 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) model = ModelManager.get_model_by_id(model_id)
if not model: if not model:
raise HTTPException(status_code=404, detail="模型不存在") return {'success': False, 'error': '模型不存在'}
# 检查API配置 # 检查API配置
api_url = model.get('apiUrl', '').strip() api_url = model.get('apiUrl', '').strip()
api_key = model.get('apiKey', '').strip() api_key = model.get('apiKey', '').strip()
version = model.get('version', '').strip() version = model.get('version', '').strip()
provider = model.get('provider', '').strip()
# 调试日志 # 记录调试信息
logger.info(f"Model {model.get('name')} config", extra={ model_name = str(model.get('name', 'unknown')) if model.get('name') is not None else 'unknown'
'provider': provider, logger.info(f"测试模型: {model_name}")
'api_url': api_url, logger.info(f"API地址: {api_url}")
'version': version, logger.info(f"模型版本: {version}")
'has_api_key': bool(api_key), logger.info(f"API密钥长度: {len(api_key) if api_key else 0}")
'api_key_length': len(api_key) if api_key else 0
})
if not api_url: if not api_url:
raise HTTPException(status_code=400, detail="模型API地址未配置") return {'success': False, 'error': 'API地址未配置'}
if not version: if not version:
raise HTTPException(status_code=400, detail="模型版本未配置") return {'success': False, 'error': '模型版本未配置'}
# 准备请求数据 # 准备请求数据 - 标准的OpenAI格式
request_data = { messages = []
"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)
}
# 添加系统提示词 # 添加系统提示词
if system_prompt: if system_prompt:
request_data["messages"].append({ messages.append({"role": "system", "content": system_prompt})
"role": "system",
"content": system_prompt
})
elif model.get('systemPrompt'): elif model.get('systemPrompt'):
request_data["messages"].append({ system_prompt_content = model.get('systemPrompt')
"role": "system", if system_prompt_content:
"content": model.get('systemPrompt') messages.append({"role": "system", "content": system_prompt_content})
})
# 添加用户提示词 # 添加用户提示词
request_data["messages"].append({ messages.append({"role": "user", "content": prompt})
"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 = { headers = {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
# 添加API密钥(如果提供) # 只有在有API密钥时才添加认证头
if api_key: if api_key:
provider_lower = provider.lower() headers["Authorization"] = f"Bearer {api_key}"
# 根据提供商类型设置认证头 try:
if provider_lower == 'anthropic': # 发送API请求 - 只支持OpenAI兼容格式
# Anthropic API timeout = httpx.Timeout(30)
headers["x-api-key"] = api_key async with httpx.AsyncClient(timeout=timeout) as client:
headers["anthropic-version"] = "2023-06-01" # 记录请求信息隐藏API密钥
logger.info(f"设置Anthropic认证头x-api-key长度: {len(api_key)}") masked_headers = {k: (v[:10] + '...' if k == 'Authorization' and len(v) > 10 else v) for k, v in headers.items()}
else: logger.info(f"发送请求到: {api_url.rstrip('/')}/chat/completions")
# OpenAI 和其他兼容的API包括自定义 logger.info(f"请求头: {masked_headers}")
headers["Authorization"] = f"Bearer {api_key}" logger.info(f"请求体: {request_data}")
logger.info(f"设置Bearer认证头key长度: {len(api_key)}")
# 记录最终使用的请求头隐藏API key response = await client.post(
logger.info(f"最终请求头", extra={ f"{api_url.rstrip('/')}/chat/completions",
'headers': {k: v if k != 'Authorization' and k != 'x-api-key' else '***HIDDEN***' for k, v in headers.items()}, headers=headers,
'api_key_masked': f"{api_key[:4]}...{api_key[-4:]}" if len(api_key) > 8 else '***' json=request_data
}) )
else:
logger.warning(f"未提供API key for model {model.get('name')}")
# 记录最终使用的请求头
logger.info(f"最终请求头", extra={
'headers': headers,
'api_key': 'NONE'
})
# 获取重试配置 # 检查响应
max_retries = model.get('maxRetries', 3) logger.info(f"响应状态码: {response.status_code}")
retry_delay = 2 # 重试间隔2秒 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): if not content:
try: logger.warning("API返回的内容为空")
# 发送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 messageAnthropic使用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
}
return { return {
'success': False, 'success': True,
'error': '无法解析API响应', 'model': str(model.get('name', 'unknown')),
'content': content,
'usage': result.get('usage', {}),
'raw_response': result 'raw_response': result
} }
else: else:
error_msg = f"API调用失败 (状态码: {response.status_code})" logger.error(f"API响应格式异常: {result}")
try: return {'success': False, 'error': 'API响应格式异常'}
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
else: 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: return {
last_error = { 'success': False,
'success': False, 'error': f'API调用失败 ({response.status_code}): {error_detail_str}'
'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
except Exception as e: except Exception as e:
last_error = { error_str = str(e) if e is not None else '未知异常'
'success': False, return {'success': False, 'error': f'调用异常: {error_str}'}
'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
@staticmethod @staticmethod
def test_model(model_id: str) -> Dict[str, Any]: def test_model(model_id: str) -> Dict[str, Any]:
@@ -530,7 +332,8 @@ class ModelManager:
message = '连接测试成功' message = '连接测试成功'
else: else:
model['status'] = '连接失败' 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']}) ModelManager.update_model(model_id, {'status': model['status']})
@@ -656,7 +459,8 @@ class ModelsAPI(BaseAPI):
message = '连接测试成功' message = '连接测试成功'
else: else:
model['status'] = '连接失败' 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']}) ModelManager.update_model(model_id, {'status': model['status']})
@@ -678,157 +482,28 @@ class ModelsAPI(BaseAPI):
@post("/{model_id}/call") @post("/{model_id}/call")
async def call_model_api(self, model_id: str, request_data: Dict[str, Any] = Body(...)): async def call_model_api(self, model_id: str, request_data: Dict[str, Any] = Body(...)):
"""调用模型进行对话(支持流式输出""" """调用模型进行对话(使用非流式响应"""
try: try:
# 获取请求参数 # 获取请求参数
prompt = request_data.get('prompt', '') prompt = request_data.get('prompt', '')
system_prompt = request_data.get('systemPrompt', None) system_prompt = request_data.get('systemPrompt', None)
stream_enabled = request_data.get('stream', False)
if not prompt: if not prompt:
raise HTTPException(status_code=400, detail="提示词不能为空") raise HTTPException(status_code=400, detail="提示词不能为空")
# 如果启用流式,返回流式响应 # 强制使用非流式调用
if stream_enabled: result = await ModelManager.call_model(
return StreamingResponse( model_id=model_id,
self._stream_data(model_id, prompt, system_prompt), prompt=prompt,
media_type="text/plain", system_prompt=system_prompt
headers={ )
"Cache-Control": "no-cache", return self.success(result, "模型调用成功" if result.get('success') else "模型调用失败")
"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 "模型调用失败")
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
self.logger.error(f"调用模型失败: {str(e)}") self.logger.error(f"调用模型失败: {str(e)}")
raise HTTPException(status_code=500, detail=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") @post("/batch-delete")
async def batch_delete_models(self, model_ids: List[str] = Body(...)): async def batch_delete_models(self, model_ids: List[str] = Body(...)):
"""批量删除模型""" """批量删除模型"""

View File

@@ -1,7 +1,7 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from typing import Optional from typing import Optional
import os import os
from pathlib import Path import json
class Settings(BaseSettings): class Settings(BaseSettings):
@@ -12,21 +12,21 @@ class Settings(BaseSettings):
# 服务器配置 # 服务器配置
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 1112 port: int = 3000 # 默认使用config.json中的端口
workers: int = 1 workers: int = 1
# 日志配置 # 日志配置
log_level: str = "INFO" log_level: str = "INFO"
log_file: Optional[str] = "logs/app.log" log_file: Optional[str] = "logs/app.log" # 默认写入日志文件
log_format: str = "json" log_format: str = "json" # json 或 console
log_to_console: bool = False log_to_console: bool = False # 是否同时输出到控制台
# 高级日志配置 # 高级日志配置
advanced_logging: bool = True advanced_logging: bool = True # 是否启用高级日志系统
logs_dir: str = "logs" logs_dir: str = "logs" # 日志目录
max_log_days: int = 30 max_log_days: int = 30 # 日志文件保存天数
enable_log_cleanup: bool = True enable_log_cleanup: bool = True # 是否启用自动日志清理
route_based_logging: bool = True route_based_logging: bool = True # 是否启用基于路由的日志分类
# 性能配置 # 性能配置
max_requests: int = 1000 max_requests: int = 1000
@@ -39,10 +39,35 @@ class Settings(BaseSettings):
cors_headers: list[str] = ["*"] cors_headers: list[str] = ["*"]
class Config: class Config:
env_file = str(Path(__file__).parent.parent.parent / ".env") env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False 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() settings = Settings()

View File

@@ -224,7 +224,7 @@ def setup_routes(app: FastAPI):
@app.get("/routes") @app.get("/routes")
async def get_routes_info(): async def get_routes_info():
"""获取所有路由信息""" """获取所有路由信息"""
from ..api.discovery import get_registered_modules_info from ..api.internal.discovery import get_registered_modules_info
response_data = { response_data = {
"app_routes": [ "app_routes": [
@@ -246,6 +246,64 @@ def setup_routes(app: FastAPI):
registration_result = auto_register_routes(app) 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 from fastapi.responses import FileResponse
@@ -289,3 +347,6 @@ def get_app() -> FastAPI:
if _app_instance is None: if _app_instance is None:
_app_instance = create_app() _app_instance = create_app()
return _app_instance return _app_instance
# 导出应用实例
app = get_app()

View File

@@ -99,10 +99,84 @@ class FileUploadService:
self.max_file_size = 100 * 1024 * 1024 # 100MB 默认最大文件大小 self.max_file_size = 100 * 1024 * 1024 # 100MB 默认最大文件大小
self.allowed_extensions = None # None表示允许所有扩展名 self.allowed_extensions = None # None表示允许所有扩展名
# 初始化时加载已存在的文件信息
self._load_existing_files()
def _generate_file_id(self) -> str: def _generate_file_id(self) -> str:
"""生成唯一文件ID""" """生成唯一文件ID"""
return str(uuid.uuid4()) 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: def _generate_storage_filename(self, original_filename: str, file_id: str) -> str:
"""生成存储文件名""" """生成存储文件名"""
# 获取文件扩展名 # 获取文件扩展名

10
request/start-backend.bat Normal file
View 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

View File

@@ -1 +1 @@
0bbf8a1a0807bf0cee9cf3cc5634f318 58852b1a007ac2ceb1f790a423c070f2

View File

@@ -854,20 +854,8 @@
"Datasets" "Datasets"
], ],
"summary": "list_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", "operationId": "list_datasets_datasets_get",
"parameters": [
{
"name": "list_all",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": false,
"title": "List All"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "Successful Response", "description": "Successful Response",
@@ -878,16 +866,6 @@
} }
} }
} }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
} }
} }
} }
@@ -984,7 +962,7 @@
"Models" "Models"
], ],
"summary": "call_model_api", "summary": "call_model_api",
"description": "调用模型进行对话(支持流式输出", "description": "调用模型进行对话(强制使用非流式响应",
"operationId": "call_model_api_models__model_id__call_post", "operationId": "call_model_api_models__model_id__call_post",
"parameters": [ "parameters": [
{ {
@@ -1285,7 +1263,7 @@
"Models" "Models"
], ],
"summary": "test_model_connection", "summary": "test_model_connection",
"description": "测试模型连接", "description": "测试模型连接 - 禁用流式响应",
"operationId": "test_model_connection_models__model_id__test_post", "operationId": "test_model_connection_models__model_id__test_post",
"parameters": [ "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": { "components": {

View 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
View 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>

View File

View File

@@ -34,85 +34,6 @@
} }
} }
</script> </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"> <style type="text/tailwindcss">
@layer utilities { @layer utilities {
.gradient-blue { .gradient-blue {
@@ -515,7 +436,7 @@
</div> </div>
<!-- 模型选择区 --> <!-- 模型选择区 -->
<div class="grid-1x4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<!-- 模型A --> <!-- 模型A -->
<div class="bg-gray-50 rounded-xl p-3 border-2 border-dashboard-primary/20"> <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"> <h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
@@ -559,50 +480,6 @@
</div> </div>
</div> </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> </div>
<!-- 测试输入区 --> <!-- 测试输入区 -->
@@ -653,7 +530,7 @@
<span class="font-semibold">显示配置</span> <span class="font-semibold">显示配置</span>
</button> </button>
<div id="comparisonResults" class="hidden"> <div id="comparisonResults" class="hidden">
<div class="grid-2x2"> <div class="grid grid-cols-2 gap-4">
<!-- 模型A输出 --> <!-- 模型A输出 -->
<div class="bg-white rounded-xl p-4 border border-gray-200"> <div class="bg-white rounded-xl p-4 border border-gray-200">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
@@ -707,59 +584,18 @@
<div>Token数: <span id="modelBTokens">-</span></div> <div>Token数: <span id="modelBTokens">-</span></div>
</div> </div>
</div> </div>
</div>
<!-- 模型C输出 --> <!-- 对比分析 -->
<div class="bg-white rounded-xl p-4 border border-gray-200 hidden" id="modelCOutputCard"> <div class="mt-4 bg-white rounded-xl p-4 border border-gray-200">
<div class="flex items-center justify-between mb-4"> <h3 class="text-lg font-semibold text-dashboard-text mb-3 flex items-center">
<h3 class="text-lg font-semibold text-dashboard-text flex items-center"> <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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> <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>
<span id="modelCName">模型C</span> </svg>
</h3> 对比分析
<div class="flex space-x-2"> </h3>
<button onclick="copyToClipboard('modelCOutput')" class="text-dashboard-textLight hover:text-dashboard-primary"> <div id="comparisonAnalysis" class="space-y-3">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <div class="text-dashboard-textLight">对比结果将在这里显示...</div>
<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> </div>
</div> </div>
</div> </div>
@@ -2046,9 +1882,7 @@
let comparisonModels = { let comparisonModels = {
modelA: null, modelA: null,
modelB: null, modelB: null
modelC: null,
modelD: null
}; };
// 初始化模型对比页面 // 初始化模型对比页面
@@ -2087,16 +1921,12 @@
function loadComparisonModelList() { function loadComparisonModelList() {
const modelASelect = document.getElementById('modelASelect'); const modelASelect = document.getElementById('modelASelect');
const modelBSelect = document.getElementById('modelBSelect'); const modelBSelect = document.getElementById('modelBSelect');
const modelCSelect = document.getElementById('modelCSelect');
const modelDSelect = document.getElementById('modelDSelect');
if (!modelASelect || !modelBSelect) return; if (!modelASelect || !modelBSelect) return;
// 清空现有选项 // 清空现有选项
modelASelect.innerHTML = '<option value="">请选择模型A</option>'; modelASelect.innerHTML = '<option value="">请选择模型A</option>';
modelBSelect.innerHTML = '<option value="">请选择模型B</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) => { modelConfigs.forEach((model, index) => {
@@ -2109,20 +1939,6 @@
optionB.value = index; optionB.value = index;
optionB.textContent = `${model.name} (${getProviderName(model.provider)})`; optionB.textContent = `${model.name} (${getProviderName(model.provider)})`;
modelBSelect.appendChild(optionB); 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() { function setupComparisonEventListeners() {
const modelASelect = document.getElementById('modelASelect'); const modelASelect = document.getElementById('modelASelect');
const modelBSelect = document.getElementById('modelBSelect'); const modelBSelect = document.getElementById('modelBSelect');
const modelCSelect = document.getElementById('modelCSelect');
const modelDSelect = document.getElementById('modelDSelect');
const testTemperature = document.getElementById('testTemperature'); const testTemperature = document.getElementById('testTemperature');
if (modelASelect) { if (modelASelect) {
@@ -2142,14 +1956,6 @@
modelBSelect.onchange = () => handleModelSelection('B', modelBSelect.value); modelBSelect.onchange = () => handleModelSelection('B', modelBSelect.value);
} }
if (modelCSelect) {
modelCSelect.onchange = () => handleModelSelection('C', modelCSelect.value);
}
if (modelDSelect) {
modelDSelect.onchange = () => handleModelSelection('D', modelDSelect.value);
}
if (testTemperature) { if (testTemperature) {
testTemperature.oninput = (e) => { testTemperature.oninput = (e) => {
document.getElementById('tempDisplay').textContent = e.target.value; document.getElementById('tempDisplay').textContent = e.target.value;
@@ -2202,27 +2008,22 @@
function checkModelSelection() { function checkModelSelection() {
const modelASelect = document.getElementById('modelASelect'); const modelASelect = document.getElementById('modelASelect');
const modelBSelect = document.getElementById('modelBSelect'); const modelBSelect = document.getElementById('modelBSelect');
const modelCSelect = document.getElementById('modelCSelect');
const modelDSelect = document.getElementById('modelDSelect');
// 获取所有已选择的模型索引 if (modelASelect && modelBSelect) {
const selected = []; // 禁用已选择的模型
if (modelASelect && modelASelect.value) selected.push(modelASelect.value); const selectedA = modelASelect.value;
if (modelBSelect && modelBSelect.value) selected.push(modelBSelect.value); const selectedB = modelBSelect.value;
if (modelCSelect && modelCSelect.value) selected.push(modelCSelect.value);
if (modelDSelect && modelDSelect.value) selected.push(modelDSelect.value);
// 为每个下拉框禁用已选择的模型 Array.from(modelASelect.options).forEach((option, index) => {
const selects = [modelASelect, modelBSelect, modelCSelect, modelDSelect];
selects.forEach(select => {
if (!select) return;
Array.from(select.options).forEach((option, index) => {
if (index === 0) return; // 跳过第一个选项 if (index === 0) return; // 跳过第一个选项
const optionValue = index.toString(); option.disabled = index.toString() === selectedB;
// 如果该选项在其他下拉框中被选中,则禁用
option.disabled = selected.includes(optionValue);
}); });
});
Array.from(modelBSelect.options).forEach((option, index) => {
if (index === 0) return; // 跳过第一个选项
option.disabled = index.toString() === selectedA;
});
}
} }
// 折叠配置区 // 折叠配置区
@@ -2322,22 +2123,15 @@
// 运行模型对比 // 运行模型对比
async function runModelComparison() { async function runModelComparison() {
const modelA = comparisonModels.modelA;
const modelB = comparisonModels.modelB;
const prompt = document.getElementById('testPrompt').value.trim(); const prompt = document.getElementById('testPrompt').value.trim();
const temperature = parseFloat(document.getElementById('testTemperature').value); const temperature = parseFloat(document.getElementById('testTemperature').value);
const maxTokens = parseInt(document.getElementById('testMaxTokens').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) { if (!modelA || !modelB) {
alert('请至少选择两个模型进行对比'); alert('请选择两个模型进行对比');
return; return;
} }
@@ -2353,37 +2147,22 @@
// 折叠配置区 // 折叠配置区
collapseConfigSection(); collapseConfigSection();
// 更新模型名称
document.getElementById('modelAName').textContent = modelA.name;
document.getElementById('modelBName').textContent = modelB.name;
// 重置输出区域 // 重置输出区域
resetOutputAreas(); resetOutputAreas();
// 显示已选择模型的流式输出状态,并显示/隐藏对应的卡片 // 显示流式输出状态
selectedModels.forEach(({ model, modelType }) => { const modelAOutput = document.getElementById('modelAOutput');
// 更新模型名称 const modelBOutput = document.getElementById('modelBOutput');
const nameElement = document.getElementById(`model${modelType}Name`);
if (nameElement) {
nameElement.textContent = model.name;
}
// 显示对应的输出卡片 if (modelAOutput) {
const outputCard = document.getElementById(`model${modelType}OutputCard`); modelAOutput.innerHTML = '<div class="text-dashboard-text"><span class="animate-pulse">正在生成中...</span></div>';
if (outputCard) { }
outputCard.classList.remove('hidden'); if (modelBOutput) {
} modelBOutput.innerHTML = '<div class="text-dashboard-text"><span class="animate-pulse">正在生成中...</span></div>';
// 显示流式输出状态
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');
}
} }
// 开始对比 // 开始对比
@@ -2393,44 +2172,51 @@
try { try {
// 创建流式显示回调 // 创建流式显示回调
const streamingCache = {}; const streamingCache = {
const onChunkCallbacks = {}; 'A': { output: '', responseTime: 0, streaming: false },
const modelPromises = []; 'B': { output: '', responseTime: 0, streaming: false }
};
// 为每个选择的模型创建回调和Promise const onChunkA = (chunk) => {
selectedModels.forEach(({ model, modelType }) => { streamingCache['A'].output = chunk.content;
streamingCache[modelType] = { output: '', responseTime: 0, streaming: false }; streamingCache['A'].responseTime = Date.now();
streamingCache['A'].streaming = true;
displayStreamingModelResult('A', streamingCache['A']);
};
onChunkCallbacks[modelType] = (chunk) => { const onChunkB = (chunk) => {
streamingCache[modelType].output = chunk.content; streamingCache['B'].output = chunk.content;
streamingCache[modelType].responseTime = Date.now(); streamingCache['B'].responseTime = Date.now();
streamingCache[modelType].streaming = true; streamingCache['B'].streaming = true;
displayStreamingModelResult(modelType, streamingCache[modelType]); displayStreamingModelResult('B', streamingCache['B']);
}; };
// 创建模型调用Promise // 并行运行两个模型(带超时控制)
const promise = withTimeout( const [resultA, resultB] = await Promise.allSettled([
callRealModel(model, prompt, temperature, maxTokens, model.streaming ? onChunkCallbacks[modelType] : null), withTimeout(callRealModel(modelA, prompt, temperature, maxTokens, modelA.streaming ? onChunkA : null), 60000), // 60秒超时
60000 withTimeout(callRealModel(modelB, prompt, temperature, maxTokens, modelB.streaming ? onChunkB : null), 60000) // 60秒超时
); ]);
modelPromises.push({ promise, modelType });
});
// 并行运行所有模型 // 处理模型A结果
const results = await Promise.allSettled(modelPromises.map(item => item.promise)); if (resultA.status === 'fulfilled') {
if (!resultA.value.streaming) {
// 处理所有模型结果 displayModelResult('A', resultA.value);
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);
} }
}); } 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) { } catch (error) {
console.error('对比过程中出错:', error); console.error('对比过程中出错:', error);
@@ -2474,23 +2260,18 @@
// 检查是否启用流式输出 // 检查是否启用流式输出
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type');
const isStreaming = contentType?.includes('text/plain'); const isStreaming = contentType?.includes('text/plain');
console.log('[流式调试] Content-Type:', contentType);
console.log('[流式调试] isStreaming:', isStreaming);
console.log('[流式调试] onChunk:', onChunk);
if (isStreaming) { if (isStreaming) {
// 流式处理 // 流式处理
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let fullContent = ''; let fullContent = '';
let chunkCount = 0;
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
const chunk = decoder.decode(value); const chunk = decoder.decode(value);
console.log('[流式调试] 收到数据块:', chunk);
const lines = chunk.split('\n'); const lines = chunk.split('\n');
for (const line of lines) { for (const line of lines) {
@@ -2500,14 +2281,11 @@
try { try {
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
console.log('[流式调试] 解析数据:', parsed);
if (parsed.content !== undefined) { if (parsed.content !== undefined) {
if (parsed.content) { if (parsed.content) {
fullContent += parsed.content; fullContent += parsed.content;
chunkCount++;
// 调用回调函数更新UI // 调用回调函数更新UI
if (onChunk) { if (onChunk) {
console.log('[流式调试] 调用回调函数, 当前内容长度:', fullContent.length);
onChunk({ onChunk({
content: fullContent, content: fullContent,
delta: parsed.content, delta: parsed.content,
@@ -2516,12 +2294,11 @@
} }
} }
if (parsed.done) { if (parsed.done) {
console.log('[流式调试] 流式输出完成, 总块数:', chunkCount);
break; break;
} }
} }
} catch (e) { } catch (e) {
console.error('[流式调试] 解析错误:', e); // 忽略解析错误
} }
} }
} }
@@ -2654,8 +2431,53 @@
tokensElement.textContent = '-'; 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) { function copyToClipboard(elementId) {

View File

@@ -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