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

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(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
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",
"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
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,27 +111,15 @@ 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
data_dir = file_upload_service.upload_dir
mapping_file = data_dir / "filename_mapping.json"
@@ -139,89 +127,43 @@ class DatasetAPI(BaseAPI):
# 读取文件名映射
mappings = {}
if mapping_file.exists():
try:
with open(mapping_file, 'r', encoding='utf-8') as f:
mapping_data = json.load(f)
mappings = mapping_data.get("mappings", {})
except Exception:
mappings = {}
# 获取data目录下的所有JSON文件
# 根据映射文件构建数据集列表
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
# 从映射文件获取真实文件名
mapping_info = mappings.get(file_id, {})
original_filename = mapping_info.get("original_filename", file_path.name)
uploaded_at = mapping_info.get("uploaded_at", "")
for file_id, mapping_info in mappings.items():
# 获取物理文件信息
storage_filename = mapping_info.get("storage_filename", f"{file_id}.json")
file_path = data_dir / storage_filename
if file_path.exists():
# 获取文件大小
file_size = file_path.stat().st_size
# 格式化文件大小
size_mb = round(file_size / 1024 / 1024, 2)
size_display = format_file_size(file_size)
datasets.append({
"file_id": file_id,
"name": original_filename,
"name": mapping_info.get("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,
"description": mapping_info.get("original_filename", ""),
"uploaded_at": mapping_info.get("uploaded_at", ""),
"download_count": 0,
"is_physical_file": True
})
# 按文件名排序
datasets.sort(key=lambda x: x["name"])
# 按上传时间排序
datasets.sort(key=lambda x: x["uploaded_at"], reverse=True)
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"
"source": "filename_mapping"
})
except Exception as e:
@@ -273,52 +215,6 @@ class DatasetAPI(BaseAPI):
except Exception as e:
return StandardResponse.error(f"获取数据集详情失败: {str(e)}")
@get("/{file_id}", response_model=StandardResponse)
async def get_dataset(self, file_id: str):
"""
获取特定数据集的详细信息
Args:
file_id: 文件ID
Returns:
StandardResponse: 包含数据集详情的标准响应
"""
try:
file_info = file_upload_service.get_file(file_id)
if not file_info:
return StandardResponse.error(f"数据集 {file_id} 不存在")
# 转换为前端期望的格式
# 显示真实文件名(从映射文件中获取)
mapping = file_upload_service.get_filename_mapping(file_info.file_id)
display_name = mapping["original_filename"] if mapping else file_info.original_filename
# 格式化文件大小
size_mb = round(file_info.file_size / 1024 / 1024, 2)
size_display = format_file_size(file_info.file_size)
dataset_info = {
"file_id": file_info.file_id,
"name": display_name,
"size": file_info.file_size,
"size_mb": size_mb,
"size_display": size_display,
"status": "已处理",
"description": file_info.description or "",
"uploaded_at": file_info.uploaded_at,
"updated_at": file_info.updated_at,
"download_count": file_info.download_count,
"content_type": file_info.content_type,
"file_hash": file_info.file_hash
}
return StandardResponse.success(dataset_info)
except Exception as e:
return StandardResponse.error(f"获取数据集详情失败: {str(e)}")
@get("/{file_id}/content", response_model=StandardResponse)
async def get_dataset_content(self, file_id: str, limit: int = 5):
"""

View File

@@ -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,137 +150,72 @@ 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()
# 根据提供商类型设置认证头
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)}")
# 记录最终使用的请求头隐藏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'
})
# 获取重试配置
max_retries = model.get('maxRetries', 3)
retry_delay = 2 # 重试间隔2秒
last_error = None
# 重试循环
for attempt in range(max_retries + 1):
try:
# 发送API请求
timeout = httpx.Timeout(model.get('timeout', 30))
provider = model.get('provider', '').lower()
# 发送API请求 - 只支持OpenAI兼容格式
timeout = httpx.Timeout(30)
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
})
# 记录请求信息隐藏API密钥
masked_headers = {k: (v[:10] + '...' if k == 'Authorization' and len(v) > 10 else v) for k, v in headers.items()}
logger.info(f"发送请求到: {api_url.rstrip('/')}/chat/completions")
logger.info(f"请求头: {masked_headers}")
logger.info(f"请求体: {request_data}")
response = await client.post(
f"{api_url.rstrip('/')}/chat/completions",
@@ -365,107 +223,51 @@ class ModelManager:
json=request_data
)
# 处理流式响应
if request_data.get('stream', False):
return await handle_streaming_response(response, model, attempt + 1, max_retries + 1)
# 检查响应状态(非流式)
# 检查响应
logger.info(f"响应状态码: {response.status_code}")
if response.status_code == 200:
result = response.json()
logger.info(f"响应内容: {result}")
# 解析响应 - 根据提供商类型
provider = model.get('provider', '').lower()
# 解析响应
if "choices" in result and len(result["choices"]) > 0:
message = result["choices"][0]["message"]
content = message.get("content", "") or ""
logger.info(f"原始响应内容: {message}")
logger.info(f"解析成功,收到回复长度: {len(content)}")
# 如果内容为空,记录警告但仍然返回成功
if not content:
logger.warning("API返回的内容为空")
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'],
'model': str(model.get('name', 'unknown')),
'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 {
'success': False,
'error': '无法解析API响应',
'raw_response': result
}
else:
error_msg = f"API调用失败 (状态码: {response.status_code})"
logger.error(f"API响应格式异常: {result}")
return {'success': False, 'error': 'API响应格式异常'}
else:
# 获取错误信息
try:
error_detail = response.json()
error_msg += f": {error_detail}"
error_detail_str = str(error_detail) if error_detail is not None else '无错误详情'
logger.error(f"API错误响应: {error_detail}")
except:
error_msg += f": {response.text}"
error_detail_str = response.text or '未知错误'
logger.error(f"API错误响应文本: {error_detail_str}")
last_error = {
return {
'success': False,
'error': error_msg,
'status_code': response.status_code
'error': f'API调用失败 ({response.status_code}): {error_detail_str}'
}
# 如果不是最后一次尝试,等待后重试
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:
return last_error
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
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
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,29 +482,16 @@ 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,
@@ -713,122 +504,6 @@ class ModelsAPI(BaseAPI):
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(...)):
"""批量删除模型"""

View File

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

View File

@@ -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
@@ -289,3 +347,6 @@ def get_app() -> FastAPI:
if _app_instance is None:
_app_instance = create_app()
return _app_instance
# 导出应用实例
app = get_app()

View File

@@ -99,10 +99,84 @@ class FileUploadService:
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
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"
],
"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": {

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>
<!-- 网格布局样式 -->
<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>
<!-- 模型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>
<!-- 对比分析 -->
<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>
</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>
对比分析
</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,28 +2008,23 @@
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;
});
}
}
// 折叠配置区
function collapseConfigSection() {
@@ -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 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>';
}
});
const modelAOutput = document.getElementById('modelAOutput');
const modelBOutput = document.getElementById('modelBOutput');
// 隐藏未选择模型的输出卡片
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 = [];
// 为每个选择的模型创建回调和Promise
selectedModels.forEach(({ model, modelType }) => {
streamingCache[modelType] = { output: '', responseTime: 0, streaming: false };
onChunkCallbacks[modelType] = (chunk) => {
streamingCache[modelType].output = chunk.content;
streamingCache[modelType].responseTime = Date.now();
streamingCache[modelType].streaming = true;
displayStreamingModelResult(modelType, streamingCache[modelType]);
const streamingCache = {
'A': { output: '', responseTime: 0, streaming: false },
'B': { output: '', responseTime: 0, streaming: false }
};
// 创建模型调用Promise
const promise = withTimeout(
callRealModel(model, prompt, temperature, maxTokens, model.streaming ? onChunkCallbacks[modelType] : null),
60000
);
modelPromises.push({ promise, modelType });
});
const onChunkA = (chunk) => {
streamingCache['A'].output = chunk.content;
streamingCache['A'].responseTime = Date.now();
streamingCache['A'].streaming = true;
displayStreamingModelResult('A', streamingCache['A']);
};
// 并行运行所有模型
const results = await Promise.allSettled(modelPromises.map(item => item.promise));
const onChunkB = (chunk) => {
streamingCache['B'].output = chunk.content;
streamingCache['B'].responseTime = Date.now();
streamingCache['B'].streaming = true;
displayStreamingModelResult('B', streamingCache['B']);
};
// 处理所有模型结果
results.forEach((result, index) => {
const modelType = modelPromises[index].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秒超时
]);
if (result.status === 'fulfilled') {
if (!result.value.streaming) {
displayModelResult(modelType, result.value);
// 处理模型A结果
if (resultA.status === 'fulfilled') {
if (!resultA.value.streaming) {
displayModelResult('A', resultA.value);
}
} else {
displayModelError(modelType, result.reason);
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) {

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