File size: 4,408 Bytes
35e7795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4c341cd
35e7795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b3f878b
4c341cd
35e7795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""FastAPI 应用主入口"""

import os
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.responses import FileResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from starlette.types import Scope

from qa_annotate.api.analysis import router as analysis_router
from qa_annotate.api.annotation import annotation_result_router
from qa_annotate.api.annotation import router as annotation_router
from qa_annotate.api.auth import get_optional_user
from qa_annotate.api.dataset import router as dataset_router
from qa_annotate.api.project import router as project_router
from qa_annotate.api.seed_question import router as seed_question_router
from qa_annotate.api.system_config import router as system_config_router
from qa_annotate.api.user import router as user_router
from qa_annotate.config import settings
from qa_annotate.bootstrap import ensure_llm_config, seed_demo_llm_analysis
from qa_annotate.database.base import init_db


class NoCachedStaticFiles(StaticFiles):
    """带缓存头的静态文件服务"""

    def file_response(
        self,
        full_path: str,
        stat_result: os.stat_result,
        scope: Scope,
        status_code: int = 200,
    ) -> Response:
        response = super().file_response(full_path, stat_result, scope, status_code)
        # 禁用缓存
        response.headers["Cache-Control"] = "no-cache"
        response.headers["Pragma"] = "no-cache"
        response.headers["Expires"] = "0"
        return response


@asynccontextmanager
async def lifespan(app: FastAPI):
    """应用生命周期管理"""
    # 启动时执行
    init_db()
    ensure_llm_config()
    seed_demo_llm_analysis()
    yield
    # 关闭时执行(如果需要清理资源,可以在这里添加)


# 创建 FastAPI 应用
# 生产环境禁用文档
docs_url = None if settings.is_production else "/docs"
redoc_url = None if settings.is_production else "/redoc"

app = FastAPI(
    title="QA 标注系统 API",
    description="QA对数据集标注系统 API 接口",
    version="0.1.0",
    lifespan=lifespan,
    docs_url=docs_url,
    redoc_url=redoc_url,
)

# 注册路由(添加/api前缀)
app.include_router(user_router, prefix="/api")
app.include_router(dataset_router, prefix="/api")
app.include_router(annotation_router, prefix="/api")
app.include_router(annotation_result_router, prefix="/api")
app.include_router(project_router, prefix="/api")
app.include_router(seed_question_router, prefix="/api")
app.include_router(system_config_router, prefix="/api")
app.include_router(analysis_router, prefix="/api")

# 挂载静态文件(带1分钟缓存)
app.mount("/static", NoCachedStaticFiles(directory="qa_annotate/static"), name="static")


@app.get("/")
async def root(user=Depends(get_optional_user)):
    """根据用户登录状态和权限返回不同页面"""
    # 根据用户状态返回不同页面
    if user is None:
        # 未登录,返回 auth.html
        return RedirectResponse(url="/auth")
    elif user.is_superuser:
        # 超级用户,返回 manager.html
        return RedirectResponse(url="/manager")
    else:
        # 普通用户,返回 user.html
        return RedirectResponse(url="/user")


@app.get("/{path}")
async def html(path: str):
    """返回 html 目录中的文件"""
    if not path.endswith(".html"):
        path = path + ".html"
    html_dir = Path(__file__).parent / "html"
    file_path = (html_dir / path).resolve()
    # 防止目录穿越攻击,只允许访问html目录下的文件
    html_dir_resolved = html_dir.resolve()
    if not str(file_path).startswith(str(html_dir_resolved)):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
    if not file_path.exists() or not file_path.is_file():
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")
    return FileResponse(str(file_path))


@app.get("/api/health")
async def health_check():
    """健康检查接口"""
    return {"status": "healthy"}


def main():
    """启动应用的入口函数"""
    import uvicorn

    from qa_annotate.config import settings

    uvicorn.run(
        "qa_annotate.main:app",
        host=settings.HOST,
        port=settings.PORT,
        reload=settings.RELOAD,
        reload_dirs=["qa_annotate"],
    )