6
This commit is contained in:
+120
-17
@@ -12,12 +12,14 @@ import json
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, Response
|
||||
from fastapi.responses import HTMLResponse, PlainTextResponse
|
||||
from fastapi import Path as PathParam
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.core.errors import NotFoundError, RateLimitedError
|
||||
from app.core.rate_limit import check_rate_limit
|
||||
from app.core.security import decrypt_token
|
||||
from app.models.document_category import DocumentCategory
|
||||
from app.services.access_log_service import AccessLogService
|
||||
from app.services.kb_public_service import KbPublicService
|
||||
|
||||
@@ -151,10 +153,14 @@ def kb_index_json(
|
||||
"updated_at": doc.updated_at,
|
||||
})
|
||||
|
||||
# 获取目录树
|
||||
category_tree = svc.get_category_tree(kb)
|
||||
|
||||
data = {
|
||||
"name": kb.name,
|
||||
"description": kb.description,
|
||||
"document_count": len(doc_list),
|
||||
"categories": category_tree,
|
||||
"documents": doc_list,
|
||||
}
|
||||
|
||||
@@ -168,15 +174,52 @@ def kb_index_json(
|
||||
def kb_index_html(
|
||||
token: str,
|
||||
request: Request,
|
||||
category: str = Query(None, description="按分类路径过滤,如 /01公司层/公司基本信息/"),
|
||||
page: int = Query(1, ge=1),
|
||||
db: Session = Depends(get_db),
|
||||
) -> HTMLResponse:
|
||||
"""知识库首页(HTML)。"""
|
||||
"""知识库首页(HTML)。支持按目录过滤。"""
|
||||
_rate_limit(request, token)
|
||||
svc = KbPublicService(db)
|
||||
kb = svc.get_kb_by_token(token)
|
||||
_log_access(db, kb.id, f"/k/{token}", request, req_type="html")
|
||||
docs, total = svc.list_documents(kb, page=page, page_size=50)
|
||||
|
||||
# 获取目录树
|
||||
category_tree = svc.get_category_tree(kb)
|
||||
|
||||
# 按分类过滤
|
||||
category_id = None
|
||||
if category:
|
||||
# 根据路径查找分类 ID
|
||||
from sqlalchemy import select
|
||||
stmt = select(DocumentCategory).where(
|
||||
DocumentCategory.knowledge_base_id == kb.id,
|
||||
DocumentCategory.path == category,
|
||||
)
|
||||
cat = db.scalars(stmt).first()
|
||||
if cat:
|
||||
category_id = cat.id
|
||||
|
||||
docs, total = svc.list_documents(kb, category_id=category_id, page=page, page_size=50)
|
||||
|
||||
# 渲染目录树侧边栏
|
||||
def render_tree(nodes: list, level: int = 0) -> str:
|
||||
html = ""
|
||||
for node in nodes:
|
||||
indent = " " * level
|
||||
is_active = category == node["path"]
|
||||
active_class = ' class="active"' if is_active else ""
|
||||
doc_count = f' <span class="count">({node["doc_count"]})</span>' if node["doc_count"] > 0 else ""
|
||||
|
||||
if node["is_folder"]:
|
||||
html += f'{indent}<li{active_class}><a href="/k/{token}?category={node["path"]}">{node["name"]}</a>{doc_count}</li>\n'
|
||||
if node["children"]:
|
||||
html += f'{indent}<ul>\n{render_tree(node["children"], level + 1)}{indent}</ul>\n'
|
||||
else:
|
||||
html += f'{indent}<li{active_class}><a href="/k/{token}?category={node["path"]}">{node["name"]}</a>{doc_count}</li>\n'
|
||||
return html
|
||||
|
||||
tree_html = render_tree(category_tree)
|
||||
|
||||
doc_rows = ""
|
||||
for doc in docs:
|
||||
@@ -208,8 +251,19 @@ def kb_index_html(
|
||||
{_robots_meta()}
|
||||
{_referrer_meta()}
|
||||
<style>
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; }}
|
||||
h1 {{ color: #333; }}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 20px; line-height: 1.6; display: flex; gap: 30px; }}
|
||||
.sidebar {{ width: 280px; flex-shrink: 0; }}
|
||||
.main {{ flex: 1; max-width: 800px; }}
|
||||
h1 {{ color: #333; margin-top: 0; }}
|
||||
.tree {{ list-style: none; padding: 0; margin: 0; }}
|
||||
.tree ul {{ list-style: none; padding-left: 20px; margin: 0; }}
|
||||
.tree li {{ padding: 6px 10px; border-radius: 4px; }}
|
||||
.tree li:hover {{ background: #f5f7fa; }}
|
||||
.tree li.active {{ background: #ecf5ff; }}
|
||||
.tree a {{ color: #333; text-decoration: none; font-size: 0.95em; }}
|
||||
.tree a:hover {{ color: #409eff; }}
|
||||
.tree .count {{ color: #999; font-size: 0.85em; }}
|
||||
.doc-item {{ border-bottom: 1px solid #eee; padding: 15px 0; }}
|
||||
.doc-item h3 {{ margin: 0 0 5px 0; }}
|
||||
.doc-item a {{ color: #0066cc; text-decoration: none; }}
|
||||
@@ -220,17 +274,29 @@ def kb_index_html(
|
||||
.pagination {{ color: #666; font-size: 0.9em; text-align: center; }}
|
||||
.status-badge {{ color: #e67e22; font-size: 0.8em; font-weight: normal; }}
|
||||
.footer {{ margin-top: 30px; padding-top: 15px; border-top: 1px solid #eee; color: #999; font-size: 0.85em; }}
|
||||
.back-link {{ display: inline-block; margin-bottom: 15px; color: #0066cc; text-decoration: none; }}
|
||||
.back-link:hover {{ text-decoration: underline; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{kb.name}</h1>
|
||||
{"<p>" + kb.description + "</p>" if kb.description else ""}
|
||||
<h2>文档列表</h2>
|
||||
{doc_rows if doc_rows else "<p>暂无文档。</p>"}
|
||||
{pagination}
|
||||
<div class="footer">
|
||||
<p>This page is an AI-readable knowledge base index. Use the document links above to retrieve specific documents.</p>
|
||||
<p>本页为 AI 可读知识库目录,请通过上述文档链接获取具体内容。</p>
|
||||
<div class="sidebar">
|
||||
<h2 style="margin-top: 0; font-size: 1.1em;">目录</h2>
|
||||
<ul class="tree">
|
||||
<li{" class='active'" if not category else ""}><a href="/k/{token}">全部文档</a> <span class="count">({total})</span></li>
|
||||
{tree_html}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main">
|
||||
<h1>{kb.name}</h1>
|
||||
{"<p>" + kb.description + "</p>" if kb.description else ""}
|
||||
{"<a href='/k/" + token + "' class='back-link'>← 返回全部文档</a>" if category else ""}
|
||||
<h2>{"当前分类:" + category if category else "文档列表"}</h2>
|
||||
{doc_rows if doc_rows else "<p>暂无文档。</p>"}
|
||||
{pagination}
|
||||
<div class="footer">
|
||||
<p>This page is an AI-readable knowledge base index. Use the document links above to retrieve specific documents.</p>
|
||||
<p>本页为 AI 可读知识库目录,请通过上述文档链接获取具体内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -240,12 +306,49 @@ def kb_index_html(
|
||||
# --- 单文档访问 ---
|
||||
|
||||
|
||||
@router.get("/{token}/doc/{doc_token}")
|
||||
def doc_page_html(
|
||||
@router.get("/{token}/doc/{doc_token}.md")
|
||||
def doc_page_markdown(
|
||||
token: str,
|
||||
doc_token: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> PlainTextResponse:
|
||||
"""文档(Markdown)。"""
|
||||
_rate_limit(request, token)
|
||||
svc = KbPublicService(db)
|
||||
kb = svc.get_kb_by_token(token)
|
||||
doc = svc.get_document_by_token(kb, doc_token)
|
||||
_log_access(db, kb.id, f"/k/{token}/doc/{doc_token}.md", request, doc_id=doc.id, req_type="doc_md")
|
||||
markdown_content = svc.get_document_content(doc)
|
||||
return PlainTextResponse(content=markdown_content, media_type="text/markdown")
|
||||
|
||||
|
||||
@router.get("/{token}/doc/{doc_token}.txt")
|
||||
def doc_page_text(
|
||||
token: str,
|
||||
doc_token: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
) -> PlainTextResponse:
|
||||
"""文档(纯文本)。"""
|
||||
_rate_limit(request, token)
|
||||
svc = KbPublicService(db)
|
||||
kb = svc.get_kb_by_token(token)
|
||||
doc = svc.get_document_by_token(kb, doc_token)
|
||||
_log_access(db, kb.id, f"/k/{token}/doc/{doc_token}.txt", request, doc_id=doc.id, req_type="doc_txt")
|
||||
markdown_content = svc.get_document_content(doc)
|
||||
import re
|
||||
text = re.sub(r"[#*_`\[\]()>]", "", markdown_content)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return PlainTextResponse(content=text.strip(), media_type="text/plain")
|
||||
|
||||
|
||||
@router.get("/{token}/doc/{doc_token}")
|
||||
def doc_page_html(
|
||||
token: str,
|
||||
doc_token: str = PathParam(pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
request: Request = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> HTMLResponse:
|
||||
"""文档页面(HTML)。"""
|
||||
_rate_limit(request, token)
|
||||
@@ -253,7 +356,7 @@ def doc_page_html(
|
||||
kb = svc.get_kb_by_token(token)
|
||||
doc = svc.get_document_by_token(kb, doc_token)
|
||||
_log_access(db, kb.id, f"/k/{token}/doc/{doc_token}", request, doc_id=doc.id, req_type="doc_html")
|
||||
markdown_content = svc.get_document_markdown(doc)
|
||||
markdown_content = svc.get_document_content(doc)
|
||||
|
||||
# Markdown → HTML(简单转换)
|
||||
html_content = _markdown_to_html(markdown_content)
|
||||
@@ -313,7 +416,7 @@ def doc_page_markdown(
|
||||
kb = svc.get_kb_by_token(token)
|
||||
doc = svc.get_document_by_token(kb, doc_token)
|
||||
_log_access(db, kb.id, f"/k/{token}/doc/{doc_token}.md", request, doc_id=doc.id, req_type="doc_md")
|
||||
markdown_content = svc.get_document_markdown(doc)
|
||||
markdown_content = svc.get_document_content(doc)
|
||||
return PlainTextResponse(content=markdown_content, media_type="text/markdown")
|
||||
|
||||
|
||||
@@ -330,7 +433,7 @@ def doc_page_text(
|
||||
kb = svc.get_kb_by_token(token)
|
||||
doc = svc.get_document_by_token(kb, doc_token)
|
||||
_log_access(db, kb.id, f"/k/{token}/doc/{doc_token}.txt", request, doc_id=doc.id, req_type="doc_txt")
|
||||
markdown_content = svc.get_document_markdown(doc)
|
||||
markdown_content = svc.get_document_content(doc)
|
||||
|
||||
# 去掉 Markdown 标记
|
||||
import re
|
||||
|
||||
Reference in New Issue
Block a user