- 将全部路由文件(12个)的 jsonify 响应迁移至 success_response/error_response 统一格式 - 修复 sync_routes.py error_response 参数错误(P0) - 新增异步任务系统:task_registry + task_routes - 新增状态码:TASK_NOT_FOUND(4014)、TASK_CONFLICT(4015)、REINDEX_ERROR(5040) - 修正 task_routes/exam_pkg 中语义不匹配的状态码 - 更新 curl 测试手册、后端对接规范文档 - 添加缓存性能报告和 Redis 迁移计划
263 lines
8.7 KiB
Python
263 lines
8.7 KiB
Python
"""
|
||
API 路由层 — Flask 应用工厂
|
||
|
||
本模块实现 Flask 应用工厂模式,负责:
|
||
- 创建和配置 Flask 应用实例
|
||
- 初始化核心服务(同步服务)
|
||
- 注册所有 API Blueprint
|
||
- 配置前端静态文件路由
|
||
|
||
核心路由模块:
|
||
- chat_routes.py : 问答接口 (/chat, /rag, /rag/stream, /search)
|
||
- kb_routes.py : 向量库管理 (/collections, /documents/sync)
|
||
- document_routes.py: 文档管理 (/documents/upload, /documents/list)
|
||
- sync_routes.py : 同步服务 (/sync, /sync/status)
|
||
- image_routes.py : 图片服务 (/images/*)
|
||
- exam_pkg/api.py : 出题系统 (/exam/generate, /exam/grade)
|
||
|
||
架构说明:
|
||
- 会话管理、审计日志、反馈系统由后端服务负责
|
||
- 权限验证由后端网关完成(Header 注入)
|
||
- RAG 服务无状态,不存储用户数据
|
||
|
||
Example:
|
||
>>> from api import create_app
|
||
>>> app = create_app()
|
||
>>> app.run(host='0.0.0.0', port=5001)
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import logging
|
||
from typing import Optional
|
||
|
||
# 确保项目根目录在路径中
|
||
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
if PROJECT_ROOT not in sys.path:
|
||
sys.path.insert(0, PROJECT_ROOT)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def create_app() -> 'Flask':
|
||
"""
|
||
Flask 应用工厂函数
|
||
|
||
创建并配置 Flask 应用实例,完成以下初始化步骤:
|
||
|
||
1. 创建 Flask 应用,配置 CORS
|
||
2. 初始化 Repository(会话存储)
|
||
3. 初始化核心服务(同步服务)
|
||
4. 注册所有 API Blueprint
|
||
5. 配置前端静态文件路由
|
||
6. 执行生产环境配置校验
|
||
|
||
Returns:
|
||
Flask: 配置完成的 Flask 应用实例
|
||
|
||
Example:
|
||
>>> app = create_app()
|
||
>>> app.run(host='0.0.0.0', port=5001)
|
||
"""
|
||
from flask import Flask, send_from_directory
|
||
from flask_cors import CORS
|
||
from config import ENABLE_SESSION, ENABLE_FEEDBACK, IS_PROD
|
||
|
||
# 静态文件目录(前端)
|
||
static_folder = os.path.join(PROJECT_ROOT, 'chat-ui')
|
||
|
||
app = Flask(__name__, static_folder=static_folder, static_url_path='')
|
||
|
||
# CORS 配置:生产环境限制来源,开发环境允许全部
|
||
if IS_PROD:
|
||
cors_origins = os.environ.get('CORS_ORIGINS', '').split(',') if os.environ.get('CORS_ORIGINS') else []
|
||
if cors_origins:
|
||
CORS(app, origins=cors_origins)
|
||
else:
|
||
CORS(app) # 未配置时仍允许全部,但记录警告
|
||
logger.warning("生产环境未配置 CORS_ORIGINS,CORS 允许所有来源")
|
||
else:
|
||
CORS(app)
|
||
|
||
# ==================== Repository 依赖注入 ====================
|
||
|
||
# 会话存储:开发环境用SQLite,生产环境无状态
|
||
if ENABLE_SESSION:
|
||
from repositories.sqlite_session_repo import SQLiteSessionRepo
|
||
app.session_repo = SQLiteSessionRepo()
|
||
logger.info("会话存储: SQLite (开发环境)")
|
||
else:
|
||
from repositories.stateless_session_repo import StatelessSessionRepo
|
||
app.session_repo = StatelessSessionRepo()
|
||
logger.info("会话存储: 无状态 (生产环境)")
|
||
|
||
# ==================== 核心服务初始化 ====================
|
||
|
||
# 同步服务
|
||
try:
|
||
from knowledge.sync import KnowledgeSyncService
|
||
from config import DOCUMENTS_PATH
|
||
sync_service = KnowledgeSyncService(documents_path=DOCUMENTS_PATH)
|
||
app.config['SYNC_SERVICE'] = sync_service
|
||
logger.info("知识库同步服务已初始化")
|
||
except Exception as e:
|
||
app.config['SYNC_SERVICE'] = None
|
||
logger.warning(f"知识库同步服务未启用: {e}")
|
||
|
||
# 会话管理器(仅开发环境)
|
||
if ENABLE_SESSION:
|
||
try:
|
||
from services.session import SessionManager
|
||
session_manager = SessionManager()
|
||
app.config['SESSION_MANAGER'] = session_manager
|
||
logger.info("会话管理器已初始化")
|
||
except Exception as e:
|
||
app.config['SESSION_MANAGER'] = None
|
||
logger.warning(f"会话管理器初始化失败: {e}")
|
||
|
||
# ==================== 注册 Blueprint ====================
|
||
|
||
# 核心 API
|
||
from api.chat_routes import chat_bp
|
||
from api.kb_routes import kb_bp
|
||
from api.document_routes import document_bp
|
||
from api.sync_routes import sync_bp
|
||
|
||
app.register_blueprint(chat_bp)
|
||
app.register_blueprint(kb_bp)
|
||
app.register_blueprint(document_bp)
|
||
app.register_blueprint(sync_bp)
|
||
|
||
# 图片服务
|
||
from api.image_routes import image_bp
|
||
app.register_blueprint(image_bp)
|
||
|
||
# 异步任务查询
|
||
from api.task_routes import task_bp
|
||
app.register_blueprint(task_bp)
|
||
|
||
# 健康检查
|
||
from api.auth_routes import auth_bp
|
||
app.register_blueprint(auth_bp)
|
||
|
||
# 会话管理(仅开发环境)
|
||
if ENABLE_SESSION:
|
||
try:
|
||
from api.session_routes import session_bp
|
||
app.register_blueprint(session_bp)
|
||
logger.info("会话管理 API 已启用")
|
||
except ImportError as e:
|
||
logger.info(f"会话管理 API 未加载: {e}")
|
||
|
||
# 审计日志(仅开发环境,依赖 session.db)
|
||
if ENABLE_SESSION:
|
||
try:
|
||
from api.audit_routes import audit_bp
|
||
app.register_blueprint(audit_bp)
|
||
logger.info("审计日志 API 已启用")
|
||
except ImportError as e:
|
||
logger.info(f"审计日志 API 未加载: {e}")
|
||
|
||
# 反馈系统(开发和生产环境都启用)
|
||
if ENABLE_FEEDBACK:
|
||
try:
|
||
from api.feedback_routes import feedback_bp
|
||
app.register_blueprint(feedback_bp)
|
||
logger.info("反馈系统 API 已启用")
|
||
except ImportError as e:
|
||
logger.info(f"反馈系统 API 未加载: {e}")
|
||
|
||
# 出题系统(可选)
|
||
try:
|
||
from exam_pkg.api import exam_bp
|
||
app.register_blueprint(exam_bp, url_prefix='/exam')
|
||
logger.info("出题系统 API 已启用: /exam")
|
||
except ImportError as e:
|
||
logger.info(f"出题系统 API 未加载: {e}")
|
||
|
||
# ==================== 生产环境启动校验 ====================
|
||
|
||
if IS_PROD:
|
||
_validate_production_config()
|
||
|
||
# ==================== 前端静态文件路由 ====================
|
||
|
||
# 首页
|
||
@app.route('/')
|
||
def serve_index():
|
||
"""首页"""
|
||
return send_from_directory(static_folder, 'index.html')
|
||
|
||
# 静态文件(需要明确指定,避免与 API 路由冲突)
|
||
@app.route('/app.js')
|
||
def serve_app_js():
|
||
return send_from_directory(static_folder, 'app.js')
|
||
|
||
@app.route('/style.css')
|
||
def serve_style_css():
|
||
return send_from_directory(static_folder, 'style.css')
|
||
|
||
@app.route('/exam.html')
|
||
def serve_exam_html():
|
||
return send_from_directory(static_folder, 'exam.html')
|
||
|
||
@app.route('/exam.js')
|
||
def serve_exam_js():
|
||
return send_from_directory(static_folder, 'exam.js')
|
||
|
||
@app.route('/api-test.html')
|
||
def serve_api_test_html():
|
||
return send_from_directory(static_folder, 'api-test.html')
|
||
|
||
@app.route('/api-test.js')
|
||
def serve_api_test_js():
|
||
return send_from_directory(static_folder, 'api-test.js')
|
||
|
||
# ==================== 启动信息 ====================
|
||
|
||
_print_startup_info(app)
|
||
|
||
return app
|
||
|
||
|
||
def _validate_production_config() -> None:
|
||
"""
|
||
生产环境启动前配置校验
|
||
|
||
检查必要的配置项是否存在:
|
||
- DASHSCOPE_API_KEY: 大模型调用必需
|
||
|
||
Raises:
|
||
RuntimeError: 缺少必需的配置项
|
||
"""
|
||
import os
|
||
from config import DASHSCOPE_API_KEY
|
||
# 检查环境变量或配置文件中的 API Key
|
||
has_key = os.getenv("DASHSCOPE_API_KEY") or os.environ.get("DASHSCOPE_API_KEY") or DASHSCOPE_API_KEY
|
||
if not has_key or has_key == "":
|
||
raise RuntimeError("Missing DASHSCOPE_API_KEY in production environment")
|
||
logger.info("Configuration validated")
|
||
|
||
|
||
def _print_startup_info(app: 'Flask') -> None:
|
||
"""
|
||
打印启动信息摘要
|
||
|
||
在应用启动时输出注册的路由数量和主要端点列表,
|
||
便于运维人员确认服务状态。
|
||
|
||
Args:
|
||
app: Flask 应用实例
|
||
"""
|
||
route_count = len([rule for rule in app.url_map.iter_rules() if rule.endpoint != 'static'])
|
||
|
||
logger.info(f"应用初始化完成,共注册 {route_count} 个路由")
|
||
logger.info(" 问答接口: /chat, /rag, /rag/stream, /search")
|
||
logger.info(" 向量库: /collections, /collections/<name>")
|
||
logger.info(" 文档管理: /documents/upload, /documents/list, /documents/*")
|
||
logger.info(" 切片管理: /chunks/*")
|
||
logger.info(" 同步服务: /sync, /sync/status")
|
||
logger.info(" 图片服务: /images/*")
|
||
logger.info(" 任务查询: /tasks, /tasks/<id>, /tasks/<id>/progress")
|
||
logger.info(" 健康检查: /health")
|