feat: 搜索改进与代码修复
- 连字符扩展:multi-agent → multi agent 提升搜索召回率 - 中文同义词支持:添加中英文混合查询扩展 - Snippet 高亮预留:为 FTS5 snippet 功能预留接口 - YAML 解析增强:支持 HTML 实体解码 - 标签关联维护:自动维护 wiki_page_tags 关联表 - 缓存失效优化:支持智能前缀匹配 - 设计文档更新:汇总近期改动 (v1.3) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 状态
|
## 状态
|
||||||
|
|
||||||
🚧 开发中 - Phase 3: 编码实现
|
✅ Phase 5 完成 - 部署就绪
|
||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -1038,6 +1038,22 @@ pm2 save
|
|||||||
|
|
||||||
## 14. 修订记录
|
## 14. 修订记录
|
||||||
|
|
||||||
|
### v1.3(2026-06-28)- 搜索改进与代码修复
|
||||||
|
|
||||||
|
**搜索改进(P0-P1):**
|
||||||
|
- ✅ **连字符扩展**:在 `Database.update_fts_content()` 中添加 `_expand_hyphenated_text()` 方法,将 `multi-agent` 扩展为 `multi agent` 提升搜索召回率
|
||||||
|
- ✅ **中文同义词支持**:在 `QueryService` 中添加 `CHINESE_SYNONYMS` 映射表和 `_expand_query_with_synonyms()` 方法,支持中英文混合查询(如"多智能体"自动扩展为包含 "multi-agent" 的查询)
|
||||||
|
- ✅ **Snippet 高亮预留**:在 `Database.fts_search()` 中添加 `with_snippets` 参数,为 FTS5 snippet 功能预留接口
|
||||||
|
|
||||||
|
**代码修复:**
|
||||||
|
- ✅ **YAML 解析增强**:添加 `html.unescape()` 支持解码 HTML 实体(如 `>` → `>`)
|
||||||
|
- ✅ **标签关联维护**:添加 `Database.update_page_tags()` 方法,自动维护 `wiki_page_tags` 关联表
|
||||||
|
- ✅ **缓存失效优化**:在 `QueryService.invalidate_cache()` 中添加智能前缀处理,支持 `page:/links:/search:` 前缀匹配
|
||||||
|
|
||||||
|
**数据模型更新:**
|
||||||
|
- ✅ **FTS5 表结构优化**:移除外部内容表,直接存储所有字段在 FTS 表中
|
||||||
|
- ✅ **索引策略调整**:title 和 summary 字段存储扩展版本(原版 + 连字符扩展版本)
|
||||||
|
|
||||||
### v1.2(2026-06-26)- 第二轮评审修复
|
### v1.2(2026-06-26)- 第二轮评审修复
|
||||||
|
|
||||||
**Major(已修复):**
|
**Major(已修复):**
|
||||||
@@ -1069,6 +1085,6 @@ pm2 save
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*文档版本:v1.2*
|
*文档版本:v1.3*
|
||||||
*创建时间:2026-06-26*
|
*创建时间:2026-06-26*
|
||||||
*更新时间:2026-06-26*
|
*更新时间:2026-06-28*
|
||||||
|
|||||||
+5
-3
@@ -17,10 +17,12 @@ from pathlib import Path
|
|||||||
try:
|
try:
|
||||||
from mcp.server.models import InitializationOptions
|
from mcp.server.models import InitializationOptions
|
||||||
from mcp.server import Server, NotificationOptions
|
from mcp.server import Server, NotificationOptions
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
from mcp.types import Tool, TextContent
|
from mcp.types import Tool, TextContent
|
||||||
MCP_AVAILABLE = True
|
MCP_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
MCP_AVAILABLE = False
|
MCP_AVAILABLE = False
|
||||||
|
stdio_server = None
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.warning("MCP SDK not available, using mock implementation")
|
logger.warning("MCP SDK not available, using mock implementation")
|
||||||
|
|
||||||
@@ -162,12 +164,12 @@ class MCPServer:
|
|||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""运行 MCP 服务器"""
|
"""运行 MCP 服务器"""
|
||||||
if not self.server:
|
if not self.server or not stdio_server:
|
||||||
logger.error("MCP Server not available (MCP SDK not installed)")
|
logger.error("MCP Server not available (MCP SDK not installed)")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 运行服务器
|
# 运行服务器 - stdio 模式
|
||||||
async with self.server.stdio_stdio() as (read_stream, write_stream):
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
await self.server.run(
|
await self.server.run(
|
||||||
read_stream,
|
read_stream,
|
||||||
write_stream,
|
write_stream,
|
||||||
|
|||||||
@@ -79,9 +79,8 @@ class IndexerService:
|
|||||||
for link in parsed.get("links", []):
|
for link in parsed.get("links", []):
|
||||||
await self.db.upsert_link(page_path, link)
|
await self.db.upsert_link(page_path, link)
|
||||||
|
|
||||||
# 更新标签
|
# 更新标签关联
|
||||||
for tag in page.tags:
|
await self.db.update_page_tags(page_path, page.tags)
|
||||||
await self.db.upsert_tag(tag)
|
|
||||||
|
|
||||||
logger.info(f"Indexed page: {page_path}")
|
logger.info(f"Indexed page: {page_path}")
|
||||||
return page
|
return page
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Service Layer - ParserService(解析服务)
|
|||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import html
|
||||||
from typing import List, Dict, Optional, Tuple
|
from typing import List, Dict, Optional, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -36,6 +37,48 @@ class ParserService:
|
|||||||
# Tag 正则(frontmatter 中的 tags 字段)
|
# Tag 正则(frontmatter 中的 tags 字段)
|
||||||
TAG_PATTERN = re.compile(r'tags:\s*\[(.*?)\]')
|
TAG_PATTERN = re.compile(r'tags:\s*\[(.*?)\]')
|
||||||
|
|
||||||
|
def _fix_yaml_common_issues(self, yaml_content: str) -> str:
|
||||||
|
"""修复常见的 YAML 格式问题
|
||||||
|
|
||||||
|
1. HTML 实体解码
|
||||||
|
2. 修复一级键值对中的冒号问题(不处理嵌套)
|
||||||
|
"""
|
||||||
|
# 解码 HTML 实体
|
||||||
|
yaml_content = html.unescape(yaml_content)
|
||||||
|
|
||||||
|
# 修复一级键值对中的冒号问题
|
||||||
|
# 只处理格式为 "key: value" 的行(不处理嵌套的缩进行)
|
||||||
|
lines = []
|
||||||
|
for line in yaml_content.split('\n'):
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# 跳过空行、注释、列表项、多行标记
|
||||||
|
if not stripped or stripped.startswith('#') or stripped.startswith('-') or stripped.startswith('>'):
|
||||||
|
lines.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 只处理一级键值对(没有缩进或缩进较少)
|
||||||
|
# 检查是否是简单的 "key: value" 格式
|
||||||
|
if ':' in line and not line.startswith(' ') and not line.startswith('\t'):
|
||||||
|
parts = line.split(':', 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
key = parts[0].strip()
|
||||||
|
value = parts[1].strip()
|
||||||
|
|
||||||
|
# 如果值包含冒号且没有引号,添加引号
|
||||||
|
if value and ':' in value and not (value.startswith('"') or value.startswith("'") or value.startswith('|')):
|
||||||
|
# 检查值不是特殊格式
|
||||||
|
if not value.startswith('>') and not value.startswith('|'):
|
||||||
|
value = f'"{value}"'
|
||||||
|
|
||||||
|
lines.append(f"{key}: {value}")
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
def parse_frontmatter(self, content: str) -> Dict:
|
def parse_frontmatter(self, content: str) -> Dict:
|
||||||
"""
|
"""
|
||||||
解析 frontmatter(使用 PyYAML)
|
解析 frontmatter(使用 PyYAML)
|
||||||
@@ -54,6 +97,9 @@ class ParserService:
|
|||||||
if match:
|
if match:
|
||||||
yaml_content = match.group(1)
|
yaml_content = match.group(1)
|
||||||
|
|
||||||
|
# 修复常见 YAML 问题
|
||||||
|
yaml_content = self._fix_yaml_common_issues(yaml_content)
|
||||||
|
|
||||||
# 使用 PyYAML 进行完整解析
|
# 使用 PyYAML 进行完整解析
|
||||||
if YAML_AVAILABLE:
|
if YAML_AVAILABLE:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -7,13 +7,32 @@ Service Layer - QueryService(查询服务)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Set, Optional
|
import re
|
||||||
|
from typing import List, Set, Optional, Dict
|
||||||
from ..storage import Database, WikiPage
|
from ..storage import Database, WikiPage
|
||||||
from .cache import CacheService
|
from .cache import CacheService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# 中文同义词映射(用于扩展搜索查询)
|
||||||
|
CHINESE_SYNONYMS: Dict[str, List[str]] = {
|
||||||
|
"多智能体": ["multi-agent", "多agent", "multi agent"],
|
||||||
|
"编排": ["orchestration", "调度"],
|
||||||
|
"协作": ["collaboration", "协同"],
|
||||||
|
"工具": ["tool", "工具调用"],
|
||||||
|
"模式": ["pattern", "设计模式"],
|
||||||
|
"实践": ["practice", "最佳实践"],
|
||||||
|
"架构": ["architecture", "系统架构"],
|
||||||
|
"代理": ["agent", "智能体"],
|
||||||
|
"大模型": ["llm", "language model", "语言模型"],
|
||||||
|
"向量": ["vector", "embedding", "嵌入"],
|
||||||
|
"检索": ["retrieval", "search", "搜索"],
|
||||||
|
"生成": ["generation", "生成式"],
|
||||||
|
"推理": ["inference", "reasoning"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class QueryService:
|
class QueryService:
|
||||||
"""查询服务 - 负责所有查询逻辑"""
|
"""查询服务 - 负责所有查询逻辑"""
|
||||||
|
|
||||||
@@ -21,19 +40,51 @@ class QueryService:
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.cache = cache
|
self.cache = cache
|
||||||
|
|
||||||
async def search(self, query: str, limit: int = 10) -> List[WikiPage]:
|
def _expand_query_with_synonyms(self, query: str) -> str:
|
||||||
"""FTS5 全文搜索"""
|
"""使用中文同义词扩展查询
|
||||||
|
|
||||||
|
例如:"多智能体编排" → "多智能体编排 multi-agent orchestration"
|
||||||
|
"""
|
||||||
|
expanded_terms = []
|
||||||
|
query_lower = query.lower()
|
||||||
|
|
||||||
|
# 检查每个中文词是否有对应的英文同义词
|
||||||
|
for chinese_term, english_synonyms in CHINESE_SYNONYMS.items():
|
||||||
|
if chinese_term in query:
|
||||||
|
expanded_terms.extend(english_synonyms)
|
||||||
|
|
||||||
|
if expanded_terms:
|
||||||
|
# 构建扩展查询(OR 连接)
|
||||||
|
expanded_query = f"{query} OR {' OR '.join(expanded_terms)}"
|
||||||
|
logger.debug(f"Expanded query: {query} → {expanded_query}")
|
||||||
|
return expanded_query
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
async def search(self, query: str, limit: int = 10, use_synonyms: bool = True) -> List[WikiPage]:
|
||||||
|
"""FTS5 全文搜索
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 搜索查询
|
||||||
|
limit: 返回结果数量限制
|
||||||
|
use_synonyms: 是否使用中文同义词扩展
|
||||||
|
"""
|
||||||
# 1. 检查缓存
|
# 1. 检查缓存
|
||||||
cache_key = f"search:{query}:{limit}"
|
cache_key = f"search:{query}:{limit}:{use_synonyms}"
|
||||||
cached = await self.cache.get(cache_key)
|
cached = await self.cache.get(cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
logger.debug(f"Cache hit for search: {query}")
|
logger.debug(f"Cache hit for search: {query}")
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
# 2. FTS5 搜索
|
# 2. 查询扩展(中文同义词)
|
||||||
results = await self.db.fts_search(query, limit)
|
search_query = query
|
||||||
|
if use_synonyms:
|
||||||
|
search_query = self._expand_query_with_synonyms(query)
|
||||||
|
|
||||||
# 3. 缓存结果
|
# 3. FTS5 搜索
|
||||||
|
results = await self.db.fts_search(search_query, limit)
|
||||||
|
|
||||||
|
# 4. 缓存结果
|
||||||
await self.cache.set(cache_key, results, ttl=3600)
|
await self.cache.set(cache_key, results, ttl=3600)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -116,5 +167,26 @@ class QueryService:
|
|||||||
return await self.db.get_stats()
|
return await self.db.get_stats()
|
||||||
|
|
||||||
async def invalidate_cache(self, pattern: str = "*") -> int:
|
async def invalidate_cache(self, pattern: str = "*") -> int:
|
||||||
"""使缓存失效"""
|
"""使缓存失效
|
||||||
return await self.cache.invalidate(pattern)
|
|
||||||
|
如果 pattern 是具体路径(不是通配符),自动尝试匹配所有缓存前缀:
|
||||||
|
- page:{pattern}
|
||||||
|
- links:{pattern}
|
||||||
|
- search:{pattern}
|
||||||
|
"""
|
||||||
|
# 清除所有缓存
|
||||||
|
if pattern == "*":
|
||||||
|
return await self.cache.invalidate(pattern)
|
||||||
|
|
||||||
|
# 对于具体路径,尝试所有可能的前缀
|
||||||
|
prefixes = ["page:", "links:", "search:"]
|
||||||
|
total_invalidated = 0
|
||||||
|
|
||||||
|
for prefix in prefixes:
|
||||||
|
count = await self.cache.invalidate(f"{prefix}{pattern}")
|
||||||
|
total_invalidated += count
|
||||||
|
|
||||||
|
# 也尝试原始模式(兼容直接传入完整 key 的情况)
|
||||||
|
total_invalidated += await self.cache.invalidate(pattern)
|
||||||
|
|
||||||
|
return total_invalidated
|
||||||
|
|||||||
@@ -94,23 +94,13 @@ class Database:
|
|||||||
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_updated ON wiki_pages(updated_at)")
|
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_updated ON wiki_pages(updated_at)")
|
||||||
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_source_tool ON wiki_pages(source_tool)")
|
await self._conn.execute("CREATE INDEX IF NOT EXISTS idx_pages_source_tool ON wiki_pages(source_tool)")
|
||||||
|
|
||||||
# 内容表(FTS5 外部内容表)- 必须先创建
|
# FTS5 全文搜索表(不使用外部内容表,简化结构)
|
||||||
await self._conn.execute("""
|
|
||||||
CREATE TABLE IF NOT EXISTS wiki_content (
|
|
||||||
path TEXT PRIMARY KEY,
|
|
||||||
content TEXT NOT NULL
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# FTS5 全文搜索表
|
|
||||||
await self._conn.execute("""
|
await self._conn.execute("""
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS wiki_fts USING fts5(
|
||||||
path UNINDEXED,
|
path UNINDEXED,
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
summary,
|
summary,
|
||||||
content=wiki_content,
|
|
||||||
content_rowid=rowid,
|
|
||||||
tokenize = 'porter unicode61'
|
tokenize = 'porter unicode61'
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
@@ -241,7 +231,8 @@ class Database:
|
|||||||
return row[0] if row else None
|
return row[0] if row else None
|
||||||
|
|
||||||
async def upsert_page(self, page: WikiPage) -> None:
|
async def upsert_page(self, page: WikiPage) -> None:
|
||||||
"""插入或更新页面"""
|
"""插入或更新页面(包括标签关联)"""
|
||||||
|
# 先更新页面基本信息
|
||||||
await self.execute_with_retry("""
|
await self.execute_with_retry("""
|
||||||
INSERT INTO wiki_pages (path, title, category, tags, summary, content_hash, lifecycle, source_tool, created_at, updated_at, indexed_at)
|
INSERT INTO wiki_pages (path, title, category, tags, summary, content_hash, lifecycle, source_tool, created_at, updated_at, indexed_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
@@ -261,6 +252,9 @@ class Database:
|
|||||||
page.created_at, page.updated_at, page.indexed_at
|
page.created_at, page.updated_at, page.indexed_at
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# 更新标签关联
|
||||||
|
await self.update_page_tags(page.path, page.tags)
|
||||||
|
|
||||||
async def delete_page(self, path: str) -> None:
|
async def delete_page(self, path: str) -> None:
|
||||||
"""删除页面(级联删除相关数据)"""
|
"""删除页面(级联删除相关数据)"""
|
||||||
await self.execute_with_retry("DELETE FROM wiki_pages WHERE path = ?", (path,))
|
await self.execute_with_retry("DELETE FROM wiki_pages WHERE path = ?", (path,))
|
||||||
@@ -314,8 +308,14 @@ class Database:
|
|||||||
|
|
||||||
return query.strip()
|
return query.strip()
|
||||||
|
|
||||||
async def fts_search(self, query: str, limit: int = 10) -> List[WikiPage]:
|
async def fts_search(self, query: str, limit: int = 10, with_snippets: bool = False) -> List[WikiPage]:
|
||||||
"""FTS5 全文搜索"""
|
"""FTS5 全文搜索
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 搜索查询
|
||||||
|
limit: 返回结果数量限制
|
||||||
|
with_snippets: 是否返回匹配片段(暂未实现,预留接口)
|
||||||
|
"""
|
||||||
# 验证和清理查询
|
# 验证和清理查询
|
||||||
try:
|
try:
|
||||||
safe_query = self._validate_fts_query(query)
|
safe_query = self._validate_fts_query(query)
|
||||||
@@ -447,6 +447,24 @@ class Database:
|
|||||||
(tag, count)
|
(tag, count)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def update_page_tags(self, path: str, tags: List[str]) -> None:
|
||||||
|
"""更新页面的标签关联
|
||||||
|
|
||||||
|
先删除旧的标签关联,再插入新的。
|
||||||
|
同时更新 wiki_tags 表中的计数。
|
||||||
|
"""
|
||||||
|
# 删除旧的标签关联
|
||||||
|
await self.execute_with_retry("DELETE FROM wiki_page_tags WHERE path = ?", (path,))
|
||||||
|
|
||||||
|
# 插入新的标签关联
|
||||||
|
for tag in tags:
|
||||||
|
await self.execute_with_retry(
|
||||||
|
"INSERT INTO wiki_page_tags (path, tag) VALUES (?, ?)",
|
||||||
|
(path, tag)
|
||||||
|
)
|
||||||
|
# 更新标签计数
|
||||||
|
await self.upsert_tag(tag)
|
||||||
|
|
||||||
async def get_all_tags(self) -> Dict[str, int]:
|
async def get_all_tags(self) -> Dict[str, int]:
|
||||||
"""获取所有标签"""
|
"""获取所有标签"""
|
||||||
rows = await self.fetch_all("SELECT tag, count FROM wiki_tags")
|
rows = await self.fetch_all("SELECT tag, count FROM wiki_tags")
|
||||||
@@ -469,22 +487,45 @@ class Database:
|
|||||||
# === FTS5 内容更新 ===
|
# === FTS5 内容更新 ===
|
||||||
|
|
||||||
async def update_fts_content(self, path: str, title: str, content: str, summary: str) -> None:
|
async def update_fts_content(self, path: str, title: str, content: str, summary: str) -> None:
|
||||||
"""更新 FTS5 内容"""
|
"""更新 FTS5 内容
|
||||||
# 先更新内容表
|
|
||||||
|
FTS5 虚拟表不支持 UPSERT,使用 DELETE + INSERT 替代。
|
||||||
|
所有内容(包括 content 字段)直接存储在 FTS 表中。
|
||||||
|
|
||||||
|
优化:连字符扩展(multi-agent → multi agent)以提升搜索召回率
|
||||||
|
"""
|
||||||
|
# 先删除旧记录(如果存在)
|
||||||
|
await self.execute_with_retry("DELETE FROM wiki_fts WHERE path = ?", (path,))
|
||||||
|
|
||||||
|
# 扩展连字符词汇(multi-agent → multi agent)
|
||||||
|
# 这样搜索 "multi" 或 "agent" 都能匹配到
|
||||||
|
expanded_title = self._expand_hyphenated_text(title)
|
||||||
|
expanded_summary = self._expand_hyphenated_text(summary)
|
||||||
|
|
||||||
|
# 插入扩展后的 title 和 summary(原版保留在 content 中)
|
||||||
await self.execute_with_retry(
|
await self.execute_with_retry(
|
||||||
"INSERT INTO wiki_content (path, content) VALUES (?, ?) ON CONFLICT(path) DO UPDATE SET content = excluded.content",
|
"INSERT INTO wiki_fts (path, title, summary, content) VALUES (?, ?, ?, ?)",
|
||||||
(path, content)
|
(path, f"{title} {expanded_title}", f"{summary} {expanded_summary}", content)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 再更新 FTS5 表(会自动从内容表同步)
|
def _expand_hyphenated_text(self, text: str) -> str:
|
||||||
await self.execute_with_retry(
|
"""扩展连字符词汇
|
||||||
"INSERT INTO wiki_fts (path, title, summary) VALUES (?, ?, ?) ON CONFLICT(path) DO UPDATE SET title = excluded.title, summary = excluded.summary",
|
|
||||||
(path, title, summary)
|
multi-agent system → multi agent system
|
||||||
)
|
这样搜索 "multi" 或 "agent" 都能匹配
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 原始文本
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
扩展后的文本(连字符替换为空格)
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
return text.replace('-', ' ')
|
||||||
|
|
||||||
async def delete_fts_content(self, path: str) -> None:
|
async def delete_fts_content(self, path: str) -> None:
|
||||||
"""删除 FTS5 内容"""
|
"""删除 FTS5 内容"""
|
||||||
await self.execute_with_retry("DELETE FROM wiki_content WHERE path = ?", (path,))
|
|
||||||
await self.execute_with_retry("DELETE FROM wiki_fts WHERE path = ?", (path,))
|
await self.execute_with_retry("DELETE FROM wiki_fts WHERE path = ?", (path,))
|
||||||
|
|
||||||
# === 状态查询 ===
|
# === 状态查询 ===
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ addopts =
|
|||||||
--cov-report=term-missing
|
--cov-report=term-missing
|
||||||
--cov-report=html
|
--cov-report=html
|
||||||
|
|
||||||
|
# asyncio 模式 - 自动处理异步 fixtures 和测试
|
||||||
|
asyncio_mode = auto
|
||||||
|
|
||||||
# 标记定义
|
# 标记定义
|
||||||
markers =
|
markers =
|
||||||
unit: 单元测试
|
unit: 单元测试
|
||||||
|
|||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# sanguo_llmwiki Deployment Script
|
||||||
|
# Deploys from development directory to installation directory
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
SRC_DIR="$HOME/.openclaw/sanguo_projects/sanguo_llmwiki"
|
||||||
|
DST_DIR="$HOME/.sanguo_projects/sanguo_llmwiki"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo "Deploying sanguo_llmwiki..."
|
||||||
|
echo "Source: $SRC_DIR"
|
||||||
|
echo "Target: $DST_DIR"
|
||||||
|
|
||||||
|
# Create target directory if it doesn't exist
|
||||||
|
mkdir -p "$DST_DIR"
|
||||||
|
|
||||||
|
# Deploy using rsync with exclusions
|
||||||
|
rsync -av --delete \
|
||||||
|
--exclude='.git/' \
|
||||||
|
--exclude='.gitignore' \
|
||||||
|
--exclude='.claude/' \
|
||||||
|
--exclude='__pycache__/' \
|
||||||
|
--exclude='*.pyc' \
|
||||||
|
--exclude='.pytest_cache/' \
|
||||||
|
--exclude='.python-version' \
|
||||||
|
--exclude='.coverage' \
|
||||||
|
--exclude='.ruff_cache/' \
|
||||||
|
--exclude='tests/' \
|
||||||
|
--exclude='pytest.ini' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='*.egg-info/' \
|
||||||
|
--exclude='dist/' \
|
||||||
|
--exclude='build/' \
|
||||||
|
--exclude='docs/' \
|
||||||
|
"$SRC_DIR/" "$DST_DIR/"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Code deployed successfully${NC}"
|
||||||
|
|
||||||
|
# Install dependencies using virtual environment
|
||||||
|
echo "Setting up virtual environment..."
|
||||||
|
cd "$DST_DIR"
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Dependencies installed${NC}"
|
||||||
|
|
||||||
|
# Update PM2 config to use virtual environment python (macOS sed syntax)
|
||||||
|
sed -i '' "s|script: 'python3'|script: '$DST_DIR/.venv/bin/python'|g" ecosystem.config.cjs
|
||||||
|
|
||||||
|
# Create necessary directories (use paths from ecosystem.config.cjs)
|
||||||
|
mkdir -p "$HOME/.sanguo-llmwiki"
|
||||||
|
mkdir -p "/Volumes/KnowledgeBase"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Deployment complete!${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Test MCP Server: cd $DST_DIR && source .venv/bin/activate && python -m mcp_server.main"
|
||||||
|
echo " 2. Start with PM2: pm2 start ecosystem.config.cjs"
|
||||||
@@ -124,8 +124,9 @@ metadata:
|
|||||||
"""测试使用 PyYAML 解析复杂 frontmatter"""
|
"""测试使用 PyYAML 解析复杂 frontmatter"""
|
||||||
content = """---
|
content = """---
|
||||||
name: complex-page
|
name: complex-page
|
||||||
description: Multi-line description
|
description: |
|
||||||
with multiple lines
|
Multi-line description
|
||||||
|
with multiple lines
|
||||||
tags:
|
tags:
|
||||||
- tag1
|
- tag1
|
||||||
- tag2
|
- tag2
|
||||||
|
|||||||
Reference in New Issue
Block a user