# ============================================================================= # 企微IT智能服务台 — RAGFlow 文档摄入 API(Tier1 新增 / P1-5 / 通道 C) # ============================================================================= # 说明:RAGFlow 文档摄入接口,训练师上传非标准格式文档, # 经 RAGFlow ETL 整理/结构化后生成 KnowledgeSuggestion 进审批队列。 # # 1. POST /api/ragflow/ingest — 上传文档触发 RAGFlow 处理 # 2. GET /api/ragflow/tasks/{task_id} — 查询处理任务状态 # # P1-5 硬约束: # - 触发方式:训练师手动上传(非定时扫描) # - 支持格式:.docx/.pdf/.txt/.png/.jpg # - source_type=document_ragflow, audience=engineer_workguide # - 产出走 D7 审批流 # ============================================================================= import logging import uuid from typing import Optional from fastapi import APIRouter, Depends, File, Form, Query, UploadFile from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, require_admin, UserInfo from app.models.knowledge_suggestion import KnowledgeSuggestion from app.schemas.enums import ( AudienceEnum, GraphSyncStatusEnum, SourceTypeEnum, SuggestionStatusEnum, ) from app.services.ragflow_ingestion_service import RagflowIngestionService logger = logging.getLogger(__name__) router = APIRouter() # 支持的文件格式 ALLOWED_EXTENSIONS = {".docx", ".pdf", ".txt", ".png", ".jpg", ".jpeg"} ALLOWED_MIME_TYPES = { "application/vnd.openxmlformats-officedocument.wordprocessingml.document", # .docx "application/pdf", # .pdf "text/plain", # .txt "image/png", # .png "image/jpeg", # .jpg/.jpeg } # 文件大小上限(20MB) MAX_FILE_SIZE = 20 * 1024 * 1024 # 内存中的任务状态缓存(生产环境应迁移到 Redis) _task_cache: dict = {} # ----------------------------------------------------------------------------- # 上传文档触发 RAGFlow 处理(Tier1 新增) # ----------------------------------------------------------------------------- # POST /api/ragflow/ingest @router.post("/ingest") @require_admin async def ingest_document( file: UploadFile = File(..., description="文档文件(.docx/.pdf/.txt/.png/.jpg)"), category_hint: str = Form( default="其他", description="分类提示(可选,帮助RAGFlow归类):硬件/软件/网络/安全/账号/其他", ), current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """上传非标准格式文档到 RAGFlow 进行 ETL 处理。 训练师上传文档后,RAGFlow 自动整理/筛选/结构化内容, 生成 KnowledgeSuggestion 提案进入 D7 审批队列。 **请求格式**: multipart/form-data **字段说明**: - **file**: 文档文件(必填,支持 .docx/.pdf/.txt/.png/.jpg) - **category_hint**: 分类提示(可选,默认"其他") **文件大小限制**: 最大 20MB **处理时间**: 最长等待 5 分钟,超时返回 pending 状态 **需要管理员权限。** """ # 校验文件扩展名 file_name = file.filename or "unknown" ext = "." + file_name.rsplit(".", 1)[-1].lower() if "." in file_name else "" if ext not in ALLOWED_EXTENSIONS: return { "code": 400, "message": f"不支持的文件格式: {ext},仅支持 {', '.join(ALLOWED_EXTENSIONS)}", "data": None, } # 校验 MIME 类型(如可获取) if file.content_type and file.content_type not in ALLOWED_MIME_TYPES: logger.warning( f"文件 MIME 类型不在白名单中: {file.content_type},仍允许上传" ) # 读取文件内容 file_data = await file.read() # 校验文件大小 if len(file_data) > MAX_FILE_SIZE: return { "code": 400, "message": f"文件过大({len(file_data) / 1024 / 1024:.1f}MB),最大支持 20MB", "data": None, } if len(file_data) == 0: return { "code": 400, "message": "文件内容为空", "data": None, } # 调用 RAGFlow Ingestion 服务 service = RagflowIngestionService() logger.info( f"管理员 {current_user.name} 上传文档到 RAGFlow: " f"file_name={file_name}, category_hint={category_hint}, size={len(file_data)}" ) result = await service.upload_and_process(file_data, file_name, category_hint) # 将生成的 suggestions 写入数据库(pending 状态) saved_suggestions = [] if result.get("suggestions"): for sug_data in result["suggestions"]: suggestion = KnowledgeSuggestion( suggestion_type=sug_data.get("suggestion_type", "new_faq"), status=SuggestionStatusEnum.pending.value, title=sug_data.get("title", ""), content=sug_data.get("content", ""), category=sug_data.get("category", category_hint), tags=sug_data.get("tags", []), source_type=SourceTypeEnum.document_ragflow.value, source_data=sug_data.get("source_data", []), reason=sug_data.get("reason", ""), confidence=sug_data.get("confidence", 0.85), audience=AudienceEnum.engineer_workguide.value, # 通道 C 默认 issue=sug_data.get("issue", ""), action=sug_data.get("action", ""), relation_type=sug_data.get("relation_type", "LEADS_TO"), parent_issue=sug_data.get("parent_issue", ""), graph_meta=sug_data.get("graph_meta", {}), graph_sync_status=GraphSyncStatusEnum.pending.value, source_failed=sug_data.get("source_failed", False), ) db.add(suggestion) saved_suggestions.append({ "title": suggestion.title, "category": suggestion.category, "confidence": suggestion.confidence, }) await db.commit() logger.info(f"RAGFlow 生成 {len(saved_suggestions)} 条 KnowledgeSuggestion 待审批") # 缓存任务状态 task_id = result["task_id"] _task_cache[task_id] = { "task_id": task_id, "status": result["status"], "file_name": file_name, "created_at": __import__("datetime").datetime.now().isoformat(), "suggestions_count": len(saved_suggestions), } return { "code": 0, "message": "文档已提交 RAGFlow 处理", "data": { "task_id": task_id, "status": result["status"], "file_name": file_name, "suggestions_count": len(saved_suggestions), "suggestions": saved_suggestions, }, } # ----------------------------------------------------------------------------- # 查询处理任务状态(Tier1 新增) # ----------------------------------------------------------------------------- # GET /api/ragflow/tasks/{task_id} @router.get("/tasks/{task_id}") @require_admin async def get_ingestion_task_status( task_id: str, current_user: UserInfo = Depends(get_current_user), ): """查询 RAGFlow 文档处理任务状态。 - **task_id**: 任务ID(来自 ingest 接口返回值) **需要管理员权限。** """ task = _task_cache.get(task_id) if not task: return { "code": 404, "message": "任务不存在或已过期", "data": None, } return { "code": 0, "message": "success", "data": task, }