dfd8421dc6
**Storage Layer:** - Database 类(aiosqlite + WAL + 并发保护) - WikiPage 数据模型 - fix_dirty_states 恢复机制 - FTS5 全文搜索支持 - 完整的 SQLite 表结构 **Service Layer:** - CacheService(LRU 缓存 + TTL + 大小限制) - QueryService(查询服务) - ParserService(Markdown 解析) - IndexerService(索引服务) - GraphService(链接图服务) **Tool Layer (8 个 MCP Tools):** - wiki_query - FTS5 全文搜索 + 标签过滤 - memory_bridge - 按工具来源浏览 - wiki_status - 索引状态 - wiki_lint - 健康审计 - cross_linker - 缺失链接发现 - tag_taxonomy - 标签一致性 - wiki_synthesize - 跨概念综合分析 - daily_update - 日常维护 + hot.md 生成 **MCP Protocol Layer:** - MCP 协议解析和封装 - 工具注册和路由 - 错误处理和日志 - stdio 模式支持 **配置和部署:** - requirements.txt - config.example.yaml - ecosystem.config.cjs (PM2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""
|
||
Service Layer - GraphService(图服务)
|
||
|
||
管理页面间的链接关系,查找孤立页面和缺失链接。
|
||
|
||
参考设计文档:第 2.3 节
|
||
"""
|
||
|
||
import logging
|
||
from typing import Set, List, Tuple
|
||
from ..storage import Database
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class GraphService:
|
||
"""图服务 - 管理链接关系"""
|
||
|
||
def __init__(self, db: Database):
|
||
self.db = db
|
||
|
||
async def get_links(self, path: str) -> Set[str]:
|
||
"""获取页面的出链"""
|
||
return await self.db.get_links(path)
|
||
|
||
async def get_backlinks(self, path: str) -> Set[str]:
|
||
"""获取页面的反向链接"""
|
||
return await self.db.get_backlinks(path)
|
||
|
||
async def find_orphans(self) -> Set[str]:
|
||
"""查找孤立页面(无反向链接)"""
|
||
all_pages = await self.db.get_all_pages()
|
||
orphans = set()
|
||
|
||
for page in all_pages:
|
||
backlinks = await self.db.get_backlinks(page.path)
|
||
if not backlinks and page.path != "index.md":
|
||
orphans.add(page.path)
|
||
|
||
return orphans
|
||
|
||
async def find_missing_links(self) -> List[Tuple[str, str]]:
|
||
"""查找缺失的交叉引用"""
|
||
# 获取所有页面内容
|
||
all_pages = await self.db.get_all_pages()
|
||
page_paths = {page.path for page in all_pages}
|
||
|
||
# 获取所有链接关系
|
||
links_rows = await self.db.fetch_all("SELECT source, target FROM wiki_links")
|
||
|
||
missing = []
|
||
for source, target in links_rows:
|
||
if target not in page_paths:
|
||
missing.append((source, target))
|
||
|
||
return missing
|
||
|
||
async def get_link_graph(self) -> dict:
|
||
"""获取完整的链接图"""
|
||
pages = await self.db.get_all_pages()
|
||
graph = {}
|
||
|
||
for page in pages:
|
||
links = await self.db.get_links(page.path)
|
||
backlinks = await self.db.get_backlinks(page.path)
|
||
graph[page.path] = {
|
||
"title": page.title,
|
||
"category": page.category,
|
||
"links": sorted(links),
|
||
"backlinks": sorted(backlinks),
|
||
"links_count": len(links),
|
||
"backlinks_count": len(backlinks)
|
||
}
|
||
|
||
return graph
|
||
|
||
async def get_highly_connected_pages(self, threshold: int = 5) -> List[dict]:
|
||
"""获取高度连接的页面(入链 + 出链 >= threshold)"""
|
||
pages = await self.db.get_all_pages()
|
||
highly_connected = []
|
||
|
||
for page in pages:
|
||
links = await self.db.get_links(page.path)
|
||
backlinks = await self.db.get_backlinks(page.path)
|
||
total = len(links) + len(backlinks)
|
||
|
||
if total >= threshold:
|
||
highly_connected.append({
|
||
"path": page.path,
|
||
"title": page.title,
|
||
"links_count": len(links),
|
||
"backlinks_count": len(backlinks),
|
||
"total_connections": total
|
||
})
|
||
|
||
return sorted(highly_connected, key=lambda x: x["total_connections"], reverse=True)
|
||
|
||
async def get_disconnected_components(self) -> List[Set[str]]:
|
||
"""获取不连通的图分量(使用 BFS)"""
|
||
pages = await self.db.get_all_pages()
|
||
page_paths = {page.path for page in pages}
|
||
|
||
if not page_paths:
|
||
return []
|
||
|
||
visited = set()
|
||
components = []
|
||
|
||
for start_path in page_paths:
|
||
if start_path in visited:
|
||
continue
|
||
|
||
# BFS 构建连通分量
|
||
component = set()
|
||
queue = [start_path]
|
||
|
||
while queue:
|
||
current = queue.pop(0)
|
||
if current in visited:
|
||
continue
|
||
|
||
visited.add(current)
|
||
component.add(current)
|
||
|
||
# 添加出链和入链
|
||
for link in await self.db.get_links(current):
|
||
if link in page_paths and link not in visited:
|
||
queue.append(link)
|
||
for backlink in await self.db.get_backlinks(current):
|
||
if backlink in page_paths and backlink not in visited:
|
||
queue.append(backlink)
|
||
|
||
components.append(component)
|
||
|
||
# 返回最大的分量以外的所有分量(即孤立的子图)
|
||
main_component = max(components, key=len)
|
||
return [c for c in components if c != main_component]
|