Files
sanguo_llmwiki/mcp_server/services/graph.py
T
claude_dev dfd8421dc6 feat(§03): 实现 MCP Server 核心 - Storage/Service/Tool/MCP Protocol Layers
**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>
2026-06-26 11:32:43 +08:00

138 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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]