UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

179 lines (178 loc) 7.65 kB
/** * MCP Handlers — 搜索类 * * v2: 合并原 4 个搜索函数(search / contextSearch / keywordSearch / semanticSearch) * 为统一 search() 入口,通过 mode 参数路由。 * consolidated.ts 的 mode 路由直接指向本函数。 * * 设计原则: * 1. 通过 container.get('searchEngine') 获取 singleton 实例(含 vectorStore + aiProvider) * 2. 统一 responseTime、byKind 分组、kind 过滤 * 3. 投影使用 SearchTypes.slimSearchResult()(消除 3 处重复投影) */ import { groupByKind, slimSearchResult, } from '#service/search/SearchTypes.js'; import { envelope } from '../envelope.js'; // ─── 工具函数 ──────────────────────────────────────────────── /** * 获取 SearchEngine singleton(带 vectorStore + aiProvider) * 避免每次调用 new SearchEngine(db) —— 那样没有向量能力、每次重建索引 */ function getSearchEngine(ctx) { try { return ctx.container.get('searchEngine'); } catch { // 降级:直接创建基础实例(无向量能力) return null; } } /** 降级创建 SearchEngine(仅在 container 无法提供时) */ async function getFallbackEngine(ctx) { const { SearchEngine } = await import('#service/search/SearchEngine.js'); const db = ctx.container.get('database'); const knowledgeRepo = ctx.container.get('knowledgeRepository'); const sourceRefRepo = ctx.container.get('recipeSourceRefRepository'); return new SearchEngine(db, { knowledgeRepo, sourceRefRepo }); } /** 根据 kind 参数过滤 items */ function filterByKind(items, kind) { if (!kind || kind === 'all') { return items; } return items.filter((it) => (it.kind || it.metadata?.kind || 'pattern') === kind); } // ─── 统一搜索入口 ──────────────────────────────────────────── /** * 统一搜索入口 — 支持 auto / keyword / weighted / semantic / context 五种模式 * * 合并了原 search / contextSearch / keywordSearch / semanticSearch 4 个函数。 * mode 路由: * - auto (默认): FieldWeighted + semantic 融合 + Ranking Pipeline * - keyword: SQL LIKE 精确匹配,适合已知函数名/类名 * - weighted: 加权字段评分搜索(原 bm25 模式,已替换为 FieldWeightedScorer) * - bm25: weighted 的向后兼容别名 * - semantic: 向量语义搜索(不可用时降级 weighted) * - context: weighted + Ranking Pipeline + 会话上下文加成 * * 所有模式共享: kind 过滤 → slimSearchResult 投影 → byKind 分组 */ export async function search(ctx, args) { const t0 = Date.now(); const engine = getSearchEngine(ctx) || (await getFallbackEngine(ctx)); const query = args.query; const mode = args.mode || 'auto'; const kind = args.kind || args.type || 'all'; // ── Mode-specific 参数适配 ── // context 模式: 默认 limit=5, 传递 sessionHistory const isContext = mode === 'context'; const limit = args.limit ?? (isContext ? 5 : 10); // keyword 模式不排序(默认),其他模式排序 const rank = mode !== 'keyword'; // context 模式额外传递会话上下文 const context = isContext ? { intent: 'search', language: args.language, sessionHistory: args.sessionHistory || [], } : undefined; // kind 过滤时过采样 2x 以保证过滤后仍有足够结果 const recallLimit = kind !== 'all' ? limit * 2 : limit; // semantic 模式也过采样 2x(向量搜索可能有噪声) const engineLimit = mode === 'semantic' ? recallLimit * 2 : recallLimit; // ── 统一调用 SearchEngine ── const result = await engine.search(query, { mode: isContext ? 'bm25' : mode, limit: engineLimit, rank, groupByKind: true, context, }); let items = result?.items || []; const actualMode = result?.mode || mode; // ── Kind 过滤 + 截断 ── items = filterByKind(items, kind); items = items.slice(0, limit); // ── 统一投影: slimSearchResult() ── const slimItems = items.map(slimSearchResult); const byKindGroups = groupByKind(slimItems); const elapsed = Date.now() - t0; // ── 构造工具名称 ── const toolName = _toolName(mode); // ── semantic 降级提示 ── const degraded = mode === 'semantic' && actualMode !== 'semantic'; // ── 统一响应格式 ── const source = result?.ranked ? 'search-engine+ranking' : 'search-engine'; return envelope({ success: true, data: { query, mode: actualMode, kind: kind === 'all' ? undefined : kind, totalResults: slimItems.length, items: slimItems, byKind: byKindGroups, kindCounts: { rule: byKindGroups.rule.length, pattern: byKindGroups.pattern.length, fact: byKindGroups.fact.length, }, // semantic 模式专属: 降级提示 ...(mode === 'semantic' ? { degraded, degradedReason: degraded ? 'vectorStore/aiProvider 不可用,已降级到 BM25' : undefined, } : {}), // context 模式专属: metadata 包装(保持向后兼容) ...(isContext ? { metadata: { responseTimeMs: elapsed, totalResults: slimItems.length, kindCounts: { rule: byKindGroups.rule.length, pattern: byKindGroups.pattern.length, fact: byKindGroups.fact.length, }, }, } : {}), }, meta: { tool: toolName, source, responseTimeMs: elapsed }, }); } // ─── Backward-compatible aliases ──────────────────────────── // consolidated.ts 按 mode 路由时直接调用这些别名 /** contextSearch — mode='context' 的别名 */ export function contextSearch(ctx, args) { return search(ctx, { ...args, mode: 'context' }); } /** keywordSearch — mode='keyword' 的别名 */ export function keywordSearch(ctx, args) { return search(ctx, { ...args, mode: 'keyword' }); } /** semanticSearch — mode='semantic' 的别名 */ export function semanticSearch(ctx, args) { return search(ctx, { ...args, mode: 'semantic' }); } // ─── 内部辅助 ──────────────────────────────────────────────── /** 根据 mode 返回对应的 MCP 工具名称 */ function _toolName(mode) { switch (mode) { case 'context': return 'autosnippet_context_search'; case 'keyword': return 'autosnippet_keyword_search'; case 'semantic': return 'autosnippet_semantic_search'; default: return 'autosnippet_search'; } } // ─── Re-export slim projection for backward compatibility ──── // (部分内部模块可能直接 import 了这些) /** @deprecated Use `slimSearchResult` from `SearchTypes.ts` instead */ export function _slimSearchItem(item) { return slimSearchResult(item); }