主要功能: - 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate) - 图片管理 Tab:展示所有图片、手动删除、一键去重 - 搜索结果详情弹窗增加删除按钮(带确认弹窗) - 图片管理卡片点击查看详情(复用 showOcrDetailModal) - 搜索限制和 LLM 批量判断数量可通过网站设置 - MiniMax API 调用添加 reasoning_split=True - 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量 - 版本号升级至 1.2.0
203 lines
6.6 KiB
Python
203 lines
6.6 KiB
Python
"""
|
|
知识页面服务
|
|
提供知识页面的 CRUD 操作
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import List, Optional
|
|
|
|
from sqlalchemy import func, select, text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.base import KnowledgeChunk, KnowledgePage
|
|
from app.schemas.page import PageCreate, PageResponse, PageUpdate
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PageService:
|
|
"""知识页面服务"""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
self.db = db
|
|
|
|
async def create_page(self, data: PageCreate) -> KnowledgePage:
|
|
"""创建知识页面"""
|
|
page = KnowledgePage(
|
|
title=data.title,
|
|
content=data.content,
|
|
source_file=data.source_file,
|
|
course_name=data.course_name,
|
|
teacher_name=data.teacher_name,
|
|
live_date=data.live_date,
|
|
page_number=data.page_number,
|
|
metadata_json=data.metadata_json,
|
|
)
|
|
self.db.add(page)
|
|
await self.db.flush()
|
|
await self.db.refresh(page)
|
|
return page
|
|
|
|
async def get_page(self, page_id: int) -> Optional[KnowledgePage]:
|
|
"""根据 ID 获取知识页面"""
|
|
result = await self.db.execute(
|
|
select(KnowledgePage).where(KnowledgePage.id == page_id)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
async def list_pages(
|
|
self,
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
course_name: Optional[str] = None,
|
|
teacher_name: Optional[str] = None,
|
|
keyword: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
分页查询知识页面列表。
|
|
|
|
Args:
|
|
page: 页码(从 1 开始)
|
|
page_size: 每页数量
|
|
course_name: 按课程名称过滤
|
|
teacher_name: 按讲师名称过滤
|
|
keyword: 关键词搜索(标题或内容)
|
|
|
|
Returns:
|
|
{"total": int, "page": int, "page_size": int, "items": list}
|
|
"""
|
|
query = select(KnowledgePage)
|
|
count_query = select(func.count(KnowledgePage.id))
|
|
|
|
# 过滤条件
|
|
if course_name:
|
|
query = query.where(KnowledgePage.course_name == course_name)
|
|
count_query = count_query.where(KnowledgePage.course_name == course_name)
|
|
if teacher_name:
|
|
query = query.where(KnowledgePage.teacher_name == teacher_name)
|
|
count_query = count_query.where(KnowledgePage.teacher_name == teacher_name)
|
|
if keyword:
|
|
like_pattern = f"%{keyword}%"
|
|
query = query.where(
|
|
KnowledgePage.title.ilike(like_pattern)
|
|
| KnowledgePage.content.ilike(like_pattern)
|
|
)
|
|
count_query = count_query.where(
|
|
KnowledgePage.title.ilike(like_pattern)
|
|
| KnowledgePage.content.ilike(like_pattern)
|
|
)
|
|
|
|
# 总数
|
|
total_result = await self.db.execute(count_query)
|
|
total = total_result.scalar() or 0
|
|
|
|
# 分页
|
|
offset = (page - 1) * page_size
|
|
query = query.order_by(KnowledgePage.created_at.desc()).offset(offset).limit(page_size)
|
|
result = await self.db.execute(query)
|
|
items = result.scalars().all()
|
|
|
|
return {
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"items": items,
|
|
}
|
|
|
|
async def update_page(self, page_id: int, data: PageUpdate) -> Optional[KnowledgePage]:
|
|
"""更新知识页面"""
|
|
page = await self.get_page(page_id)
|
|
if page is None:
|
|
return None
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(page, field, value)
|
|
|
|
await self.db.flush()
|
|
await self.db.refresh(page)
|
|
return page
|
|
|
|
async def delete_page(self, page_id: int) -> bool:
|
|
"""删除知识页面及其关联分块"""
|
|
page = await self.get_page(page_id)
|
|
if page is None:
|
|
return False
|
|
|
|
# 先删除关联的分块
|
|
await self.db.execute(
|
|
text("DELETE FROM knowledge_chunks WHERE page_id = :page_id"),
|
|
{"page_id": page_id},
|
|
)
|
|
# 再删除页面
|
|
await self.db.delete(page)
|
|
await self.db.flush()
|
|
return True
|
|
|
|
async def get_page_chunks(self, page_id: int) -> List[KnowledgeChunk]:
|
|
"""获取知识页面的所有分块"""
|
|
result = await self.db.execute(
|
|
select(KnowledgeChunk)
|
|
.where(KnowledgeChunk.page_id == page_id)
|
|
.order_by(KnowledgeChunk.chunk_index)
|
|
)
|
|
return list(result.scalars().all())
|
|
|
|
async def reindex_page(self, page_id: int) -> dict:
|
|
"""
|
|
重新索引知识页面:重新分块并生成嵌入向量。
|
|
用于页面内容更新后刷新向量索引。
|
|
"""
|
|
page = await self.get_page(page_id)
|
|
if page is None:
|
|
raise ValueError(f"知识页面不存在: {page_id}")
|
|
|
|
# 删除旧分块
|
|
await self.db.execute(
|
|
text("DELETE FROM knowledge_chunks WHERE page_id = :page_id"),
|
|
{"page_id": page_id},
|
|
)
|
|
|
|
# 重新分块
|
|
from app.services.import_service import ImportService
|
|
chunks = ImportService._chunk_text(page.content)
|
|
|
|
if not chunks:
|
|
await self.db.flush()
|
|
return {"page_id": page_id, "chunks": 0}
|
|
|
|
# 生成嵌入并插入
|
|
from app.services.embedding_service import EmbeddingService
|
|
try:
|
|
embeddings = await EmbeddingService.embed_batch(chunks)
|
|
except Exception as exc:
|
|
logger.error("重新索引嵌入失败: %s", exc)
|
|
embeddings = [None] * len(chunks)
|
|
|
|
for idx, (chunk_text, embedding) in enumerate(zip(chunks, embeddings)):
|
|
embedding_str = None
|
|
if embedding:
|
|
embedding_str = "[" + ",".join(str(x) for x in embedding) + "]"
|
|
|
|
await self.db.execute(text("""
|
|
INSERT INTO knowledge_chunks (page_id, chunk_index, content, embedding)
|
|
VALUES (:page_id, :chunk_index, :content, :embedding::vector)
|
|
"""), {
|
|
"page_id": page_id,
|
|
"chunk_index": idx,
|
|
"content": chunk_text,
|
|
"embedding": embedding_str,
|
|
})
|
|
|
|
# 更新全文搜索向量
|
|
await self.db.execute(text("""
|
|
UPDATE knowledge_chunks
|
|
SET search_vector = to_tsvector('chinese_zh', content)
|
|
WHERE page_id = :page_id
|
|
"""), {"page_id": page_id})
|
|
|
|
await self.db.flush()
|
|
return {"page_id": page_id, "chunks": len(chunks)}
|