feat: 知识库笔记工坊立项 + contributor 功能 + Fable5 视角文档

- 知识库笔记工坊子项目立项:PRD v1.0 + 寄存条 + CLAUDE.md 登记
- 后端:知识库上传记录 contributor、新增 /contributors 接口
- 前端:知识库列表支持按贡献人筛选
- 新增 Fable5 视角模式库/案例集/建议文档

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
simonkoson
2026-07-10 18:06:42 +08:00
parent db9d10a795
commit 8be51ebbde
12 changed files with 836 additions and 57 deletions
+11 -1
View File
@@ -37,7 +37,7 @@ async def upload_md_files(
continue
try:
content = await f.read()
item = svc.store_md_file(content, f.filename)
item = svc.store_md_file(content, f.filename, contributor=current_user.display_name or current_user.username)
results.append({
"id": item.id,
"title": item.title,
@@ -93,6 +93,16 @@ def list_distinct_sources(
return [{"source": s} for s in sources]
@router.get("/contributors")
def list_distinct_contributors(
session: Session = Depends(get_session),
current_user: User = Depends(require_role(UserRole.zhipianren, UserRole.zebian, UserRole.biandao)),
):
svc = KnowledgeService()
contributors = svc.get_distinct_contributors()
return [{"contributor": c} for c in contributors]
@router.get("/grouped")
def get_grouped_knowledge_items(
session: Session = Depends(get_session),
+19 -2
View File
@@ -187,12 +187,16 @@ class KnowledgeService:
import re
return re.findall(r"\[\[([^\]]+)\]\]", text)
def store_md_file(self, file_content: bytes, file_name: str) -> KnowledgeItem:
def store_md_file(self, file_content: bytes, file_name: str, contributor: Optional[str] = None) -> KnowledgeItem:
"""
读取一篇 md 内容,调用 embo-01 拿到向量,写入 knowledge_items + knowledge_embeddings
"""
parsed = self.parse_md_file(file_content, file_name)
metadata = parsed["metadata"] or {}
if contributor:
metadata["contributor"] = contributor
# 调用 embeddingtype="db" 表示存入知识库)
embedding_list = self.embedder.embed_single(parsed["content_md"], embed_type="db")
@@ -204,7 +208,7 @@ class KnowledgeService:
source_file_name=parsed["source_file_name"],
author=parsed["author"],
publish_date=parsed["publish_date"],
tags=parsed["metadata"],
tags=metadata if metadata else None,
related_entities=parsed["related_entities"],
)
session.add(item)
@@ -244,6 +248,7 @@ class KnowledgeService:
# 从 tags(JSONB) 取 source_detail
tags = item.tags or {}
source_detail = tags.get("source_detail") if isinstance(tags, dict) else None
contributor = tags.get("contributor") if isinstance(tags, dict) else None
results.append({
"id": item.id,
"title": item.title,
@@ -252,6 +257,7 @@ class KnowledgeService:
"source_type": item.source_type,
"source_file_name": item.source_file_name,
"source_detail": source_detail,
"contributor": contributor or "栏目冷启动",
"created_at": item.created_at,
})
return results
@@ -267,6 +273,17 @@ class KnowledgeService:
sources.add(tags["source_detail"])
return sorted(list(sources))
def get_distinct_contributors(self) -> list[str]:
"""返回库里所有不重复的贡献人(从 JSONB 提取),供筛选下拉用。"""
with Session(engine) as session:
items = session.exec(select(KnowledgeItem)).all()
contributors = set()
for item in items:
tags = item.tags or {}
c = tags.get("contributor") if isinstance(tags, dict) else None
contributors.add(c or "栏目冷启动")
return sorted(list(contributors))
def search_similar(self, query_text: str, top_k: int = 10, min_similarity: float = 0.3) -> list[dict]:
"""
语义检索:查询句转为向量,用 SQL 余弦距离(<=>)在数据库层检索