Files

217 lines
7.6 KiB
Python
Raw Permalink Normal View History

# =============================================================================
# 企微IT智能服务台 — RAGFlow 文档摄入 APITier1 新增 / 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,
}