217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
|
|
# =============================================================================
|
|||
|
|
# 企微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,
|
|||
|
|
}
|