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>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Tool Layer - 工具层
|
||||
|
||||
提供所有 MCP Tools。
|
||||
"""
|
||||
|
||||
from .wiki_query import WikiQueryTool
|
||||
from .memory_bridge import MemoryBridgeTool
|
||||
from .wiki_status import WikiStatusTool
|
||||
from .wiki_lint import WikiLintTool
|
||||
from .cross_linker import CrossLinkerTool
|
||||
from .tag_taxonomy import TagTaxonomyTool
|
||||
from .wiki_synthesize import WikiSynthesizeTool
|
||||
from .daily_update import DailyUpdateTool
|
||||
|
||||
__all__ = [
|
||||
"WikiQueryTool",
|
||||
"MemoryBridgeTool",
|
||||
"WikiStatusTool",
|
||||
"WikiLintTool",
|
||||
"CrossLinkerTool",
|
||||
"TagTaxonomyTool",
|
||||
"WikiSynthesizeTool",
|
||||
"DailyUpdateTool"
|
||||
]
|
||||
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Tool Layer - cross_linker 工具
|
||||
|
||||
扫描 wiki,自动发现缺失的交叉引用。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from ..services import GraphService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CrossLinkerTool:
|
||||
"""cross_linker 工具实现"""
|
||||
|
||||
def __init__(self, graph_service: GraphService):
|
||||
self.graph_service = graph_service
|
||||
|
||||
async def handle(self, path: str = "", dry_run: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
扫描 wiki,发现缺失的交叉引用
|
||||
|
||||
Args:
|
||||
path: 指定路径(空字符串表示全部)
|
||||
dry_run: 是否为模拟运行(不实际修改)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"missing_links": [
|
||||
{
|
||||
"source": "practices/a.md",
|
||||
"target": "concepts/x",
|
||||
"suggestion": "Consider creating [[concepts/x]]"
|
||||
}
|
||||
],
|
||||
"total": 12,
|
||||
"dry_run": true
|
||||
}
|
||||
"""
|
||||
# 获取缺失的链接
|
||||
all_missing = await self.graph_service.find_missing_links()
|
||||
|
||||
# 过滤指定路径
|
||||
if path:
|
||||
missing_links = [(s, t) for s, t in all_missing if s == path or t == path]
|
||||
else:
|
||||
missing_links = all_missing
|
||||
|
||||
# 格式化结果
|
||||
missing_links_formatted = []
|
||||
for source, target in missing_links:
|
||||
missing_links_formatted.append({
|
||||
"source": source,
|
||||
"target": target,
|
||||
"suggestion": f"Consider creating [[{target}]] or updating the link in [[{source}]]"
|
||||
})
|
||||
|
||||
return {
|
||||
"missing_links": missing_links_formatted[:50], # 最多返回 50 条
|
||||
"total": len(missing_links),
|
||||
"dry_run": dry_run
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "cross_linker",
|
||||
"description": "扫描 wiki,自动发现缺失的交叉引用",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "指定路径(空字符串表示全部)"
|
||||
},
|
||||
"dry_run": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "是否为模拟运行(不实际修改)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Tool Layer - daily_update 工具
|
||||
|
||||
日常维护(检查源新鲜度、更新 index、重新生成 hot.md)。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from ..services import QueryService, IndexerService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DailyUpdateTool:
|
||||
"""daily_update 工具实现"""
|
||||
|
||||
def __init__(self, query_service: QueryService, indexer_service: IndexerService):
|
||||
self.query_service = query_service
|
||||
self.indexer_service = indexer_service
|
||||
|
||||
async def handle(self) -> Dict[str, Any]:
|
||||
"""
|
||||
日常维护
|
||||
|
||||
Returns:
|
||||
{
|
||||
"updated": 5,
|
||||
"new": 2,
|
||||
"hot_md_generated": true,
|
||||
"index_status": {...}
|
||||
}
|
||||
"""
|
||||
# 1. 增量更新索引
|
||||
index_status = await self.indexer_service.incremental_update()
|
||||
|
||||
# 2. 获取最近更新的页面
|
||||
recent_pages = await self.query_service.db.get_recent_pages(days=7, limit=20)
|
||||
|
||||
# 3. 获取新增标签
|
||||
new_tags = await self.query_service.db.get_new_tags(days=7)
|
||||
|
||||
# 4. 查找孤立页面
|
||||
from ..services import GraphService
|
||||
graph_service = GraphService(self.query_service.db)
|
||||
orphans = await graph_service.find_orphans()
|
||||
|
||||
# 5. 生成 hot.md
|
||||
hot_md_generated = await self._generate_hot_md(recent_pages, new_tags, orphans)
|
||||
|
||||
return {
|
||||
"updated": len(recent_pages),
|
||||
"new": len(new_tags),
|
||||
"hot_md_generated": hot_md_generated,
|
||||
"index_status": index_status
|
||||
}
|
||||
|
||||
async def _generate_hot_md(self, recent_pages, new_tags, orphans) -> bool:
|
||||
"""生成热点文件"""
|
||||
try:
|
||||
# 确定 hot.md 保存路径
|
||||
vault_path = Path(self.query_service.db.path).parent.parent / "wiki-vault"
|
||||
hot_path = vault_path / "hot.md"
|
||||
|
||||
# 格式化内容
|
||||
content = f"""# Wiki Hot - {datetime.now().strftime('%Y-%m-%d')}
|
||||
|
||||
## 最近更新(7 天内)
|
||||
|
||||
"""
|
||||
for page in recent_pages[:10]:
|
||||
content += f"- [[{page.path}|{page.title}]] - {page.updated_at}\n"
|
||||
|
||||
content += "\n## 新增标签(7 天内)\n\n"
|
||||
for tag, updated_at in new_tags[:10]:
|
||||
content += f"- `{tag}` - {updated_at}\n"
|
||||
|
||||
content += "\n## 待链接页面(孤立页面)\n\n"
|
||||
for orphan in sorted(list(orphans))[:10]:
|
||||
content += f"- [[{orphan}]]\n"
|
||||
|
||||
# 写入文件
|
||||
with open(hot_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
logger.info(f"Generated hot.md: {hot_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate hot.md: {e}")
|
||||
return False
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "daily_update",
|
||||
"description": "日常维护(检查源新鲜度、更新 index、重新生成 hot.md)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Tool Layer - memory_bridge 工具
|
||||
|
||||
按 AI 工具来源浏览和对比 wiki 知识。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from ..services import QueryService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemoryBridgeTool:
|
||||
"""memory_bridge 工具实现"""
|
||||
|
||||
def __init__(self, query_service: QueryService):
|
||||
self.query_service = query_service
|
||||
|
||||
async def handle(self, tool_name: str = "claude", date_range: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
按 AI 工具来源浏览和对比 wiki 知识
|
||||
|
||||
Args:
|
||||
tool_name: AI 工具名称(claude/web_reader/gitea/other)
|
||||
date_range: 日期范围(如 "2024-01-01:2024-12-31" 或空字符串表示不限)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"path": "practices/moziplus-orchestration.md",
|
||||
"title": "moziplus 编排实践",
|
||||
"summary": "...",
|
||||
"updated_at": "2024-06-15T10:30:00",
|
||||
"relevance_score": 0.85
|
||||
}
|
||||
],
|
||||
"total": 12,
|
||||
"tool_name": "claude",
|
||||
"date_range": "2024-01-01:2024-12-31"
|
||||
}
|
||||
"""
|
||||
# 按工具来源搜索
|
||||
pages = await self.query_service.search_by_source_tool(tool_name, limit=100)
|
||||
|
||||
# 按日期范围过滤
|
||||
if date_range:
|
||||
try:
|
||||
start_str, end_str = date_range.split(":")
|
||||
start_date = datetime.fromisoformat(start_str)
|
||||
end_date = datetime.fromisoformat(end_str)
|
||||
|
||||
filtered = []
|
||||
for page in pages:
|
||||
updated = datetime.fromisoformat(page.updated_at)
|
||||
if start_date <= updated <= end_date:
|
||||
filtered.append(page)
|
||||
pages = filtered
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid date_range format '{date_range}': {e}")
|
||||
|
||||
# 格式化结果
|
||||
entries = []
|
||||
for page in pages:
|
||||
# 计算相关性分数(基于摘要长度和更新时间)
|
||||
recency_days = (datetime.now() - datetime.fromisoformat(page.updated_at)).days
|
||||
relevance_score = max(0.1, 1.0 - recency_days / 365) # 简单衰减
|
||||
|
||||
entries.append({
|
||||
"path": page.path,
|
||||
"title": page.title,
|
||||
"summary": page.summary,
|
||||
"updated_at": page.updated_at,
|
||||
"relevance_score": round(relevance_score, 2)
|
||||
})
|
||||
|
||||
# 按相关性排序
|
||||
entries.sort(key=lambda x: x["relevance_score"], reverse=True)
|
||||
|
||||
return {
|
||||
"entries": entries[:50], # 最多返回 50 条
|
||||
"total": len(entries),
|
||||
"tool_name": tool_name,
|
||||
"date_range": date_range
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "memory_bridge",
|
||||
"description": "按 AI 工具来源浏览和对比 wiki 知识",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tool_name": {
|
||||
"type": "string",
|
||||
"default": "claude",
|
||||
"description": "AI 工具名称(claude/web_reader/gitea/other)",
|
||||
"enum": ["claude", "web_reader", "gitea", "other"]
|
||||
},
|
||||
"date_range": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "日期范围,格式:YYYY-MM-DD:YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
Tool Layer - tag_taxonomy 工具
|
||||
|
||||
用受控词表强制标签一致性。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Set
|
||||
from ..services import QueryService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TagTaxonomyTool:
|
||||
"""tag_taxonomy 工具实现"""
|
||||
|
||||
# 受控词表(可配置)
|
||||
CONTROLLED_VOCABULARY: Set[str] = {
|
||||
# 分类
|
||||
"practices", "concepts", "entities", "projects", "skills", "references",
|
||||
# 技术栈
|
||||
"python", "typescript", "rust", "go", "swift",
|
||||
# 领域
|
||||
"ai", "ml", "database", "web", "mobile", "devops",
|
||||
# 团队
|
||||
"sanguo", "moziplus", "bitnet",
|
||||
# 通用
|
||||
"tutorial", "reference", "guide", "example", "draft"
|
||||
}
|
||||
|
||||
def __init__(self, query_service: QueryService):
|
||||
self.query_service = query_service
|
||||
|
||||
async def handle(self, path: str = "", enforce: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
用受控词表强制标签一致性
|
||||
|
||||
Args:
|
||||
path: 指定路径(空字符串表示全部)
|
||||
enforce: 是否强制修正(否则仅报告)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"conflicts": [
|
||||
{
|
||||
"path": "practices/example.md",
|
||||
"invalid_tags": ["CustomTag"],
|
||||
"suggested_tags": ["practices"]
|
||||
}
|
||||
],
|
||||
"total_conflicts": 5,
|
||||
"enforced": false
|
||||
}
|
||||
"""
|
||||
conflicts = []
|
||||
|
||||
# 获取所有标签
|
||||
all_tags = await self.query_service.db.get_all_tags()
|
||||
|
||||
# 检查非受控标签
|
||||
invalid_tags = set(all_tags.keys()) - self.CONTROLLED_VOCABULARY
|
||||
|
||||
if invalid_tags:
|
||||
# 获取使用这些标签的页面
|
||||
for tag in invalid_tags:
|
||||
tag_pages = await self.query_service.search_by_tags([tag])
|
||||
|
||||
for page in tag_pages:
|
||||
page_tags = set(page.tags) & invalid_tags
|
||||
|
||||
if page_tags:
|
||||
# 推断修正建议(基于页面分类)
|
||||
suggested_tags = [page.category] if page.category else []
|
||||
|
||||
conflicts.append({
|
||||
"path": page.path,
|
||||
"invalid_tags": list(page_tags),
|
||||
"suggested_tags": suggested_tags
|
||||
})
|
||||
|
||||
if enforce and path and page.path == path:
|
||||
# TODO: 实际修正标签
|
||||
pass
|
||||
|
||||
return {
|
||||
"conflicts": conflicts[:50],
|
||||
"total_conflicts": len(conflicts),
|
||||
"enforced": enforce
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "tag_taxonomy",
|
||||
"description": "用受控词表强制标签一致性",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "指定路径(空字符串表示全部)"
|
||||
},
|
||||
"enforce": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "是否强制修正(否则仅报告)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def get_controlled_vocabulary(self) -> List[str]:
|
||||
"""获取当前受控词表"""
|
||||
return sorted(list(self.CONTROLLED_VOCABULARY))
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Tool Layer - wiki_lint 工具
|
||||
|
||||
审计 wiki 健康(格式、链接、frontmatter 规范)。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from ..services import ParserService, QueryService, GraphService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WikiLintTool:
|
||||
"""wiki_lint 工具实现"""
|
||||
|
||||
def __init__(self, parser: ParserService, query_service: QueryService, graph_service: GraphService):
|
||||
self.parser = parser
|
||||
self.query_service = query_service
|
||||
self.graph_service = graph_service
|
||||
|
||||
async def handle(self, path: str = "", level: str = "basic") -> Dict[str, Any]:
|
||||
"""
|
||||
审计 wiki 健康
|
||||
|
||||
Args:
|
||||
path: 指定路径(空字符串表示全部)
|
||||
level: 检查级别(basic/strict)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"issues": [
|
||||
{
|
||||
"path": "practices/example.md",
|
||||
"type": "missing_title",
|
||||
"message": "Missing title",
|
||||
"severity": "warning"
|
||||
}
|
||||
],
|
||||
"fixes": [
|
||||
{
|
||||
"path": "practices/example.md",
|
||||
"action": "add_title",
|
||||
"suggestion": "# Example Page"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_issues": 5,
|
||||
"critical": 0,
|
||||
"warning": 5,
|
||||
"info": 0
|
||||
}
|
||||
}
|
||||
"""
|
||||
issues = []
|
||||
fixes = []
|
||||
|
||||
# 获取所有页面或指定页面
|
||||
if path:
|
||||
pages = [await self.query_service.get_page(path)]
|
||||
else:
|
||||
pages = await self.query_service.db.get_all_pages()
|
||||
|
||||
summary = {"critical": 0, "warning": 0, "info": 0}
|
||||
|
||||
for page in pages:
|
||||
# 读取内容
|
||||
try:
|
||||
from pathlib import Path
|
||||
vault_path = Path(self.query_service.db.path).parent.parent / "wiki-vault"
|
||||
full_path = vault_path / page.path
|
||||
with open(full_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
issues.append({
|
||||
"path": page.path,
|
||||
"type": "file_error",
|
||||
"message": f"Failed to read file: {e}",
|
||||
"severity": "critical"
|
||||
})
|
||||
summary["critical"] += 1
|
||||
continue
|
||||
|
||||
# 验证页面
|
||||
validation_issues = self.parser.validate_page(page.path, content)
|
||||
|
||||
for issue in validation_issues:
|
||||
severity = "warning" if "Missing" in issue else "info"
|
||||
issues.append({
|
||||
"path": page.path,
|
||||
"type": issue.lower().replace(" ", "_"),
|
||||
"message": issue,
|
||||
"severity": severity
|
||||
})
|
||||
summary[severity] += 1
|
||||
|
||||
# 生成修复建议
|
||||
if "Missing title" in issue:
|
||||
title = self.parser.extract_title(content)
|
||||
if not title:
|
||||
title = page.path.replace(".md", "").replace("-", " ").replace("_", " ").title()
|
||||
fixes.append({
|
||||
"path": page.path,
|
||||
"action": "add_title",
|
||||
"suggestion": f"# {title}"
|
||||
})
|
||||
|
||||
# strict 模式额外检查
|
||||
if level == "strict":
|
||||
# 检查孤立页面
|
||||
orphans = await self.graph_service.find_orphans()
|
||||
for orphan in orphans:
|
||||
issues.append({
|
||||
"path": orphan,
|
||||
"type": "orphan",
|
||||
"message": "No backlinks found",
|
||||
"severity": "info"
|
||||
})
|
||||
summary["info"] += 1
|
||||
|
||||
# 检查缺失链接
|
||||
missing_links = await self.graph_service.find_missing_links()
|
||||
for source, target in missing_links[:10]:
|
||||
issues.append({
|
||||
"path": source,
|
||||
"type": "broken_link",
|
||||
"message": f"Link to non-existent page: {target}",
|
||||
"severity": "warning"
|
||||
})
|
||||
summary["warning"] += 1
|
||||
|
||||
return {
|
||||
"issues": issues,
|
||||
"fixes": fixes,
|
||||
"summary": {
|
||||
"total_issues": len(issues),
|
||||
**summary
|
||||
}
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "wiki_lint",
|
||||
"description": "审计 wiki 健康(格式、链接、frontmatter 规范)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "指定路径(空字符串表示全部)"
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"default": "basic",
|
||||
"enum": ["basic", "strict"],
|
||||
"description": "检查级别"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Tool Layer - wiki_query 工具
|
||||
|
||||
基于 SQLite 索引查询 wiki,支持关键词(FTS5)和标签搜索。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Any
|
||||
from ..services import QueryService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WikiQueryTool:
|
||||
"""wiki_query 工具实现"""
|
||||
|
||||
def __init__(self, query_service: QueryService):
|
||||
self.query_service = query_service
|
||||
|
||||
async def handle(self, query: str = "", tags: List[str] = None, limit: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
查询 wiki 页面
|
||||
|
||||
Args:
|
||||
query: 查询关键词(FTS5 全文搜索)
|
||||
tags: 标签过滤
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"path": "practices/moziplus-orchestration.md",
|
||||
"title": "moziplus 编排实践",
|
||||
"category": "practices",
|
||||
"tags": ["moziplus", "orchestration"],
|
||||
"summary": "...",
|
||||
"lifecycle": "verified",
|
||||
"updated_at": "2024-06-15T10:30:00",
|
||||
"wikilink": "[[practices/moziplus-orchestration.md|moziplus 编排实践]]"
|
||||
}
|
||||
],
|
||||
"total": 5,
|
||||
"query": "...",
|
||||
"tags": [...]
|
||||
}
|
||||
"""
|
||||
if tags is None:
|
||||
tags = []
|
||||
|
||||
results = []
|
||||
|
||||
# 组合查询
|
||||
if query and tags:
|
||||
# 先 FTS5 搜索,再标签过滤
|
||||
fts_results = await self.query_service.search(query, limit * 2)
|
||||
filtered = [r for r in fts_results if any(t in r.tags for t in tags)]
|
||||
pages = filtered[:limit]
|
||||
elif query:
|
||||
# 仅 FTS5 搜索
|
||||
pages = await self.query_service.search(query, limit)
|
||||
elif tags:
|
||||
# 仅标签搜索
|
||||
pages = await self.query_service.search_by_tags(tags[:5]) # 限制标签数量
|
||||
pages = pages[:limit]
|
||||
else:
|
||||
# 无查询条件,返回最近更新的页面
|
||||
pages = []
|
||||
|
||||
# 格式化结果
|
||||
for page in pages:
|
||||
results.append({
|
||||
"path": page.path,
|
||||
"title": page.title,
|
||||
"category": page.category,
|
||||
"tags": page.tags,
|
||||
"summary": page.summary,
|
||||
"lifecycle": page.lifecycle,
|
||||
"updated_at": page.updated_at,
|
||||
"wikilink": f"[[{page.path}|{page.title}]]"
|
||||
})
|
||||
|
||||
return {
|
||||
"results": results,
|
||||
"total": len(results),
|
||||
"query": query,
|
||||
"tags": tags
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "wiki_query",
|
||||
"description": "查询 wiki 页面,支持关键词(FTS5)和标签搜索",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "查询关键词(FTS5 全文搜索)"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "标签过滤"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "返回数量限制"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Tool Layer - wiki_status 工具
|
||||
|
||||
显示 wiki 当前状态(页面数、待处理项、增量差异)。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from ..services import QueryService, IndexerService, GraphService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WikiStatusTool:
|
||||
"""wiki_status 工具实现"""
|
||||
|
||||
def __init__(self, query_service: QueryService, indexer_service: IndexerService, graph_service: GraphService):
|
||||
self.query_service = query_service
|
||||
self.indexer_service = indexer_service
|
||||
self.graph_service = graph_service
|
||||
|
||||
async def handle(self) -> Dict[str, Any]:
|
||||
"""
|
||||
显示 wiki 当前状态
|
||||
|
||||
Returns:
|
||||
{
|
||||
"stats": {
|
||||
"total_pages": 1353,
|
||||
"total_links": 4200,
|
||||
"total_tags": 156,
|
||||
"last_indexed": "2024-06-26T10:30:00"
|
||||
},
|
||||
"orphans": ["path1", "path2"],
|
||||
"orphans_count": 5,
|
||||
"dirty_pages": 12,
|
||||
"cache_stats": {
|
||||
"size": 100,
|
||||
"max_size": 1000,
|
||||
"utilization": 0.1
|
||||
}
|
||||
}
|
||||
"""
|
||||
# 获取统计信息
|
||||
stats = await self.query_service.get_stats()
|
||||
|
||||
# 获取孤立页面
|
||||
orphans = await self.graph_service.find_orphans()
|
||||
|
||||
# 获取索引状态
|
||||
index_status = await self.indexer_service.get_index_status()
|
||||
|
||||
# 获取缓存统计
|
||||
cache_stats = await self.query_service.cache.get_stats()
|
||||
|
||||
return {
|
||||
"stats": stats,
|
||||
"orphans": sorted(list(orphans)),
|
||||
"orphans_count": len(orphans),
|
||||
"dirty_pages": index_status.get("dirty_pages", 0),
|
||||
"cache_stats": cache_stats
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "wiki_status",
|
||||
"description": "显示 wiki 当前状态(页面数、待处理项、增量差异)",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
Tool Layer - wiki_synthesize 工具
|
||||
|
||||
发现跨概念的综合分析机会,生成 synthesis 页面。
|
||||
|
||||
参考设计文档:第 2.2 节
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
from ..services import QueryService, GraphService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WikiSynthesizeTool:
|
||||
"""wiki_synthesize 工具实现"""
|
||||
|
||||
def __init__(self, query_service: QueryService, graph_service: GraphService):
|
||||
self.query_service = query_service
|
||||
self.graph_service = graph_service
|
||||
|
||||
async def handle(self, concepts: List[str] = None, threshold: float = 0.3) -> Dict[str, Any]:
|
||||
"""
|
||||
发现跨概念的综合分析机会
|
||||
|
||||
Args:
|
||||
concepts: 概念列表(空表示自动发现)
|
||||
threshold: 相关性阈值
|
||||
|
||||
Returns:
|
||||
{
|
||||
"synthesis": [
|
||||
{
|
||||
"concept_a": "SQLite",
|
||||
"concept_b": "Concurrency",
|
||||
"common_references": ["page1", "page2"],
|
||||
"strength": 0.8,
|
||||
"suggestion": "Consider creating synthesis page: SQLite & Concurrency"
|
||||
}
|
||||
],
|
||||
"total": 5
|
||||
}
|
||||
"""
|
||||
synthesis = []
|
||||
|
||||
# 如果未提供概念,自动发现高相关性概念
|
||||
if not concepts:
|
||||
# 获取高度连接的页面作为候选
|
||||
highly_connected = await self.graph_service.get_highly_connected_pages(threshold=10)
|
||||
concepts = [item["title"] for item in highly_connected[:10]]
|
||||
|
||||
# 分析概念间的关联
|
||||
for i, concept_a in enumerate(concepts):
|
||||
for concept_b in concepts[i + 1:]:
|
||||
# 查找同时引用两个概念的页面
|
||||
pages_a = await self.query_service.search(concept_a, limit=50)
|
||||
pages_b = await self.query_service.search(concept_b, limit=50)
|
||||
|
||||
paths_a = {p.path for p in pages_a}
|
||||
paths_b = {p.path for p in pages_b}
|
||||
|
||||
common = paths_a & paths_b
|
||||
|
||||
if len(common) >= 2: # 至少 2 个共同引用
|
||||
strength = len(common) / max(len(paths_a), len(paths_b))
|
||||
|
||||
if strength >= threshold:
|
||||
synthesis.append({
|
||||
"concept_a": concept_a,
|
||||
"concept_b": concept_b,
|
||||
"common_references": sorted(list(common)),
|
||||
"strength": round(strength, 2),
|
||||
"suggestion": f"Consider creating synthesis page: {concept_a} & {concept_b}"
|
||||
})
|
||||
|
||||
# 按关联强度排序
|
||||
synthesis.sort(key=lambda x: x["strength"], reverse=True)
|
||||
|
||||
return {
|
||||
"synthesis": synthesis[:20],
|
||||
"total": len(synthesis)
|
||||
}
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
"""返回 MCP Tool schema"""
|
||||
return {
|
||||
"name": "wiki_synthesize",
|
||||
"description": "发现跨概念的综合分析机会,生成 synthesis 页面",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concepts": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "概念列表(空表示自动发现)"
|
||||
},
|
||||
"threshold": {
|
||||
"type": "number",
|
||||
"default": 0.3,
|
||||
"description": "相关性阈值"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user