6
This commit is contained in:
@@ -41,6 +41,7 @@ class DocumentService:
|
||||
kb_id: str,
|
||||
filename: str,
|
||||
content: bytes,
|
||||
category_id: str | None = None,
|
||||
) -> Document:
|
||||
"""上传文档。
|
||||
|
||||
@@ -111,6 +112,7 @@ class DocumentService:
|
||||
doc_token_hash=doc_token_hash,
|
||||
doc_token_encrypted=doc_token_encrypted,
|
||||
doc_token_hint=doc_token_hint,
|
||||
category_id=category_id,
|
||||
)
|
||||
|
||||
# 更新存储路径中的 document_id
|
||||
@@ -200,3 +202,87 @@ class DocumentService:
|
||||
|
||||
processor = LocalDocumentProcessor(self._session)
|
||||
processor.process(doc.id)
|
||||
|
||||
def create_text(
|
||||
self,
|
||||
user: User,
|
||||
kb_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
content_format: str = "markdown",
|
||||
category_id: str | None = None,
|
||||
) -> Document:
|
||||
"""创建文本内容文档(无需上传文件)。"""
|
||||
# 校验 KB 归属
|
||||
kb = self._kb_repo.get_by_id(kb_id)
|
||||
if kb is None or kb.user_id != user.id or kb.status == "DELETED":
|
||||
raise NotFoundError("知识库不存在。")
|
||||
|
||||
# 生成文档 token
|
||||
doc_token = generate_token()
|
||||
doc_token_hash = hash_token(doc_token)
|
||||
doc_token_encrypted = encrypt_token(doc_token)
|
||||
doc_token_hint = doc_token[-8:] if len(doc_token) >= 8 else doc_token
|
||||
|
||||
# 计算内容大小
|
||||
content_size = len(content.encode("utf-8"))
|
||||
|
||||
# 配额检查
|
||||
self._check_quota(user, content_size)
|
||||
|
||||
# 从内容提取摘要和关键词
|
||||
summary = self._extract_summary(content)
|
||||
keywords = self._extract_keywords(content)
|
||||
|
||||
# 创建文档记录
|
||||
doc = Document(
|
||||
knowledge_base_id=kb_id,
|
||||
user_id=user.id,
|
||||
category_id=category_id,
|
||||
original_filename=title,
|
||||
storage_path=None, # 文本内容不存储文件
|
||||
markdown_path=None,
|
||||
content=content,
|
||||
content_format=content_format,
|
||||
file_size=content_size,
|
||||
mime_type="text/markdown" if content_format == "markdown" else "text/plain",
|
||||
file_ext=".md" if content_format == "markdown" else ".txt",
|
||||
sha256=None,
|
||||
doc_token_hash=doc_token_hash,
|
||||
doc_token_encrypted=doc_token_encrypted,
|
||||
doc_token_hint=doc_token_hint,
|
||||
title=title,
|
||||
content_summary=summary,
|
||||
keywords=keywords,
|
||||
status="READY", # 文本内容直接就绪
|
||||
)
|
||||
self._session.add(doc)
|
||||
|
||||
# 扣减配额
|
||||
self._deduct_quota(user, content_size)
|
||||
|
||||
self._session.commit()
|
||||
return doc
|
||||
|
||||
def _extract_summary(self, content: str, max_len: int = 200) -> str:
|
||||
"""提取摘要。"""
|
||||
import re
|
||||
|
||||
text = re.sub(r"[#*_`\[\]()>]", "", content)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len] + "..."
|
||||
|
||||
def _extract_keywords(self, content: str, top_k: int = 10) -> str:
|
||||
"""提取关键词。"""
|
||||
try:
|
||||
import jieba.analyse
|
||||
import re
|
||||
|
||||
text = re.sub(r"[#*_`\[\]()>]", "", content)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
keywords = jieba.analyse.extract_tags(text, topK=top_k)
|
||||
return ",".join(keywords)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"""公共知识库服务(Phase 10/11/12 统一数据来源)。
|
||||
|
||||
HTML/MD/TXT/JSON/搜索全部通过此 Service 获取数据,不各自写查询逻辑。
|
||||
支持目录树结构和分类过滤。
|
||||
"""
|
||||
|
||||
from app.core.errors import NotFoundError
|
||||
from app.core.security import hash_token
|
||||
from app.models.document import Document
|
||||
from app.models.document_category import DocumentCategory
|
||||
from app.models.knowledge_base import KnowledgeBase
|
||||
from app.repositories.doc_repo import DocumentRepository
|
||||
from app.repositories.kb_repo import KnowledgeBaseRepository
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
@@ -26,12 +29,55 @@ class KbPublicService:
|
||||
raise NotFoundError("知识库不存在。")
|
||||
return kb
|
||||
|
||||
def get_category_tree(self, kb: KnowledgeBase) -> list[dict]:
|
||||
"""获取目录树(含文档数量)。"""
|
||||
stmt = (
|
||||
select(DocumentCategory)
|
||||
.where(DocumentCategory.knowledge_base_id == kb.id)
|
||||
.order_by(DocumentCategory.sort_order, DocumentCategory.name)
|
||||
)
|
||||
all_cats = list(self._session.scalars(stmt).all())
|
||||
|
||||
# 获取每个分类的文档数量
|
||||
count_stmt = (
|
||||
select(Document.category_id, func.count())
|
||||
.where(
|
||||
Document.knowledge_base_id == kb.id,
|
||||
Document.status != "DELETED",
|
||||
)
|
||||
.group_by(Document.category_id)
|
||||
)
|
||||
doc_count_map = dict(self._session.execute(count_stmt).all())
|
||||
|
||||
# 构建树
|
||||
cat_map = {}
|
||||
for cat in all_cats:
|
||||
cat_map[cat.id] = {
|
||||
"id": cat.id,
|
||||
"name": cat.name,
|
||||
"path": cat.path or "/",
|
||||
"is_folder": getattr(cat, 'is_folder', True),
|
||||
"sort_order": cat.sort_order,
|
||||
"doc_count": doc_count_map.get(cat.id, 0),
|
||||
"children": [],
|
||||
}
|
||||
|
||||
roots = []
|
||||
for cat in all_cats:
|
||||
node = cat_map[cat.id]
|
||||
if cat.parent_id and cat.parent_id in cat_map:
|
||||
cat_map[cat.parent_id]["children"].append(node)
|
||||
else:
|
||||
roots.append(node)
|
||||
|
||||
return roots
|
||||
|
||||
def list_documents(
|
||||
self, kb: KnowledgeBase, *, page: int = 1, page_size: int = 50
|
||||
self, kb: KnowledgeBase, *, category_id: str | None = None, page: int = 1, page_size: int = 50
|
||||
) -> tuple[list[Document], int]:
|
||||
"""获取知识库的文档列表(排除 DELETED)。"""
|
||||
"""获取知识库的文档列表(排除 DELETED)。支持按分类过滤。"""
|
||||
return self._doc_repo.list_by_knowledge_base(
|
||||
kb.id, page=page, page_size=page_size
|
||||
kb.id, category_id=category_id, page=page, page_size=page_size
|
||||
)
|
||||
|
||||
def get_document_by_token(self, kb: KnowledgeBase, doc_token: str) -> Document:
|
||||
@@ -42,34 +88,38 @@ class KbPublicService:
|
||||
raise NotFoundError("文档不存在。")
|
||||
return doc
|
||||
|
||||
def get_document_markdown(self, doc: Document) -> str:
|
||||
"""读取文档的 Markdown 内容。"""
|
||||
from app.storage.local_storage import get_storage
|
||||
def get_document_content(self, doc: Document) -> str:
|
||||
"""获取文档内容(优先直接内容,其次 Markdown 文件)。"""
|
||||
# 直接文本内容
|
||||
if doc.content:
|
||||
return doc.content
|
||||
|
||||
if not doc.markdown_path:
|
||||
return ""
|
||||
storage = get_storage()
|
||||
content = storage.read(doc.markdown_path)
|
||||
return content.decode("utf-8")
|
||||
# 从文件读取 Markdown
|
||||
if doc.markdown_path:
|
||||
from app.storage.local_storage import get_storage
|
||||
|
||||
storage = get_storage()
|
||||
content = storage.read(doc.markdown_path)
|
||||
return content.decode("utf-8")
|
||||
|
||||
return ""
|
||||
|
||||
def search_documents(
|
||||
self, kb: KnowledgeBase, query: str, *, page: int = 1, page_size: int = 20
|
||||
) -> tuple[list[dict], int]:
|
||||
"""关键词搜索文档(Phase 12:SQLite LIKE / FTS5)。"""
|
||||
# MVP 简化:使用 LIKE 搜索标题+描述+关键词
|
||||
from sqlalchemy import func, or_, select
|
||||
|
||||
conditions = [
|
||||
Document.knowledge_base_id == kb.id,
|
||||
Document.status == "READY",
|
||||
]
|
||||
|
||||
like_pattern = f"%{query}%"
|
||||
search_condition = or_(
|
||||
Document.title.like(like_pattern),
|
||||
Document.description.like(like_pattern),
|
||||
Document.keywords.like(like_pattern),
|
||||
Document.content_summary.like(like_pattern),
|
||||
search_condition = (
|
||||
Document.title.like(like_pattern)
|
||||
| Document.description.like(like_pattern)
|
||||
| Document.keywords.like(like_pattern)
|
||||
| Document.content_summary.like(like_pattern)
|
||||
| Document.content.like(like_pattern)
|
||||
)
|
||||
conditions.append(search_condition)
|
||||
|
||||
@@ -87,6 +137,13 @@ class KbPublicService:
|
||||
|
||||
results = []
|
||||
for doc in docs:
|
||||
# 获取分类路径
|
||||
category_path = ""
|
||||
if doc.category_id:
|
||||
cat = self._session.get(DocumentCategory, doc.category_id)
|
||||
if cat:
|
||||
category_path = cat.path or ""
|
||||
|
||||
results.append({
|
||||
"id": doc.id,
|
||||
"title": doc.title or doc.original_filename,
|
||||
@@ -95,6 +152,7 @@ class KbPublicService:
|
||||
"file_type": doc.file_ext,
|
||||
"updated_at": doc.updated_at,
|
||||
"url_hint": doc.doc_token_hint,
|
||||
"category_path": category_path,
|
||||
})
|
||||
|
||||
return results, total
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""知识库服务:CRUD + Token 管理。"""
|
||||
"""知识库服务:CRUD + Token 管理 + 默认目录树。"""
|
||||
|
||||
from app.core.errors import NotFoundError, PermissionDeniedError
|
||||
from app.models.document_category import DocumentCategory
|
||||
from app.models.knowledge_base import KnowledgeBase
|
||||
from app.models.user import User
|
||||
from app.repositories.kb_repo import KnowledgeBaseRepository
|
||||
@@ -18,6 +19,7 @@ class KnowledgeBaseService:
|
||||
"""创建知识库。返回 (kb, full_token)。
|
||||
|
||||
full_token 仅此一次返回,用于构建完整 AI URL。
|
||||
自动创建默认目录树结构。
|
||||
"""
|
||||
token, token_hash, token_encrypted, token_hint = self._token_svc.create_token_pair()
|
||||
kb = self._kb_repo.create(
|
||||
@@ -28,9 +30,46 @@ class KnowledgeBaseService:
|
||||
token_encrypted=token_encrypted,
|
||||
token_hint=token_hint,
|
||||
)
|
||||
self._seed_default_categories(kb.id)
|
||||
self._session.commit()
|
||||
return kb, token
|
||||
|
||||
def _seed_default_categories(self, kb_id: str) -> None:
|
||||
"""创建默认目录树结构。"""
|
||||
default_tree = [
|
||||
("01 公司层", ["公司基本信息", "经营理念", "四大价值", "A/M/B三态"]),
|
||||
("02 战略层", ["公司战略", "客户战略", "AI战略", "产品战略"]),
|
||||
("03 部门层", ["企划", "技术", "交付", "市场"]),
|
||||
("04 岗位/AI角色", []),
|
||||
("05 业务知识", ["价值创造", "价值传递", "价值交付", "价值支持"]),
|
||||
("06 资产库", ["文案", "SOP", "模板", "案例", "Prompt"]),
|
||||
]
|
||||
|
||||
for i, (folder_name, children) in enumerate(default_tree):
|
||||
# 创建顶层文件夹
|
||||
folder = DocumentCategory(
|
||||
knowledge_base_id=kb_id,
|
||||
parent_id=None,
|
||||
name=folder_name,
|
||||
path=f"/{folder_name}/",
|
||||
is_folder=True,
|
||||
sort_order=i,
|
||||
)
|
||||
self._session.add(folder)
|
||||
self._session.flush()
|
||||
|
||||
# 创建子分类
|
||||
for j, child_name in enumerate(children):
|
||||
child = DocumentCategory(
|
||||
knowledge_base_id=kb_id,
|
||||
parent_id=folder.id,
|
||||
name=child_name,
|
||||
path=f"/{folder_name}/{child_name}/",
|
||||
is_folder=False,
|
||||
sort_order=j,
|
||||
)
|
||||
self._session.add(child)
|
||||
|
||||
def get_or_404(self, kb_id: str, user: User) -> KnowledgeBase:
|
||||
"""获取知识库,校验所有权。不存在或无权 → 404。"""
|
||||
kb = self._kb_repo.get_by_id(kb_id)
|
||||
|
||||
Reference in New Issue
Block a user