docker build 构建修改

This commit is contained in:
2025-11-06 17:15:46 +08:00
parent 33e09afc54
commit ecdb4321e8
58 changed files with 72216 additions and 237 deletions

View File

@@ -162,6 +162,9 @@ def setup_routes(app: FastAPI):
from api.apps.chunk_app import router as chunk_router
from api.apps.mcp_server_app import router as mcp_router
from api.apps.canvas_app import router as canvas_router
from api.apps.tenant_app import router as tenant_router
from api.apps.dialog_app import router as dialog_router
from api.apps.system_app import router as system_router
app.include_router(user_router, prefix=f"/{API_VERSION}/user", tags=["User"])
app.include_router(kb_router, prefix=f"/{API_VERSION}/kb", tags=["KnowledgeBase"])
@@ -170,6 +173,9 @@ def setup_routes(app: FastAPI):
app.include_router(chunk_router, prefix=f"/{API_VERSION}/chunk", tags=["Chunk"])
app.include_router(mcp_router, prefix=f"/{API_VERSION}/mcp", tags=["MCP"])
app.include_router(canvas_router, prefix=f"/{API_VERSION}/canvas", tags=["Canvas"])
app.include_router(tenant_router, prefix=f"/{API_VERSION}/tenant", tags=["Tenant"])
app.include_router(dialog_router, prefix=f"/{API_VERSION}/dialog", tags=["Dialog"])
app.include_router(system_router, prefix=f"/{API_VERSION}/system", tags=["System"])

View File

@@ -14,8 +14,17 @@
# limitations under the License.
#
from flask import request
from flask_login import login_required, current_user
from typing import Optional
from fastapi import APIRouter, Depends, Query
from api.apps.models.auth_dependencies import get_current_user
from api.apps.models.dialog_models import (
SetDialogRequest,
ListDialogsNextQuery,
ListDialogsNextBody,
DeleteDialogRequest,
)
from api.db.services import duplicate_name
from api.db.services.dialog_service import DialogService
from api.db import StatusEnum
@@ -23,16 +32,21 @@ from api.db.services.tenant_llm_service import TenantLLMService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.user_service import TenantService, UserTenantService
from api import settings
from api.utils.api_utils import server_error_response, get_data_error_result, validate_request
from api.utils.api_utils import server_error_response, get_data_error_result
from api.utils import get_uuid
from api.utils.api_utils import get_json_result
# 创建路由器
router = APIRouter()
@manager.route('/set', methods=['POST']) # noqa: F821
@validate_request("prompt_config")
@login_required
def set_dialog():
req = request.json
@router.post('/set')
async def set_dialog(
request: SetDialogRequest,
current_user = Depends(get_current_user)
):
"""设置/创建对话框"""
req = request.model_dump(exclude_unset=True)
dialog_id = req.get("dialog_id", "")
is_create = not dialog_id
name = req.get("name", "New Dialog")
@@ -124,10 +138,12 @@ def set_dialog():
return server_error_response(e)
@manager.route('/get', methods=['GET']) # noqa: F821
@login_required
def get():
dialog_id = request.args["dialog_id"]
@router.get('/get')
async def get(
dialog_id: str = Query(..., description="对话框ID"),
current_user = Depends(get_current_user)
):
"""获取对话框详情"""
try:
e, dia = DialogService.get_by_id(dialog_id)
if not e:
@@ -150,9 +166,11 @@ def get_kb_names(kb_ids):
return ids, nms
@manager.route('/list', methods=['GET']) # noqa: F821
@login_required
def list_dialogs():
@router.get('/list')
async def list_dialogs(
current_user = Depends(get_current_user)
):
"""列出对话框"""
try:
diags = DialogService.query(
tenant_id=current_user.id,
@@ -167,21 +185,24 @@ def list_dialogs():
return server_error_response(e)
@manager.route('/next', methods=['POST']) # noqa: F821
@login_required
def list_dialogs_next():
keywords = request.args.get("keywords", "")
page_number = int(request.args.get("page", 0))
items_per_page = int(request.args.get("page_size", 0))
parser_id = request.args.get("parser_id")
orderby = request.args.get("orderby", "create_time")
if request.args.get("desc", "true").lower() == "false":
desc = False
else:
desc = True
@router.post('/next')
async def list_dialogs_next(
query: ListDialogsNextQuery = Depends(),
body: Optional[ListDialogsNextBody] = None,
current_user = Depends(get_current_user)
):
"""列出对话框(分页)"""
if body is None:
body = ListDialogsNextBody()
keywords = query.keywords or ""
page_number = int(query.page or 0)
items_per_page = int(query.page_size or 0)
parser_id = query.parser_id
orderby = query.orderby or "create_time"
desc = query.desc.lower() == "true" if query.desc else True
req = request.get_json()
owner_ids = req.get("owner_ids", [])
owner_ids = body.owner_ids or []
try:
if not owner_ids:
# tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
@@ -204,15 +225,16 @@ def list_dialogs_next():
return server_error_response(e)
@manager.route('/rm', methods=['POST']) # noqa: F821
@login_required
@validate_request("dialog_ids")
def rm():
req = request.json
dialog_list=[]
@router.post('/rm')
async def rm(
request: DeleteDialogRequest,
current_user = Depends(get_current_user)
):
"""删除对话框"""
dialog_list = []
tenants = UserTenantService.query(user_id=current_user.id)
try:
for id in req["dialog_ids"]:
for id in request.dialog_ids:
for tenant in tenants:
if DialogService.query(tenant_id=tenant.tenant_id, id=id):
break
@@ -220,7 +242,7 @@ def rm():
return get_json_result(
data=False, message='Only owner of dialog authorized for this operation.',
code=settings.RetCode.OPERATING_ERROR)
dialog_list.append({"id": id,"status":StatusEnum.INVALID.value})
dialog_list.append({"id": id, "status": StatusEnum.INVALID.value})
DialogService.update_many_by_id(dialog_list)
return get_json_result(data=True)
except Exception as e:

View File

@@ -24,25 +24,43 @@ from api.utils.api_utils import get_json_result
http_bearer = HTTPBearer(auto_error=False)
def get_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Security(http_bearer)):
def get_current_user(
authorization: Optional[str] = Header(None, alias="Authorization"),
credentials: Optional[HTTPAuthorizationCredentials] = Security(http_bearer)
):
"""FastAPI 依赖注入:获取当前用户(替代 Flask 的 login_required 和 current_user
支持两种格式的 Authorization 头:
1. 标准格式Bearer <token>
2. 简化格式:<token>(不带 Bearer 前缀)
使用 Security(http_bearer) 可以让 FastAPI 自动在 OpenAPI schema 中添加安全要求,
这样 Swagger UI 就会显示授权输入框并自动在请求中添加 Authorization 头。
"""
# 延迟导入以避免循环导入
from api.apps.__init___fastapi import get_current_user_from_token
if not credentials:
token = None
# 优先从 HTTPBearer 获取标准格式Bearer <token>
if credentials:
token = credentials.credentials
# 如果 HTTPBearer 没有获取到,尝试直接从 Header 获取(可能是简化格式)
elif authorization:
# 如果包含 "Bearer " 前缀,则去除它
if authorization.startswith("Bearer "):
token = authorization[7:] # 去除 "Bearer " 前缀7个字符
else:
# 不带 Bearer 前缀,直接使用
token = authorization
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header is required"
)
# HTTPBearer 已经提取了 Bearer tokencredentials.credentials 就是 token 本身
authorization = credentials.credentials
user = get_current_user_from_token(authorization)
user = get_current_user_from_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,

View File

@@ -0,0 +1,57 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
class SetDialogRequest(BaseModel):
"""设置/创建对话框请求"""
dialog_id: Optional[str] = Field(default="", description="对话框ID为空时创建新对话框")
name: Optional[str] = Field(default="New Dialog", description="对话框名称")
description: Optional[str] = Field(default="A helpful dialog", description="对话框描述")
icon: Optional[str] = Field(default="", description="图标")
top_n: Optional[int] = Field(default=6, description="Top N")
top_k: Optional[int] = Field(default=1024, description="Top K")
rerank_id: Optional[str] = Field(default="", description="重排序模型ID")
similarity_threshold: Optional[float] = Field(default=0.1, description="相似度阈值")
vector_similarity_weight: Optional[float] = Field(default=0.3, description="向量相似度权重")
llm_setting: Optional[Dict[str, Any]] = Field(default={}, description="LLM设置")
meta_data_filter: Optional[Dict[str, Any]] = Field(default={}, description="元数据过滤器")
prompt_config: Dict[str, Any] = Field(..., description="提示配置")
kb_ids: Optional[List[str]] = Field(default=[], description="知识库ID列表")
llm_id: Optional[str] = Field(default=None, description="LLM ID")
class ListDialogsNextQuery(BaseModel):
"""列出对话框查询参数"""
keywords: Optional[str] = ""
page: Optional[int] = 0
page_size: Optional[int] = 0
parser_id: Optional[str] = None
orderby: Optional[str] = "create_time"
desc: Optional[str] = "true"
class ListDialogsNextBody(BaseModel):
"""列出对话框请求体"""
owner_ids: Optional[List[str]] = []
class DeleteDialogRequest(BaseModel):
"""删除对话框请求"""
dialog_ids: List[str] = Field(..., description="要删除的对话框ID列表")

View File

@@ -138,11 +138,11 @@ class GetDocumentInfosRequest(BaseModel):
class ChangeStatusRequest(BaseModel):
"""修改文档状态请求"""
doc_ids: List[str]
status: str # "0" 或 "1"
status: int
@model_validator(mode='after')
def validate_status(self):
if self.status not in ["0", "1"]:
if self.status not in [0, 1]:
raise ValueError('Status must be either 0 or 1!')
return self
@@ -155,7 +155,7 @@ class DeleteDocumentRequest(BaseModel):
class RunDocumentRequest(BaseModel):
"""运行文档解析请求"""
doc_ids: List[str]
run: str # TaskStatus 值
run: int # TaskStatus 值
delete: Optional[bool] = False

View File

@@ -0,0 +1,23 @@
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from pydantic import BaseModel, Field, EmailStr
class InviteUserRequest(BaseModel):
"""邀请用户请求"""
email: EmailStr = Field(..., description="要邀请的用户邮箱")

View File

@@ -17,7 +17,8 @@ import logging
from datetime import datetime
import json
from flask_login import login_required, current_user
from fastapi import APIRouter, Depends
from fastapi.responses import JSONResponse
from api.db.db_models import APIToken
from api.db.services.api_service import APITokenService
@@ -36,67 +37,26 @@ from rag.utils.storage_factory import STORAGE_IMPL, STORAGE_IMPL_TYPE
from timeit import default_timer as timer
from rag.utils.redis_conn import REDIS_CONN
from flask import jsonify
from api.utils.health_utils import run_health_checks
from api.apps.models.auth_dependencies import get_current_user
# 创建路由器
router = APIRouter()
@manager.route("/version", methods=["GET"]) # noqa: F821
@login_required
def version():
"""
Get the current version of the application.
---
tags:
- System
security:
- ApiKeyAuth: []
responses:
200:
description: Version retrieved successfully.
schema:
type: object
properties:
version:
type: string
description: Version number.
"""
@router.get("/version")
async def version(
current_user = Depends(get_current_user)
):
"""获取应用程序当前版本"""
return get_json_result(data=get_ragflow_version())
@manager.route("/status", methods=["GET"]) # noqa: F821
@login_required
def status():
"""
Get the system status.
---
tags:
- System
security:
- ApiKeyAuth: []
responses:
200:
description: System is operational.
schema:
type: object
properties:
es:
type: object
description: Elasticsearch status.
storage:
type: object
description: Storage status.
database:
type: object
description: Database status.
503:
description: Service unavailable.
schema:
type: object
properties:
error:
type: string
description: Error message.
"""
@router.get("/status")
async def status(
current_user = Depends(get_current_user)
):
"""获取系统状态"""
res = {}
st = timer()
try:
@@ -172,43 +132,24 @@ def status():
return get_json_result(data=res)
@manager.route("/healthz", methods=["GET"]) # noqa: F821
def healthz():
@router.get("/healthz")
async def healthz():
"""健康检查"""
result, all_ok = run_health_checks()
return jsonify(result), (200 if all_ok else 500)
return JSONResponse(content=result, status_code=200 if all_ok else 500)
@manager.route("/ping", methods=["GET"]) # noqa: F821
def ping():
return "pong", 200
@router.get("/ping")
async def ping():
"""心跳检测"""
return "pong"
@manager.route("/new_token", methods=["POST"]) # noqa: F821
@login_required
def new_token():
"""
Generate a new API token.
---
tags:
- API Tokens
security:
- ApiKeyAuth: []
parameters:
- in: query
name: name
type: string
required: false
description: Name of the token.
responses:
200:
description: Token generated successfully.
schema:
type: object
properties:
token:
type: string
description: The generated API token.
"""
@router.post("/new_token")
async def new_token(
current_user = Depends(get_current_user)
):
"""生成新的 API 令牌"""
try:
tenants = UserTenantService.query(user_id=current_user.id)
if not tenants:
@@ -233,37 +174,11 @@ def new_token():
return server_error_response(e)
@manager.route("/token_list", methods=["GET"]) # noqa: F821
@login_required
def token_list():
"""
List all API tokens for the current user.
---
tags:
- API Tokens
security:
- ApiKeyAuth: []
responses:
200:
description: List of API tokens.
schema:
type: object
properties:
tokens:
type: array
items:
type: object
properties:
token:
type: string
description: The API token.
name:
type: string
description: Name of the token.
create_time:
type: string
description: Token creation time.
"""
@router.get("/token_list")
async def token_list(
current_user = Depends(get_current_user)
):
"""列出当前用户的所有 API 令牌"""
try:
tenants = UserTenantService.query(user_id=current_user.id)
if not tenants:
@@ -282,55 +197,21 @@ def token_list():
return server_error_response(e)
@manager.route("/token/<token>", methods=["DELETE"]) # noqa: F821
@login_required
def rm(token):
"""
Remove an API token.
---
tags:
- API Tokens
security:
- ApiKeyAuth: []
parameters:
- in: path
name: token
type: string
required: true
description: The API token to remove.
responses:
200:
description: Token removed successfully.
schema:
type: object
properties:
success:
type: boolean
description: Deletion status.
"""
@router.delete("/token/{token}")
async def rm(
token: str,
current_user = Depends(get_current_user)
):
"""删除 API 令牌"""
APITokenService.filter_delete(
[APIToken.tenant_id == current_user.id, APIToken.token == token]
)
return get_json_result(data=True)
@manager.route('/config', methods=['GET']) # noqa: F821
def get_config():
"""
Get system configuration.
---
tags:
- System
responses:
200:
description: Return system configuration
schema:
type: object
properties:
registerEnable:
type: integer 0 means disabled, 1 means enabled
description: Whether user registration is enabled
"""
@router.get('/config')
async def get_config():
"""获取系统配置"""
return get_json_result(data={
"registerEnabled": settings.REGISTER_ENABLED
})

View File

@@ -14,9 +14,10 @@
# limitations under the License.
#
from flask import request
from flask_login import login_required, current_user
from fastapi import APIRouter, Depends, Path
from api.apps.models.auth_dependencies import get_current_user
from api.apps.models.tenant_models import InviteUserRequest
from api import settings
from api.apps import smtp_mail_server
from api.db import UserTenantRole, StatusEnum
@@ -24,13 +25,19 @@ from api.db.db_models import UserTenant
from api.db.services.user_service import UserTenantService, UserService
from api.utils import get_uuid, delta_seconds
from api.utils.api_utils import get_json_result, validate_request, server_error_response, get_data_error_result
from api.utils.api_utils import get_json_result, server_error_response, get_data_error_result
from api.utils.web_utils import send_invite_email
# 创建路由器
router = APIRouter()
@manager.route("/<tenant_id>/user/list", methods=["GET"]) # noqa: F821
@login_required
def user_list(tenant_id):
@router.get("/{tenant_id}/user/list")
async def user_list(
tenant_id: str = Path(..., description="租户ID"),
current_user = Depends(get_current_user)
):
"""获取租户用户列表"""
if current_user.id != tenant_id:
return get_json_result(
data=False,
@@ -46,18 +53,20 @@ def user_list(tenant_id):
return server_error_response(e)
@manager.route('/<tenant_id>/user', methods=['POST']) # noqa: F821
@login_required
@validate_request("email")
def create(tenant_id):
@router.post('/{tenant_id}/user')
async def create(
tenant_id: str,
request: InviteUserRequest,
current_user = Depends(get_current_user)
):
"""邀请用户加入租户"""
if current_user.id != tenant_id:
return get_json_result(
data=False,
message='No authorization.',
code=settings.RetCode.AUTHENTICATION_ERROR)
req = request.json
invite_user_email = req["email"]
invite_user_email = request.email
invite_users = UserService.query(email=invite_user_email)
if not invite_users:
return get_data_error_result(message="User not found.")
@@ -101,9 +110,13 @@ def create(tenant_id):
return get_json_result(data=usr)
@manager.route('/<tenant_id>/user/<user_id>', methods=['DELETE']) # noqa: F821
@login_required
def rm(tenant_id, user_id):
@router.delete('/{tenant_id}/user/{user_id}')
async def rm(
tenant_id: str = Path(..., description="租户ID"),
user_id: str = Path(..., description="用户ID"),
current_user = Depends(get_current_user)
):
"""从租户中删除用户"""
if current_user.id != tenant_id and current_user.id != user_id:
return get_json_result(
data=False,
@@ -117,9 +130,11 @@ def rm(tenant_id, user_id):
return server_error_response(e)
@manager.route("/list", methods=["GET"]) # noqa: F821
@login_required
def tenant_list():
@router.get("/list")
async def tenant_list(
current_user = Depends(get_current_user)
):
"""获取租户列表"""
try:
users = UserTenantService.get_tenants_by_user_id(current_user.id)
for u in users:
@@ -129,9 +144,12 @@ def tenant_list():
return server_error_response(e)
@manager.route("/agree/<tenant_id>", methods=["PUT"]) # noqa: F821
@login_required
def agree(tenant_id):
@router.put("/agree/{tenant_id}")
async def agree(
tenant_id: str = Path(..., description="租户ID"),
current_user = Depends(get_current_user)
):
"""同意加入租户邀请"""
try:
UserTenantService.filter_update([UserTenant.tenant_id == tenant_id, UserTenant.user_id == current_user.id],
{"role": UserTenantRole.NORMAL})