Files
HuiBrain/app/api/v1/import_export.py
EduBrain Dev b17786b57b feat: v1.2.0 - 图片去重与管理、微信机器人优化、搜索设置可配置
主要功能:
- 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate)
- 图片管理 Tab:展示所有图片、手动删除、一键去重
- 搜索结果详情弹窗增加删除按钮(带确认弹窗)
- 图片管理卡片点击查看详情(复用 showOcrDetailModal)
- 搜索限制和 LLM 批量判断数量可通过网站设置
- MiniMax API 调用添加 reasoning_split=True
- 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量
- 版本号升级至 1.2.0
2026-04-13 22:25:08 +08:00

216 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
导入导出 API 路由
提供文件导入和知识库导出接口
支持 .md / .txt / .docx 格式
"""
from __future__ import annotations
import json
import os
from datetime import datetime
from typing import Optional
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db
from app.services.import_service import ImportService
router = APIRouter()
# 支持的导入文件格式
SUPPORTED_EXTENSIONS = (".md", ".txt", ".docx")
@router.post("/file", summary="从文件导入知识")
async def import_from_file(
file: UploadFile = File(..., description="上传文件(.md / .txt / .docx"),
course_name: Optional[str] = Form(None, description="课程名称"),
teacher_name: Optional[str] = Form(None, description="讲师名称"),
live_date: Optional[str] = Form(None, description="直播日期 (YYYY-MM-DD)"),
db: AsyncSession = Depends(get_db),
):
"""
上传文件并导入到知识库。
- Markdown (.md):解析 frontmatter 元数据,按 ## 标题分页
- 纯文本 (.txt):整体作为一个页面
- Word 文档 (.docx):提取文本,按标题分页
- 导入后自动分块、生成嵌入向量、建立全文索引
"""
# 验证文件类型
if not file.filename:
raise HTTPException(status_code=400, detail="文件名不能为空")
suffix = os.path.splitext(file.filename)[1].lower()
if suffix not in SUPPORTED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"仅支持 {', '.join(SUPPORTED_EXTENSIONS)} 文件",
)
# 保存上传文件到 transcripts 目录
dest_dir = settings.transcripts_dir
dest_path = dest_dir / file.filename
# 如果文件已存在,添加序号
counter = 1
original_stem = os.path.splitext(file.filename)[0]
while dest_path.exists():
dest_path = dest_dir / f"{original_stem}_{counter}{suffix}"
counter += 1
# 写入文件
content = await file.read()
dest_path.write_bytes(content)
# 执行导入
service = ImportService(db)
try:
result = await service.import_file(
file_path=str(dest_path),
course_name=course_name,
teacher_name=teacher_name,
live_date=live_date,
)
return {
"message": "导入成功",
"file": str(dest_path),
**result,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"导入失败: {str(e)}")
@router.post("/directory", summary="从目录批量导入")
async def import_from_directory(
directory: str = Form(..., description="数据目录中的子目录名(相对于 transcripts 目录)"),
course_name: Optional[str] = Form(None, description="课程名称"),
teacher_name: Optional[str] = Form(None, description="讲师名称"),
live_date: Optional[str] = Form(None, description="直播日期"),
db: AsyncSession = Depends(get_db),
):
"""
批量导入指定目录下的所有文件(.md / .txt / .docx
"""
target_dir = settings.transcripts_dir / directory
if not target_dir.exists():
raise HTTPException(status_code=404, detail=f"目录不存在: {directory}")
service = ImportService(db)
total_pages = 0
total_chunks = 0
files_processed = 0
errors: list[str] = []
for filepath in sorted(target_dir.iterdir()):
if filepath.suffix.lower() not in SUPPORTED_EXTENSIONS:
continue
try:
result = await service.import_file(
file_path=str(filepath),
course_name=course_name,
teacher_name=teacher_name,
live_date=live_date,
)
total_pages += result["pages"]
total_chunks += result["chunks"]
files_processed += 1
except Exception as e:
errors.append(f"{filepath.name}: {str(e)}")
return {
"message": "批量导入完成",
"files_processed": files_processed,
"total_pages": total_pages,
"total_chunks": total_chunks,
"errors": errors if errors else None,
}
@router.get("/export", summary="导出知识库")
async def export_knowledge_base(
format: str = "json",
course_name: Optional[str] = None,
db: AsyncSession = Depends(get_db),
):
"""
导出知识库内容。
Args:
format: 导出格式,支持 json / markdown
course_name: 按课程名称过滤导出
"""
from sqlalchemy import text
query = text("""
SELECT id, title, content, source_file, course_name, teacher_name,
live_date, page_number, metadata_json, created_at
FROM knowledge_pages
WHERE (:course_name IS NULL OR course_name = :course_name)
ORDER BY created_at DESC
""")
result = await db.execute(query, {"course_name": course_name})
pages = result.fetchall()
if format == "markdown":
# 导出为 Markdown 文件
lines = []
for row in pages:
lines.append(f"# {row.title}")
lines.append("")
meta_parts = []
if row.course_name:
meta_parts.append(f"课程: {row.course_name}")
if row.teacher_name:
meta_parts.append(f"讲师: {row.teacher_name}")
if row.live_date:
meta_parts.append(f"日期: {row.live_date}")
if meta_parts:
lines.append(" | ".join(meta_parts))
lines.append("")
lines.append(row.content)
lines.append("")
lines.append("---")
lines.append("")
export_content = "\n".join(lines)
ext = "md"
else:
# 导出为 JSON
pages_data = []
for row in pages:
pages_data.append({
"id": row.id,
"title": row.title,
"content": row.content,
"source_file": row.source_file,
"course_name": row.course_name,
"teacher_name": row.teacher_name,
"live_date": row.live_date,
"page_number": row.page_number,
"metadata_json": row.metadata_json,
"created_at": str(row.created_at),
})
export_content = json.dumps(pages_data, ensure_ascii=False, indent=2)
ext = "json"
# 保存到 exports 目录
export_dir = settings.exports_dir
export_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"export_{timestamp}.{ext}"
export_path = export_dir / filename
export_path.write_text(export_content, encoding="utf-8")
return {
"message": "导出成功",
"file": filename,
"pages": len(pages),
"format": format,
}