Merge branch 'main' into feat/database

This commit is contained in:
SuperManTouX 2026-03-06 18:00:58 +08:00
commit 06ebc8cdb2
4 changed files with 214 additions and 1253 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,150 +0,0 @@
"""
GLM-4.6V 平台路由处理器zai-sdk
所有智谱 GLM 相关逻辑均集中在此文件main.py 无感知任何平台细节
"""
import json
import os
import sys
from pathlib import Path
from fastapi import HTTPException
from fastapi.responses import JSONResponse, StreamingResponse
from utils.helpers import generate_unique_id, get_current_timestamp
from utils.logger import log_info
def init():
"""
初始化 GLM 后端验证 API Key 是否配置
main.py 在启动时调用 LLM_BACKEND=glm
"""
api_key = os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY")
if not api_key:
raise ValueError(
"GLM 模式需要设置环境变量 ZHIPU_API_KEY在 https://open.bigmodel.cn 申请)"
)
log_info(f"[GLM] 初始化完成ZHIPU_API_KEY 已配置")
async def chat_handler(body: dict):
"""
GLM 聊天处理器对外接口与百炼 chat_endpoint_handler 完全兼容
流式/非流式自动适配支持图像文档附件联网搜索深度思考
"""
from utils.glm_adapter import glm_chat_sync, glm_stream_generator
if not isinstance(body, dict):
raise HTTPException(status_code=400, detail="请求体必须是 JSON 对象")
messages = body.get("messages", [])
model = body.get("model", "glm-4.6v")
stream = body.get("stream", True)
temperature = body.get("temperature", 0.7)
max_tokens = body.get("max_tokens", body.get("maxTokens", 2000))
# 区分搜索模式:深度搜索 > 简单搜索 > 不搜索
if body.get("deepSearch", False):
web_search = "deep"
elif body.get("webSearch", False):
web_search = "simple"
else:
web_search = False
deep_think = body.get("deepThinking", False)
files = body.get("files", [])
# 兼容前端简化格式(非 messages 结构)
if not messages:
msg_text = body.get("message", "")
sys_prompt = body.get("systemPrompt", "你是一个智能助手。")
user_content = (
msg_text
if isinstance(msg_text, list)
else [{"type": "text", "text": msg_text}]
)
messages = [
{"role": "system", "content": sys_prompt},
{"role": "user", "content": user_content},
]
log_info(
f"[GLM] model={model} stream={stream} web_search={web_search} "
f"thinking={deep_think} files={len(files)} msgs={len(messages)}"
)
if stream:
return StreamingResponse(
glm_stream_generator(
messages=messages,
model=model,
temperature=temperature,
max_tokens=max_tokens,
files=files or None,
web_search=web_search,
deep_thinking=deep_think,
),
media_type="text/event-stream",
)
result = glm_chat_sync(
messages=messages,
model=model,
temperature=temperature,
max_tokens=max_tokens,
files=files or None,
web_search=web_search,
deep_thinking=deep_think,
)
resp = {
"id": f"chatcmpl-{generate_unique_id()}",
"object": "chat.completion",
"created": get_current_timestamp(),
"model": result["model"],
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": result["content"]},
"finish_reason": "stop",
}
],
}
if result.get("usage"):
resp["usage"] = result["usage"]
return JSONResponse(content=resp)
def models_handler():
"""返回 GLM 可用模型列表"""
return {
"data": [
{
"id": "glm-4.6v",
"name": "GLM-4.6V(推荐)",
"description": "最新旗舰模型,支持文本/图像/文档/深度思考",
"maxTokens": 128000,
"provider": "ZhipuAI",
},
{
"id": "glm-4-flash",
"name": "GLM-4 Flash",
"description": "高性价比文本模型0.2元/千token",
"maxTokens": 128000,
"provider": "ZhipuAI",
},
{
"id": "glm-4v-plus-0111",
"name": "GLM-4V Plus",
"description": "图像 + PDF/DOCX 原生多模态",
"maxTokens": 128000,
"provider": "ZhipuAI",
},
{
"id": "glm-z1-flash",
"name": "GLM-Z1 Flash",
"description": "深度思考推理模型",
"maxTokens": 128000,
"provider": "ZhipuAI",
},
],
"object": "list",
}

View File

@ -0,0 +1,200 @@
"""
会话管理路由 - 对话 CRUD 和文件上传
与平台无关的通用功能不涉及任何 LLM 平台细节
"""
import json
import os
from datetime import datetime
from pathlib import Path
from typing import Dict
from fastapi import File, HTTPException, UploadFile
from fastapi.responses import FileResponse
import sys
sys.path.append(str(Path(__file__).parent.parent))
from database import get_db
from utils.helpers import generate_unique_id
from utils.logger import log_error, log_exception, log_info
# 配置上传目录
upload_dir = Path(__file__).parent.parent / "uploads"
upload_dir.mkdir(exist_ok=True)
# ── 会话管理 ─────────────────────────────────────────────────────
async def get_conversations_handler():
"""获取所有对话处理器"""
db = get_db()
return db.list_conversations()
async def get_conversation_handler(conversation_id: str):
"""获取特定对话处理器"""
db = get_db()
conversation = db.get_conversation(conversation_id)
if not conversation:
raise HTTPException(status_code=404, detail="对话不存在")
return conversation
async def save_conversation_handler(data: dict):
"""保存或更新对话处理器"""
try:
db = get_db()
conversation_id = data.get("id")
# 检查是否已存在
existing = db.get_conversation(conversation_id) if conversation_id else None
if existing:
# 更新现有会话
return db.update_conversation(conversation_id, data)
else:
# 创建新会话
return db.create_conversation(data)
except Exception as e:
log_error(f"Error saving conversation: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
async def delete_conversation_handler(conversation_id: str):
"""删除对话处理器"""
db = get_db()
success = db.delete_conversation(conversation_id)
if success:
return {"success": True, "message": "删除成功"}
else:
raise HTTPException(status_code=404, detail="对话不存在")
# ── 文件上传 ─────────────────────────────────────────────────────
async def upload_file_handler(file: UploadFile = File(...)):
"""文件上传处理器"""
try:
# 允许的 MIME 类型(宽松策略)
allowed_types = {
# 图片
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/bmp",
"image/svg+xml",
# 文本类
"text/plain",
"text/csv",
"text/markdown",
"text/html",
"text/xml",
"application/json",
"application/xml",
# PDF
"application/pdf",
# Office 文档
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
}
# 允许的扩展名兜底MIME 类型可能被浏览器误判)
allowed_extensions = {
".jpg",
".jpeg",
".png",
".gif",
".webp",
".bmp",
".txt",
".md",
".csv",
".json",
".xml",
".yaml",
".yml",
".log",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".py",
".js",
".ts",
".html",
".css",
}
file_extension = Path(file.filename).suffix.lower()
if (
file.content_type not in allowed_types
and file_extension not in allowed_extensions
):
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型: {file.content_type}{file_extension}",
)
# 生成唯一文件名
unique_filename = f"{int(datetime.utcnow().timestamp())}_{generate_unique_id()}{file_extension}"
file_path = upload_dir / unique_filename
# 保存文件到本地(临时缓存)
content = await file.read()
with open(file_path, "wb") as f:
f.write(content)
# 文件关闭后再上传到 OSS
from utils.oss_uploader import upload_file as oss_upload
oss_result = oss_upload(str(file_path))
file_url = oss_result["url"]
# 返回文件信息
result = {
"url": file_url,
"name": file.filename,
"size": len(content),
"mimeType": file.content_type,
}
log_info(f"File uploaded: {result}")
return result
except Exception as e:
log_error(f"Upload error: {str(e)}")
raise HTTPException(status_code=500, detail=f"上传失败: {str(e)}")
def serve_upload_handler(filename: str):
"""提供上传文件访问处理器"""
file_path = upload_dir / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="文件不存在")
return FileResponse(str(file_path))
# ── 停止生成 ─────────────────────────────────────────────────────
async def stop_generation_handler(message_id: str = None):
"""停止生成处理器"""
message = (
f"已发出停止指令消息ID: {message_id}" if message_id else "已发出停止指令"
)
return {"success": True, "message": message}

View File

@ -45,28 +45,22 @@ from utils.logger import get_logger
logger = get_logger()
# ── 初始化数据库 ───────────────────────────────────────────────────────
from database import init_db
init_db()
# ── 加载环境变量 ──────────────────────────────────────────────────────
load_dotenv()
LLM_BACKEND = os.getenv("LLM_BACKEND", "dashscope").lower().strip()
if LLM_BACKEND not in {"dashscope", "glm"}:
logger.warning(f"未知的 LLM_BACKEND='{LLM_BACKEND}',回退到 dashscope")
LLM_BACKEND = "dashscope"
# ── 动态加载平台模块 ──────────────────────────────────────────────────
if LLM_BACKEND == "glm":
import api.chat_routes_glm as _platform
else:
import api.chat_routes as _platform
_platform.init() # 各平台自行完成初始化API Key 校验等)
# 通用路由处理器(文件上传、会话管理等,与平台无关,统一用百炼路由中的实现)
from api.chat_routes import (delete_conversation_handler,
get_conversation_handler,
get_conversations_handler,
save_conversation_handler, serve_upload_handler,
stop_generation_handler, upload_file_handler)
# ── 会话管理路由处理器 ────────────────────────────────────────────────
from api.conversation_routes import (delete_conversation_handler,
get_conversation_handler,
get_conversations_handler,
save_conversation_handler,
serve_upload_handler,
stop_generation_handler,
upload_file_handler)
# ── OpenAI 兼容网关初始化 ───────────────────────────────────────────────
from api.openai_gateway import init_adapters, router as openai_router
@ -110,12 +104,11 @@ async def health_check():
return {
"status": "healthy",
"version": "4.0.0",
"default_backend": LLM_BACKEND,
"available_providers": get_available_providers(),
"endpoints": {
"openai_compatible": "/v1/chat/completions",
"legacy": "/api/chat-ui/chat",
"models": "/v1/models",
"conversations": "/api/chat-ui/conversations",
},
"timestamp": datetime.now(timezone.utc).isoformat(),
}
@ -234,7 +227,6 @@ if __name__ == "__main__":
print(f" 模型列表 : http://localhost:{port}/v1/models")
print("-" * 60)
print(f" 可用平台 : {', '.join(available) or '无(请配置 API Key'}")
print(f" 默认平台 : {LLM_BACKEND} (向后兼容模式)")
print("-" * 60)
print(" 使用方法:")
print(" curl -X POST http://localhost:8000/v1/chat/completions \\")