feat: 新增Python服务器。准备逐步替换node服务器
This commit is contained in:
parent
a9b31f540f
commit
485b07c12f
|
|
@ -13,6 +13,8 @@ dist-ssr
|
|||
*.local
|
||||
uploads
|
||||
.env
|
||||
.venv
|
||||
__pycache__
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
# Python AI Chat Server
|
||||
|
||||
这是原有Node.js服务器的Python替代版本,使用FastAPI和DashScope Python SDK连接阿里云百炼平台API。
|
||||
|
||||
## 特性
|
||||
|
||||
- 基于FastAPI的高性能异步服务器
|
||||
- 支持流式和非流式对话
|
||||
- 完全兼容前端API端点
|
||||
- 支持多模态输入(文本+图像)
|
||||
- 集成阿里云百炼API
|
||||
- 文件上传功能
|
||||
- 对话历史管理
|
||||
|
||||
## 安装要求
|
||||
|
||||
- Python 3.8+
|
||||
- pip包管理器
|
||||
|
||||
## 安装步骤
|
||||
|
||||
1. **克隆或复制代码**
|
||||
|
||||
2. **安装Python依赖**:
|
||||
```bash
|
||||
cd server_python
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **配置环境变量**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑`.env`文件,填入您的阿里云百炼API密钥:
|
||||
```
|
||||
ALIYUN_API_KEY=your_actual_api_key_here
|
||||
```
|
||||
|
||||
## 启动服务器
|
||||
|
||||
### 方法一:直接运行
|
||||
```bash
|
||||
python run_server.py
|
||||
```
|
||||
|
||||
### 方法二:使用uvicorn
|
||||
```bash
|
||||
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
## API端点
|
||||
|
||||
服务器提供了与原Node.js版本完全相同的API端点:
|
||||
|
||||
- `POST /api/chat-ui/chat` - 聊天接口(支持流式和非流式)
|
||||
- `GET /api/chat-ui/models` - 获取模型列表
|
||||
- `GET /api/chat-ui/conversations` - 获取所有对话
|
||||
- `GET /api/chat-ui/conversations/{id}` - 获取特定对话
|
||||
- `POST /api/chat-ui/conversations` - 保存/更新对话
|
||||
- `DELETE /api/chat-ui/conversations/{id}` - 删除对话
|
||||
- `POST /api/chat-ui/upload` - 文件上传
|
||||
- `POST /api/chat-ui/stop` - 停止生成
|
||||
- `POST /api/chat-ui/stop/{id}` - 按ID停止生成
|
||||
- `GET /health` - 健康检查
|
||||
|
||||
## 前端配置
|
||||
|
||||
修改Vite配置(`vite.config.ts`)中的代理目标:
|
||||
|
||||
```typescript
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/chat-ui": {
|
||||
target: "http://localhost:8000", // 修改为Python服务器端口
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
## 环境变量
|
||||
|
||||
- `ALIYUN_API_KEY`: 阿里云百炼API密钥(必填)
|
||||
- `PORT`: 服务器端口(默认8000)
|
||||
|
||||
## 依赖说明
|
||||
|
||||
- `fastapi`: 现代高性能web框架
|
||||
- `uvicorn`: ASGI服务器
|
||||
- `dashscope`: 阿里云百炼SDK
|
||||
- `python-multipart`: 处理文件上传
|
||||
- `python-dotenv`: 环境变量管理
|
||||
|
||||
## 错误排查
|
||||
|
||||
1. **API密钥错误**: 确保在`.env`文件中正确设置了`ALIYUN_API_KEY`
|
||||
2. **端口冲突**: 检查8000端口是否被占用,可以修改`.env`中的`PORT`变量
|
||||
3. **依赖问题**: 确保已正确安装所有依赖
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Python服务器提供了与Node.js服务器相同的功能和API接口
|
||||
- 保留了原有的日志记录机制
|
||||
- 对话数据仍存储在内存中,生产环境建议使用数据库
|
||||
- 支持与原前端应用无缝集成
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
"""
|
||||
改进版Python FastAPI服务器实现,使用DashScope Python SDK连接阿里云百炼平台API
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Any
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
import dashscope
|
||||
from dashscope import Generation
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
import uvicorn
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 设置 DashScope API 密钥
|
||||
api_key = os.getenv("ALIYUN_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("请在环境变量中设置 ALIYUN_API_KEY")
|
||||
|
||||
dashscope.api_key = api_key
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(title="AI Chat API Server (Python)", version="2.0.0")
|
||||
|
||||
# 数据模型定义
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
images: Optional[List[str]] = None
|
||||
files: Optional[List[str]] = None
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
model: str = "qwen-plus"
|
||||
messages: List[Dict[str, Any]]
|
||||
stream: bool = True
|
||||
temperature: Optional[float] = 0.7
|
||||
max_tokens: Optional[int] = 2000
|
||||
|
||||
class ModelInfo(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
maxTokens: int
|
||||
provider: str
|
||||
|
||||
# 模拟数据库 - 实际应用中应使用持久化存储
|
||||
conversations_db: Dict[str, dict] = {}
|
||||
|
||||
# 配置上传目录
|
||||
upload_dir = Path("uploads")
|
||||
upload_dir.mkdir(exist_ok=True)
|
||||
|
||||
@app.middleware("http")
|
||||
async def logging_middleware(request: Request, call_next):
|
||||
"""中间件:记录请求日志"""
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
# 记录请求信息
|
||||
print(f"[INFO] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - "
|
||||
f"HTTP {request.method} {request.url.path} - "
|
||||
f"IP: {request.client.host if request.client else 'unknown'}")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
process_time = (datetime.utcnow() - start_time).total_seconds() * 1000
|
||||
|
||||
# 记录响应信息
|
||||
print(f"[INFO] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - "
|
||||
f"Response {response.status_code}, Process Time: {process_time:.2f}ms")
|
||||
|
||||
# 在响应头中添加处理时间
|
||||
response.headers["X-Process-Time"] = f"{process_time:.2f}ms"
|
||||
|
||||
return response
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
|
||||
|
||||
@app.post("/api/chat-ui/chat")
|
||||
async def chat_endpoint(request: Request):
|
||||
"""
|
||||
聊天接口 - 与阿里云百炼API兼容的接口
|
||||
这个端点会接收前端的聊天请求并转发到阿里云百炼API
|
||||
"""
|
||||
try:
|
||||
# 获取请求体数据
|
||||
body = await request.json()
|
||||
|
||||
# 检查请求格式并适配
|
||||
# 如果是OpenAI兼容格式 (来自streamChat)
|
||||
if 'messages' in body:
|
||||
messages = body.get('messages', [])
|
||||
model = body.get('model', 'qwen-plus')
|
||||
stream = body.get('stream', True)
|
||||
temperature = body.get('temperature', 0.7)
|
||||
max_tokens = body.get('max_tokens', 2000)
|
||||
else:
|
||||
# 否则是前端简化格式 (来自chat函数)
|
||||
# 需要将其转换为OpenAI兼容格式
|
||||
message_text = body.get('message', '')
|
||||
|
||||
# 检查message是否已经是格式化的列表(带图片的情况)
|
||||
if isinstance(message_text, list):
|
||||
user_content = message_text
|
||||
else:
|
||||
# 如果是字符串,转换为标准格式
|
||||
user_content = [{"type": "text", "text": message_text}]
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": body.get('systemPrompt', '你是一个支持视觉理解的助手。')},
|
||||
{"role": "user", "content": user_content}
|
||||
]
|
||||
model = body.get('model', 'qwen-plus')
|
||||
stream = body.get('stream', False) # 默认为非流式
|
||||
temperature = body.get('temperature', 0.7)
|
||||
max_tokens = body.get('maxTokens', 2000)
|
||||
|
||||
if stream:
|
||||
# 流式响应
|
||||
async def event_generator():
|
||||
try:
|
||||
responses = Generation.call(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=True,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
for idx, response in enumerate(responses):
|
||||
if response.status_code == 200:
|
||||
# 检查响应是否包含预期的内容
|
||||
# DashScope API的响应结构可能是 output.choices 或 output.text
|
||||
content = None
|
||||
|
||||
# 尝试从 output.choices 获取内容
|
||||
if (hasattr(response, 'output') and
|
||||
response.output and
|
||||
hasattr(response.output, 'choices') and
|
||||
response.output.choices is not None and
|
||||
len(response.output.choices) > 0 and
|
||||
'message' in response.output.choices[0] and
|
||||
'content' in response.output.choices[0]['message']):
|
||||
|
||||
content = response.output.choices[0]['message']['content']
|
||||
# 否则尝试从 output.text 获取内容(DashScope特定格式)
|
||||
elif (hasattr(response, 'output') and
|
||||
response.output and
|
||||
'text' in response.output):
|
||||
|
||||
content = response.output.get('text')
|
||||
|
||||
if content:
|
||||
# 构建 SSE 数据块
|
||||
data = {
|
||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(datetime.utcnow().timestamp()),
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {"content": content} if content else {},
|
||||
"finish_reason": None
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
else:
|
||||
# 如果响应中没有内容,跳过
|
||||
continue
|
||||
else:
|
||||
# 错误处理
|
||||
error_data = {
|
||||
"error": {
|
||||
"message": f"API Error: {response.code} - {response.message}",
|
||||
"type": "api_error",
|
||||
"param": None,
|
||||
"code": response.code
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
break
|
||||
|
||||
# 发送结束信号
|
||||
finish_data = {
|
||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": int(datetime.utcnow().timestamp()),
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {},
|
||||
"finish_reason": "stop"
|
||||
}
|
||||
]
|
||||
}
|
||||
yield f"data: {json.dumps(finish_data)}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
error_data = {
|
||||
"error": {
|
||||
"message": str(e),
|
||||
"type": "server_error"
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
else:
|
||||
# 非流式响应
|
||||
response = Generation.call(
|
||||
model=model,
|
||||
messages=messages,
|
||||
stream=False,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
# 检查响应是否包含预期的内容
|
||||
# DashScope API的响应结构可能是 output.choices 或 output.text
|
||||
content = None
|
||||
|
||||
# 尝试从 output.choices 获取内容
|
||||
if (hasattr(response, 'output') and
|
||||
response.output and
|
||||
hasattr(response.output, 'choices') and
|
||||
response.output.choices is not None and
|
||||
len(response.output.choices) > 0 and
|
||||
'message' in response.output.choices[0] and
|
||||
'content' in response.output.choices[0]['message']):
|
||||
|
||||
content = response.output.choices[0]['message']['content']
|
||||
# 否则尝试从 output.text 获取内容(DashScope特定格式)
|
||||
elif (hasattr(response, 'output') and
|
||||
response.output and
|
||||
'text' in response.output):
|
||||
|
||||
content = response.output.get('text')
|
||||
|
||||
if content:
|
||||
# 构建前端期望的响应格式
|
||||
chat_response = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversationId": body.get('conversationId', str(uuid.uuid4())),
|
||||
"content": content,
|
||||
"model": model,
|
||||
"createdAt": int(datetime.utcnow().timestamp())
|
||||
}
|
||||
|
||||
if hasattr(response, 'usage') and response.usage:
|
||||
chat_response["usage"] = {
|
||||
"promptTokens": response.usage.input_tokens,
|
||||
"completionTokens": response.usage.output_tokens,
|
||||
"totalTokens": response.usage.total_tokens
|
||||
}
|
||||
|
||||
return JSONResponse(content=chat_response)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="API Response does not contain expected content"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"API Error: {response.code} - {response.message}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error in chat endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.get("/api/chat-ui/models")
|
||||
async def get_models():
|
||||
"""获取模型列表"""
|
||||
models = [
|
||||
ModelInfo(
|
||||
id="qwen-max",
|
||||
name="通义千问 Max",
|
||||
description="最强大的模型",
|
||||
maxTokens=8192,
|
||||
provider="Aliyun"
|
||||
),
|
||||
ModelInfo(
|
||||
id="qwen-plus",
|
||||
name="通义千问 Plus",
|
||||
description="能力均衡",
|
||||
maxTokens=8192,
|
||||
provider="Aliyun"
|
||||
),
|
||||
ModelInfo(
|
||||
id="qwen-turbo",
|
||||
name="通义千问 Turbo",
|
||||
description="速度更快、成本更低",
|
||||
maxTokens=8192,
|
||||
provider="Aliyun"
|
||||
)
|
||||
]
|
||||
return [model.dict() for model in models]
|
||||
|
||||
@app.get("/api/chat-ui/conversations")
|
||||
async def get_conversations():
|
||||
"""获取所有对话"""
|
||||
return list(conversations_db.values())
|
||||
|
||||
@app.get("/api/chat-ui/conversations/{conversation_id}")
|
||||
async def get_conversation(conversation_id: str):
|
||||
"""获取特定对话"""
|
||||
conversation = conversations_db.get(conversation_id)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
return conversation
|
||||
|
||||
@app.post("/api/chat-ui/conversations")
|
||||
async def save_conversation(request: Request):
|
||||
"""保存或更新对话"""
|
||||
try:
|
||||
data = await request.json()
|
||||
conversation_id = data.get('id') or str(uuid.uuid4())
|
||||
|
||||
conversation = {
|
||||
"id": conversation_id,
|
||||
"title": data.get('title', '新对话'),
|
||||
"messages": data.get('messages', []),
|
||||
"updatedAt": datetime.utcnow().isoformat(),
|
||||
"createdAt": data.get('createdAt', datetime.utcnow().isoformat())
|
||||
}
|
||||
|
||||
conversations_db[conversation_id] = conversation
|
||||
return conversation
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Error saving conversation: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.delete("/api/chat-ui/conversations/{conversation_id}")
|
||||
async def delete_conversation(conversation_id: str):
|
||||
"""删除对话"""
|
||||
if conversation_id in conversations_db:
|
||||
del conversations_db[conversation_id]
|
||||
return {"success": True, "message": "删除成功"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
@app.post("/api/chat-ui/upload")
|
||||
async def upload_file(file: UploadFile = File(...)):
|
||||
"""文件上传接口"""
|
||||
try:
|
||||
# 检查文件类型
|
||||
allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'text/plain', 'application/pdf']
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的文件类型: {file.content_type}")
|
||||
|
||||
# 生成唯一文件名
|
||||
file_extension = Path(file.filename).suffix.lower()
|
||||
unique_filename = f"{int(datetime.utcnow().timestamp())}_{uuid.uuid4()}{file_extension}"
|
||||
file_path = upload_dir / unique_filename
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
# 返回文件信息
|
||||
file_url = f"http://localhost:8000/uploads/{unique_filename}"
|
||||
result = {
|
||||
"url": file_url,
|
||||
"name": file.filename,
|
||||
"size": len(content),
|
||||
"mimeType": file.content_type
|
||||
}
|
||||
|
||||
print(f"[INFO] File uploaded: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Upload error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}")
|
||||
|
||||
@app.get("/uploads/{filename}")
|
||||
async def serve_upload(filename: str):
|
||||
"""提供上传文件的访问"""
|
||||
file_path = upload_dir / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(str(file_path))
|
||||
|
||||
@app.post("/api/chat-ui/stop")
|
||||
async def stop_generation():
|
||||
"""停止生成接口"""
|
||||
# 在实际实现中,这里可能需要维护正在运行的任务ID列表
|
||||
# 目前只是返回成功消息
|
||||
return {"success": True, "message": "已发出停止指令"}
|
||||
|
||||
@app.post("/api/chat-ui/stop/{message_id}")
|
||||
async def stop_generation_by_id(message_id: str):
|
||||
"""根据消息ID停止生成"""
|
||||
return {"success": True, "message": "已发出停止指令,消息ID: " + message_id}
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", 8000))
|
||||
print("="*50)
|
||||
print(f"Python AI Chat Server 启动中...")
|
||||
print(f"监听端口: {port}")
|
||||
print(f"API Key 状态: {'已配置' if api_key else '未配置'}")
|
||||
print("="*50)
|
||||
|
||||
if not api_key:
|
||||
print("警告: 未在环境变量中检测到 ALIYUN_API_KEY!")
|
||||
print("请在 .env 文件中添加您的百炼 API Key。")
|
||||
else:
|
||||
print("API Key 已检测到。")
|
||||
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
"""
|
||||
Python Flask/FastAPI 服务器实现,用于替代 Node.js 服务器
|
||||
使用 DashScope Python SDK 连接阿里云百炼平台 API
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
from pathlib import Path
|
||||
|
||||
import dashscope
|
||||
from dashscope import Generation
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, HTTPException, File, UploadFile, Form
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
import uvicorn
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
# 设置 DashScope API 密钥
|
||||
dashscope.api_key = os.getenv("ALIYUN_API_KEY")
|
||||
|
||||
# 创建 FastAPI 应用
|
||||
app = FastAPI(title="AI Chat API Server", version="1.0.0")
|
||||
|
||||
# 数据模型定义
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
images: Optional[List[str]] = None
|
||||
files: Optional[List[str]] = None
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
conversationId: Optional[str] = None
|
||||
message: str
|
||||
images: Optional[List[str]] = None
|
||||
files: Optional[List[str]] = None
|
||||
model: Optional[str] = "qwen-plus"
|
||||
temperature: Optional[float] = 0.7
|
||||
maxTokens: Optional[int] = 2000
|
||||
systemPrompt: Optional[str] = "你是一个支持视觉理解的助手。"
|
||||
stream: Optional[bool] = True
|
||||
# 扩展选项
|
||||
deepSearch: Optional[bool] = False
|
||||
webSearch: Optional[bool] = False
|
||||
deepThinking: Optional[bool] = False
|
||||
|
||||
class ModelInfo(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
maxTokens: int
|
||||
provider: str
|
||||
|
||||
# 模拟数据库 - 实际应用中应使用持久化存储
|
||||
conversations_db: Dict[str, dict] = {}
|
||||
|
||||
# 配置上传目录
|
||||
upload_dir = Path("uploads")
|
||||
upload_dir.mkdir(exist_ok=True)
|
||||
|
||||
@app.middleware("http")
|
||||
async def add_process_time_header(request, call_next):
|
||||
"""中间件:记录请求处理时间"""
|
||||
start_time = datetime.utcnow()
|
||||
response = await call_next(request)
|
||||
|
||||
# 计算处理时间
|
||||
process_time = (datetime.utcnow() - start_time).total_seconds() * 1000
|
||||
|
||||
# 在响应头中添加处理时间
|
||||
response.headers["X-Process-Time"] = f"{process_time:.2f}ms"
|
||||
|
||||
# 记录请求信息
|
||||
print(f"HTTP {request.method} {request.url.path} {response.status_code} {process_time:.2f}ms")
|
||||
|
||||
return response
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
|
||||
|
||||
@app.post("/api/chat-ui/chat")
|
||||
async def chat_endpoint(request: ChatRequest):
|
||||
"""聊天接口 - 处理普通请求"""
|
||||
try:
|
||||
# 构建消息数组,考虑是否包含图片
|
||||
user_content = []
|
||||
|
||||
# 添加用户消息文本
|
||||
user_content.append({"type": "text", "text": request.message})
|
||||
|
||||
# 如果有图片,则添加到内容中
|
||||
if request.images and len(request.images) > 0:
|
||||
for image_url in request.images:
|
||||
user_content.append({
|
||||
"type": "image_url",
|
||||
"image_url": image_url
|
||||
})
|
||||
|
||||
# 构建请求给百炼的消息列表
|
||||
messages = [
|
||||
{"role": "system", "content": request.systemPrompt},
|
||||
{"role": "user", "content": user_content}
|
||||
]
|
||||
|
||||
# 调用 DashScope API
|
||||
response = Generation.call(
|
||||
model=request.model,
|
||||
messages=messages,
|
||||
stream=False, # 非流式响应
|
||||
max_tokens=request.maxTokens,
|
||||
temperature=request.temperature
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
content = response.output.choices[0]['message']['content']
|
||||
|
||||
# 构建响应
|
||||
result = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"conversationId": request.conversationId or str(uuid.uuid4()),
|
||||
"content": content,
|
||||
"model": request.model,
|
||||
"createdAt": int(datetime.utcnow().timestamp())
|
||||
}
|
||||
|
||||
if hasattr(response, 'usage'):
|
||||
result["usage"] = {
|
||||
"promptTokens": response.usage.input_tokens,
|
||||
"completionTokens": response.usage.output_tokens,
|
||||
"totalTokens": response.usage.total_tokens
|
||||
}
|
||||
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail=f"API Error: {response.code} - {response.message}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error in chat endpoint: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@app.post("/api/chat-ui/chat/stream")
|
||||
async def chat_stream_endpoint(request: ChatRequest):
|
||||
"""流式聊天接口 - 处理流式请求"""
|
||||
async def event_generator():
|
||||
try:
|
||||
# 构建消息数组,考虑是否包含图片
|
||||
user_content = []
|
||||
|
||||
# 添加用户消息文本
|
||||
user_content.append({"type": "text", "text": request.message})
|
||||
|
||||
# 如果有图片,则添加到内容中
|
||||
if request.images and len(request.images) > 0:
|
||||
for image_url in request.images:
|
||||
user_content.append({
|
||||
"type": "image_url",
|
||||
"image_url": image_url
|
||||
})
|
||||
|
||||
# 构建请求给百炼的消息列表
|
||||
messages = [
|
||||
{"role": "system", "content": request.systemPrompt},
|
||||
{"role": "user", "content": user_content}
|
||||
]
|
||||
|
||||
# 调用 DashScope API(流式)
|
||||
responses = Generation.call(
|
||||
model=request.model,
|
||||
messages=messages,
|
||||
stream=True, # 流式响应
|
||||
max_tokens=request.maxTokens,
|
||||
temperature=request.temperature
|
||||
)
|
||||
|
||||
for response in responses:
|
||||
if response.status_code == 200:
|
||||
content = response.output.choices[0]['message']['content']
|
||||
|
||||
if content:
|
||||
# 发送流式数据
|
||||
data = {
|
||||
"choices": [
|
||||
{
|
||||
"delta": {"content": content},
|
||||
"index": 0,
|
||||
"finish_reason": None
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
else:
|
||||
error_data = {
|
||||
"error": {
|
||||
"message": f"API Error: {response.code} - {response.message}",
|
||||
"type": "api_error",
|
||||
"param": None,
|
||||
"code": response.code
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
break
|
||||
|
||||
# 发送结束信号
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
except Exception as e:
|
||||
error_data = {
|
||||
"error": {
|
||||
"message": str(e),
|
||||
"type": "server_error"
|
||||
}
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
@app.get("/api/chat-ui/models")
|
||||
async def get_models():
|
||||
"""获取模型列表"""
|
||||
models = [
|
||||
ModelInfo(
|
||||
id="qwen-max",
|
||||
name="通义千问 Max",
|
||||
description="最强大的模型",
|
||||
maxTokens=8192,
|
||||
provider="Aliyun"
|
||||
),
|
||||
ModelInfo(
|
||||
id="qwen-plus",
|
||||
name="通义千问 Plus",
|
||||
description="能力均衡",
|
||||
maxTokens=8192,
|
||||
provider="Aliyun"
|
||||
)
|
||||
]
|
||||
return [model.dict() for model in models]
|
||||
|
||||
@app.get("/api/chat-ui/conversations")
|
||||
async def get_conversations():
|
||||
"""获取所有对话"""
|
||||
return list(conversations_db.values())
|
||||
|
||||
@app.get("/api/chat-ui/conversations/{conversation_id}")
|
||||
async def get_conversation(conversation_id: str):
|
||||
"""获取特定对话"""
|
||||
conversation = conversations_db.get(conversation_id)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
return conversation
|
||||
|
||||
@app.post("/api/chat-ui/conversations")
|
||||
async def save_conversation(
|
||||
id: str = Form(None),
|
||||
title: str = Form(...),
|
||||
messages: str = Form(...)
|
||||
):
|
||||
"""保存或更新对话"""
|
||||
# 解析 messages JSON 字符串
|
||||
try:
|
||||
parsed_messages = json.loads(messages)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail="Invalid messages JSON")
|
||||
|
||||
conversation_id = id or str(uuid.uuid4())
|
||||
conversation = {
|
||||
"id": conversation_id,
|
||||
"title": title,
|
||||
"messages": parsed_messages,
|
||||
"updatedAt": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
conversations_db[conversation_id] = conversation
|
||||
return conversation
|
||||
|
||||
@app.delete("/api/chat-ui/conversations/{conversation_id}")
|
||||
async def delete_conversation(conversation_id: str):
|
||||
"""删除对话"""
|
||||
if conversation_id in conversations_db:
|
||||
del conversations_db[conversation_id]
|
||||
return {"success": True, "message": "删除成功"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="对话不存在")
|
||||
|
||||
@app.post("/api/chat-ui/upload")
|
||||
async def upload_file(file: UploadFile = File(...)):
|
||||
"""文件上传接口"""
|
||||
try:
|
||||
# 生成唯一文件名
|
||||
file_extension = Path(file.filename).suffix
|
||||
unique_filename = f"{int(datetime.utcnow().timestamp())}-{uuid.uuid4()}{file_extension}"
|
||||
file_path = upload_dir / unique_filename
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as f:
|
||||
content = await file.read()
|
||||
f.write(content)
|
||||
|
||||
# 返回文件信息
|
||||
file_url = f"http://localhost:8000/uploads/{unique_filename}"
|
||||
return {
|
||||
"url": file_url,
|
||||
"name": file.filename,
|
||||
"size": len(content),
|
||||
"mimeType": file.content_type
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Upload error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}")
|
||||
|
||||
@app.get("/uploads/{filename}")
|
||||
async def serve_upload(filename: str):
|
||||
"""提供上传文件的访问"""
|
||||
file_path = upload_dir / filename
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="文件不存在")
|
||||
|
||||
from fastapi.responses import FileResponse
|
||||
return FileResponse(file_path)
|
||||
|
||||
@app.post("/api/chat-ui/stop")
|
||||
async def stop_generation():
|
||||
"""停止生成接口"""
|
||||
# 在实际实现中,这里可能需要维护正在运行的任务ID列表
|
||||
# 目前只是返回成功消息
|
||||
return {"success": True, "message": "已发出停止指令"}
|
||||
|
||||
@app.post("/api/chat-ui/stop/{message_id}")
|
||||
async def stop_generation_by_id(message_id: str):
|
||||
"""根据消息ID停止生成"""
|
||||
return {"success": True, "message": "已发出停止指令"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
port = int(os.getenv("PORT", 8000))
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fastapi==0.115.4
|
||||
uvicorn==0.32.0
|
||||
dashscope==1.20.12
|
||||
python-multipart==0.0.18
|
||||
python-dotenv==1.0.1
|
||||
aiofiles==24.1.0
|
||||
pydantic==2.9.2
|
||||
typing-extensions==4.12.2
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#!/bin/bash
|
||||
# 启动Python服务器的增强脚本
|
||||
|
||||
echo "启动Python AI Chat服务器..."
|
||||
|
||||
# 检查是否有服务器已经在8000端口运行
|
||||
if lsof -Pi :8000 -sTCP:LISTEN -t >/dev/null; then
|
||||
echo "错误: 端口8000已被占用。请先停止占用该端口的进程。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 切换到服务器目录
|
||||
cd /home/mt/project/ai-chat-ui/server_python
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "错误: 虚拟环境不存在。请先创建虚拟环境:"
|
||||
echo "python3 -m venv .venv"
|
||||
echo "source .venv/bin/activate"
|
||||
echo "pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "虚拟环境已找到,正在激活..."
|
||||
|
||||
# 激活虚拟环境并启动服务器
|
||||
source .venv/bin/activate && python3 app.py
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
# 停止Python服务器的脚本
|
||||
|
||||
echo "正在停止Python AI Chat服务器..."
|
||||
|
||||
# 查找并终止在8000端口运行的Python进程
|
||||
PID=$(lsof -t -i:8000)
|
||||
if [ -n "$PID" ]; then
|
||||
kill $PID
|
||||
echo "服务器进程 (PID: $PID) 已停止"
|
||||
else
|
||||
echo "在端口8000上没有找到运行的服务器"
|
||||
fi
|
||||
|
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||
host: "0.0.0.0",
|
||||
proxy: {
|
||||
"/api/chat-ui": {
|
||||
target: "http://localhost:3000",
|
||||
target: "http://localhost:8000", // Python服务器端口
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue