UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

824 lines (823 loc) 36.7 kB
/** * project-access.js — 项目数据访问工具 (5) * * 1. search_project_code 搜索项目源码 * 2. read_project_file 读取项目文件 * 2b. list_project_structure 列出项目目录结构 * 2c. get_file_summary 文件结构摘要 * 2d. semantic_search_code 语义知识搜索 */ import fs from 'node:fs'; import path from 'node:path'; import { LanguageProfiles } from '#shared/LanguageProfiles.js'; import { LanguageService } from '#shared/LanguageService.js'; // ─── 共享常量 ────────────────────────────────────────────── /** 三方库路径识别 — 从 LanguageProfiles 统一派生 */ export const THIRD_PARTY_RE = LanguageProfiles.thirdPartyPathRegex; /** 源码文件扩展名 — 从 LanguageService 统一派生 */ export const SOURCE_EXT_RE = LanguageService.sourceExtRegex; /** 声明行识别 — 用于对匹配行打分(与 bootstrap/shared/scanner.js 对齐) */ const DECL_RE = /^\s*(@property\b|@interface\b|@protocol\b|@class\b|@synthesize\b|@dynamic\b|@end\b|NS_ASSUME_NONNULL|#import\b|#include\b|#define\b)/; const TYPE_DECL_RE = /^\s*\w[\w<>*\s]+[\s*]+_?\w+\s*;$/; function _scoreSearchLine(line) { const t = line.trim(); if (DECL_RE.test(t)) { return -2; } if (TYPE_DECL_RE.test(t)) { return -1; } if (/^[-+]\s*\([^)]+\)\s*\w+[^{]*;\s*$/.test(t)) { return -1; } if (/\[.*\w+.*\]/.test(t)) { return 2; // ObjC message send } if (/\w+\s*\(/.test(t)) { return 2; // function call } if (/\^\s*[{(]/.test(t)) { return 1; // block literal } return 0; } /** * 收集项目文件列表 — 抽取为公用函数,供单次和批量搜索复用。 * 优先使用内存缓存(bootstrap 场景),否则从磁盘递归读取。 */ async function _getProjectFiles(params, ctx) { const { fileFilter } = params; const projectRoot = ctx.projectRoot || process.cwd(); let extFilter = null; if (fileFilter) { const exts = fileFilter.split(',').map((e) => e.trim().replace(/^\./, '')); extFilter = new RegExp(`\\.(${exts.join('|')})$`, 'i'); } const fileCache = ctx.fileCache || null; let files; let skippedThirdParty = 0; if (fileCache && Array.isArray(fileCache)) { files = fileCache.filter((f) => { const p = f.relativePath || f.path || ''; if (THIRD_PARTY_RE.test(p)) { skippedThirdParty++; return false; } if (extFilter && !extFilter.test(p)) { return false; } if (!SOURCE_EXT_RE.test(p)) { return false; } return true; }); } else { files = []; const MAX_FILE_SIZE = 512 * 1024; const walk = (dir, relBase = '') => { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const relPath = relBase ? `${relBase}/${entry.name}` : entry.name; const fullPath = path.join(dir, entry.name); const isDir = entry.isDirectory() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isDirectory(); } catch { return false; } })()); const isFile = entry.isFile() || (entry.isSymbolicLink() && (() => { try { return fs.statSync(fullPath).isFile(); } catch { return false; } })()); if (isDir) { if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'build') { continue; } if (THIRD_PARTY_RE.test(`${relPath}/`)) { skippedThirdParty++; continue; } walk(fullPath, relPath); } else if (isFile) { if (THIRD_PARTY_RE.test(relPath)) { skippedThirdParty++; continue; } if (!SOURCE_EXT_RE.test(entry.name)) { continue; } if (extFilter && !extFilter.test(entry.name)) { continue; } try { const stat = fs.statSync(fullPath); if (stat.size > MAX_FILE_SIZE) { continue; } const content = fs.readFileSync(fullPath, 'utf-8'); files.push({ relativePath: relPath, content, name: entry.name }); } catch { /* skip unreadable files */ } } } } catch { /* skip inaccessible dirs */ } }; walk(projectRoot); } return { files, skippedThirdParty }; } // ─── 1. search_project_code ──────────────────────────────── export const searchProjectCode = { name: 'search_project_code', description: '在用户项目源码中搜索指定模式。返回匹配的代码片段及上下文。' + '自动过滤三方库代码(Pods/Carthage/node_modules),优先返回实际使用行而非声明行。' + '适用场景:验证代码模式存在性、查找更多项目示例、理解项目中某个 API 的用法。' + '批量搜索:传入 patterns 数组可一次搜索多个关键词(每个关键词独立返回结果),减少工具调用次数。', parameters: { type: 'object', properties: { pattern: { type: 'string', description: '搜索词或正则表达式(单个搜索时使用)' }, patterns: { type: 'array', items: { type: 'string' }, description: '批量搜索:多个搜索词数组,如 ["methodA", "methodB", "classC"]。与 pattern 互斥,优先使用 patterns。', }, isRegex: { type: 'boolean', description: '是否为正则表达式,默认 false' }, fileFilter: { type: 'string', description: '文件扩展名过滤,如 ".m,.swift"' }, contextLines: { type: 'number', description: '匹配行前后的上下文行数,默认 3' }, maxResults: { type: 'number', description: '每个 pattern 的最大返回结果数,默认 5' }, }, required: [], }, handler: async (params, ctx) => { // ── 去重缓存初始化 ── const state = ctx._sharedState || ctx; if (!state._searchCache) { state._searchCache = new Map(); } // ── 批量模式:patterns 数组 ── if (Array.isArray(params.patterns) && params.patterns.length > 0) { const batchPatterns = params.patterns.slice(0, 10); const batchResults = {}; let dedupCount = 0; for (const p of batchPatterns) { const cacheKey = `${p}|${params.isRegex || false}|${params.fileFilter || ''}`; const cached = state._searchCache.get(cacheKey); if (cached) { batchResults[p] = { ...cached, _cached: true }; dedupCount++; continue; } const sub = (await searchProjectCode.handler({ ...params, pattern: p, patterns: undefined }, ctx)); const entry = { matches: sub.matches ?? [], total: sub.total ?? 0 }; state._searchCache.set(cacheKey, entry); batchResults[p] = entry; } return { batchResults, patternsSearched: batchPatterns.length, searchedFiles: (await _getProjectFiles(params, ctx)).files.length, ...(dedupCount > 0 ? { _deduped: dedupCount, hint: `${dedupCount} 个 pattern 命中缓存,请避免重复搜索相同关键词。`, } : {}), }; } // 兼容 AI 传 "query" / "search" / "keyword" 替代 "pattern" const pattern = params.pattern || params.query || params.search || params.keyword || params.search_query; const { isRegex = false, contextLines = 3, maxResults = 5 } = params; if (!pattern || typeof pattern !== 'string') { return { error: '参数错误: 请提供 pattern(搜索关键词或正则表达式)或 patterns 数组', matches: [], total: 0, }; } // ── 单 pattern 去重检查 ── const cacheKey = `${pattern}|${params.isRegex || false}|${params.fileFilter || ''}`; if (state._searchCache.has(cacheKey)) { const cached = state._searchCache.get(cacheKey); return { ...cached, _cached: true, hint: `⚠ 已搜索过 "${pattern}",返回缓存结果。请搜索不同的关键词以获取新信息。`, }; } // 构建搜索正则 let searchRe; try { searchRe = isRegex ? new RegExp(pattern, 'gi') : new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); } catch (err) { return { error: `Invalid pattern: ${err.message}`, matches: [], total: 0 }; } const { files, skippedThirdParty } = await _getProjectFiles(params, ctx); // 搜索匹配 const matches = []; let total = 0; for (const f of files) { if (!f.content) { continue; } searchRe.lastIndex = 0; if (!searchRe.test(f.content)) { continue; } const lines = f.content.split('\n'); searchRe.lastIndex = 0; for (let i = 0; i < lines.length; i++) { searchRe.lastIndex = 0; if (!searchRe.test(lines[i])) { continue; } total++; if (matches.length < maxResults) { const start = Math.max(0, i - contextLines); const end = Math.min(lines.length - 1, i + contextLines); const contextArr = []; for (let j = start; j <= end; j++) { contextArr.push(lines[j]); } matches.push({ file: f.relativePath || f.path || f.name || '', line: i + 1, code: lines[i], context: contextArr.join('\n'), score: _scoreSearchLine(lines[i]), }); } } } // 按 score 降序排列(实际使用行优先) matches.sort((a, b) => b.score - a.score); const result = { matches, total, searchedFiles: files.length, skippedThirdParty, ...(() => { state._searchCallCount = (state._searchCallCount || 0) + 1; if (state._searchCallCount > 12 && ctx.source === 'system') { return { hint: `💡 你已搜索 ${state._searchCallCount} 次。考虑使用 get_class_info / get_class_hierarchy / get_project_overview 获取结构化信息,效率更高。`, }; } return {}; })(), }; state._searchCache.set(cacheKey, { matches: result.matches, total: result.total }); return result; }, }; // ─── 2. read_project_file ────────────────────────────────── export const readProjectFile = { name: 'read_project_file', description: '读取项目中指定文件的内容(部分或全部)。' + '通常在 search_project_code 找到匹配后使用,获取更完整的上下文。' + '批量读取:传入 filePaths 数组可一次读取多个文件,减少工具调用次数。', parameters: { type: 'object', properties: { filePath: { type: 'string', description: '相对于项目根目录的文件路径(单个文件时使用)' }, filePaths: { type: 'array', items: { type: 'string' }, description: '批量读取:多个文件路径数组。与 filePath 互斥,优先使用 filePaths。', }, startLine: { type: 'number', description: '起始行号(1-based),默认 1' }, endLine: { type: 'number', description: '结束行号(1-based),默认文件末尾' }, maxLines: { type: 'number', description: '最大返回行数,默认 200(批量模式下每个文件最多 100 行)', }, }, required: [], }, handler: async (params, ctx) => { // ── 去重缓存初始化 ── const state = ctx._sharedState || ctx; if (!state._readCache) { state._readCache = new Map(); } // ── 批量模式:filePaths 数组 ── if (Array.isArray(params.filePaths) && params.filePaths.length > 0) { const batchPaths = params.filePaths.slice(0, 8); const batchResults = {}; let dedupCount = 0; for (const fp of batchPaths) { const cacheKey = `${fp}|${params.startLine || 1}|${params.endLine || ''}|${params.maxLines || 100}`; if (state._readCache.has(cacheKey)) { batchResults[fp] = { ...state._readCache.get(cacheKey), _cached: true }; dedupCount++; continue; } const sub = (await readProjectFile.handler({ ...params, filePath: fp, filePaths: undefined, maxLines: Math.min(params.maxLines || 100, 100), }, ctx)); const entry = sub.error ? { error: sub.error } : { content: sub.content, totalLines: sub.totalLines, language: sub.language }; state._readCache.set(cacheKey, entry); batchResults[fp] = entry; } return { batchResults, filesRead: batchPaths.length, ...(dedupCount > 0 ? { _deduped: dedupCount, hint: `${dedupCount} 个文件命中缓存,请避免重复读取相同文件。` } : {}), }; } const filePath = params.filePath || params.path || params.file_path || params.filepath || params.file || params.filename; const { startLine = 1, maxLines = 200 } = params; const projectRoot = ctx.projectRoot || process.cwd(); if (!filePath || typeof filePath !== 'string') { return { error: '参数错误: 请提供 filePath(相对于项目根目录的文件路径)或 filePaths 数组' }; } // ── 单文件去重检查 ── const readCacheKey = `${filePath}|${startLine}|${params.endLine || ''}|${maxLines}`; if (state._readCache.has(readCacheKey)) { return { ...state._readCache.get(readCacheKey), _cached: true, hint: `⚠ 已读取过该文件相同行范围,返回缓存结果。如需其他行范围请指定不同的 startLine/endLine。`, }; } // 安全检查: 禁止路径遍历 const normalized = path.normalize(filePath); if (normalized.startsWith('..') || path.isAbsolute(normalized)) { return { error: 'Path traversal not allowed. Use relative paths within the project.' }; } // 优先从内存缓存读取(bootstrap 场景) const fileCache = ctx.fileCache || null; let content = null; if (fileCache && Array.isArray(fileCache)) { const cached = fileCache.find((f) => (f.relativePath || f.path || '') === filePath || (f.relativePath || f.path || '') === normalized); if (cached) { content = cached.content ?? null; } } // 降级: 从磁盘读取 if (content === null) { const fullPath = path.resolve(projectRoot, normalized); if (!fullPath.startsWith(projectRoot)) { return { error: 'Path traversal not allowed.' }; } try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (err) { return { error: `File not found or unreadable: ${err.message}` }; } } const allLines = content.split('\n'); const totalLines = allLines.length; const start = Math.max(1, startLine); let end = params.endLine || totalLines; end = Math.min(end, totalLines); if (end - start + 1 > maxLines) { end = start + maxLines - 1; } const selectedLines = allLines.slice(start - 1, end); const ext = path.extname(filePath).toLowerCase(); const language = LanguageService.langFromExt(ext); const readResult = { filePath, totalLines, startLine: start, endLine: end, content: selectedLines.join('\n'), language, }; state._readCache.set(readCacheKey, { content: readResult.content, totalLines, language }); return readResult; }, }; // ─── 2b. list_project_structure ──────────────────────────── export const listProjectStructure = { name: 'list_project_structure', description: '列出项目目录结构和文件统计信息。不读取文件内容,只返回目录树和元数据。' + '适用场景:了解项目整体布局、识别关键目录、规划探索路径。', parameters: { type: 'object', properties: { directory: { type: 'string', description: '相对于项目根目录的子目录路径,默认根目录' }, depth: { type: 'number', description: '目录展开深度,默认 3' }, includeStats: { type: 'boolean', description: '是否包含文件统计(语言分布、行数),默认 true', }, }, }, handler: async (params, ctx) => { const directory = params.directory || ''; const depth = Math.min(params.depth ?? 3, 5); const includeStats = params.includeStats !== false; const projectRoot = ctx.projectRoot || process.cwd(); const normalized = path.normalize(directory); if (normalized.startsWith('..') || path.isAbsolute(normalized)) { return { error: 'Path traversal not allowed. Use relative paths within the project.' }; } const targetDir = directory ? path.resolve(projectRoot, normalized) : projectRoot; if (!targetDir.startsWith(projectRoot)) { return { error: 'Path traversal not allowed.' }; } const treeLines = []; const stats = { totalFiles: 0, totalDirs: 0, byLanguage: {}, totalLines: 0, }; const walk = (dir, relBase, currentDepth, prefix) => { if (currentDepth > depth) { return; } let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } entries.sort((a, b) => { const aIsDir = a.isDirectory(); const bIsDir = b.isDirectory(); if (aIsDir !== bIsDir) { return aIsDir ? -1 : 1; } return a.name.localeCompare(b.name); }); entries = entries.filter((e) => { if (e.name.startsWith('.')) { return false; } const rel = relBase ? `${relBase}/${e.name}` : e.name; if (THIRD_PARTY_RE.test(`${rel}/`)) { return false; } return true; }); for (let i = 0; i < entries.length; i++) { const entry = entries[i]; const isLast = i === entries.length - 1; const connector = isLast ? '└── ' : '├── '; const childPrefix = prefix + (isLast ? ' ' : '│ '); const rel = relBase ? `${relBase}/${entry.name}` : entry.name; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { let childCount = 0; try { childCount = fs.readdirSync(fullPath).length; } catch { /* skip */ } treeLines.push(`${prefix}${connector}${entry.name}/ (${childCount})`); stats.totalDirs++; walk(fullPath, rel, currentDepth + 1, childPrefix); } else if (entry.isFile()) { const ext = path.extname(entry.name).toLowerCase(); let lineCount = 0; let size = 0; if (includeStats) { try { const st = fs.statSync(fullPath); size = st.size; if (SOURCE_EXT_RE.test(entry.name) && size < 512 * 1024) { const content = fs.readFileSync(fullPath, 'utf-8'); lineCount = content.split('\n').length; stats.totalLines += lineCount; } } catch { /* skip */ } } const lang = LanguageService.displayNameFromExt(ext); if (lang !== ext) { stats.byLanguage[lang] = (stats.byLanguage[lang] || 0) + 1; } const sizeLabel = size > 1024 ? `${(size / 1024).toFixed(0)}KB` : `${size}B`; const lineLabel = lineCount > 0 ? `, ${lineCount}L` : ''; treeLines.push(`${prefix}${connector}${entry.name} (${sizeLabel}${lineLabel})`); stats.totalFiles++; } } }; walk(targetDir, directory, 1, ''); return { directory: directory || '.', tree: treeLines.join('\n'), stats: includeStats ? stats : undefined, }; }, }; // ─── 2c. get_file_summary ────────────────────────────────── /** 语言相关的声明提取正则 */ const SUMMARY_EXTRACTORS = { objectivec: { imports: /^\s*(#import\s+.+|#include\s+.+|@import\s+\w+;)/gm, declarations: /^\s*(@interface\s+\w+[\s:(].*|@protocol\s+\w+[\s<(].*|@implementation\s+\w+|typedef\s+(?:NS_ENUM|NS_OPTIONS)\s*\([^)]+\)\s*\{?)/gm, methods: /^\s*[-+]\s*\([^)]+\)\s*[^;{]+/gm, properties: /^\s*@property\s*\([^)]*\)\s*[^;]+;/gm, }, swift: { imports: /^\s*import\s+\w+/gm, declarations: /^\s*(?:open|public|internal|fileprivate|private|final)?\s*(?:class|struct|enum|protocol|actor|extension)\s+\w+[^{]*/gm, methods: /^\s*(?:open|public|internal|fileprivate|private|override|static|class)?\s*func\s+\w+[^{]*/gm, properties: /^\s*(?:open|public|internal|fileprivate|private|static|class|lazy)?\s*(?:var|let)\s+\w+\s*:\s*[^={\n]+/gm, }, javascript: { imports: /^\s*(?:import\s+.+from\s+['"].+['"]|const\s+\{?\s*\w+.*\}?\s*=\s*require\s*\(.+\))/gm, declarations: /^\s*(?:export\s+)?(?:default\s+)?(?:class|function|const|let|var)\s+\w+/gm, methods: /^\s*(?:async\s+)?(?:static\s+)?(?:get\s+|set\s+)?(?:#?\w+)\s*\([^)]*\)\s*\{/gm, }, typescript: { imports: /^\s*import\s+.+from\s+['"].+['"]/gm, declarations: /^\s*(?:export\s+)?(?:default\s+)?(?:class|interface|type|enum|function|const|let|var|abstract\s+class)\s+\w+/gm, methods: /^\s*(?:async\s+)?(?:static\s+)?(?:public|private|protected)?\s*(?:get\s+|set\s+)?(?:#?\w+)\s*\([^)]*\)\s*[:{]/gm, }, python: { imports: /^\s*(?:import\s+\w+|from\s+\w+\s+import\s+.+)/gm, declarations: /^\s*class\s+\w+[^:]*:/gm, methods: /^\s*(?:async\s+)?def\s+\w+\s*\([^)]*\)/gm, }, go: { imports: /^\s*(?:import\s+"[^"]+"|import\s+\w+\s+"[^"]+")/gm, declarations: /^\s*(?:type\s+\w+\s+(?:struct|interface|func)\b.*)/gm, methods: /^\s*func\s+(?:\(\s*\w+\s+\*?\w+\s*\)\s+)?\w+\s*\([^)]*\)[^{]*/gm, }, java: { imports: /^\s*import\s+(?:static\s+)?[\w.]+\*?;/gm, declarations: /^\s*(?:public|private|protected)?\s*(?:abstract|final|static)?\s*(?:class|interface|enum|record|@interface)\s+\w+/gm, methods: /^\s*(?:public|private|protected)?\s*(?:abstract|static|final|synchronized|default)?\s*(?:<[^>]+>\s+)?\w[\w<>[\],\s]*\s+\w+\s*\([^)]*\)/gm, }, kotlin: { imports: /^\s*import\s+[\w.]+/gm, declarations: /^\s*(?:open|abstract|data|sealed|inner|value|inline)?\s*(?:class|interface|object|enum\s+class|fun\s+interface)\s+\w+/gm, methods: /^\s*(?:override\s+)?(?:suspend\s+)?(?:fun|val|var)\s+(?:<[^>]+>\s+)?\w+/gm, }, dart: { imports: /^\s*import\s+['"][^'"]+['"];?/gm, declarations: /^\s*(?:abstract\s+|sealed\s+)?(?:class|mixin|extension|enum|typedef)\s+\w+[^{]*/gm, methods: /^\s*(?:@override\s+)?(?:static\s+)?(?:Future|Stream|void|\w[\w<>?]*)?\s+\w+\s*\([^)]*\)/gm, properties: /^\s*(?:static\s+)?(?:final\s+|late\s+|const\s+)?(?:\w[\w<>?]*)\s+\w+\s*[;=]/gm, }, }; SUMMARY_EXTRACTORS['objectivec++'] = SUMMARY_EXTRACTORS.objectivec; SUMMARY_EXTRACTORS.jsx = SUMMARY_EXTRACTORS.javascript; SUMMARY_EXTRACTORS.tsx = SUMMARY_EXTRACTORS.typescript; export const getFileSummary = { name: 'get_file_summary', description: '获取文件的结构摘要(导入、声明、方法签名),不包含实现代码。' + '比 read_project_file 更轻量,适合快速了解文件角色和 API。', parameters: { type: 'object', properties: { filePath: { type: 'string', description: '相对于项目根目录的文件路径' }, }, required: ['filePath'], }, handler: async (params, ctx) => { const filePath = params.filePath || params.file_path || params.path || params.file; const projectRoot = ctx.projectRoot || process.cwd(); if (!filePath || typeof filePath !== 'string') { return { error: '参数错误: 请提供 filePath' }; } const normalized = path.normalize(filePath); if (normalized.startsWith('..') || path.isAbsolute(normalized)) { return { error: 'Path traversal not allowed.' }; } const fileCache = ctx.fileCache || null; let content = null; if (fileCache && Array.isArray(fileCache)) { const cached = fileCache.find((f) => (f.relativePath || f.path || '') === filePath || (f.relativePath || f.path || '') === normalized); if (cached) { content = cached.content ?? null; } } if (content === null) { const fullPath = path.resolve(projectRoot, normalized); if (!fullPath.startsWith(projectRoot)) { return { error: 'Path traversal not allowed.' }; } try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (err) { return { error: `File not found or unreadable: ${err.message}` }; } } const ext = path.extname(filePath).toLowerCase(); const language = LanguageService.langFromExt(ext); const extractor = SUMMARY_EXTRACTORS[language]; const result = { filePath, language, lineCount: content.split('\n').length, imports: [], declarations: [], methods: [], properties: [], }; if (!extractor) { result.preview = content.split('\n').slice(0, 30).join('\n'); return result; } const extract = (regex) => { const matches = []; let m; regex.lastIndex = 0; while ((m = regex.exec(content)) !== null) { matches.push(m[0].trim()); } return matches; }; if (extractor.imports) { result.imports = extract(extractor.imports); } if (extractor.declarations) { result.declarations = extract(extractor.declarations); } if (extractor.methods) { result.methods = extract(extractor.methods).slice(0, 50); } if (extractor.properties) { result.properties = extract(extractor.properties).slice(0, 30); } return result; }, }; // ─── 2d. semantic_search_code ────────────────────────────── export const semanticSearchCode = { name: 'semantic_search_code', description: '在知识库中进行语义搜索。使用自然语言描述你要查找的代码模式或概念,' + '返回语义最相关的知识条目。比关键词搜索更适合模糊/概念性查询。' + '示例: "网络请求的错误处理策略"、"线程安全的单例实现"', parameters: { type: 'object', properties: { query: { type: 'string', description: '自然语言搜索查询' }, topK: { type: 'number', description: '返回结果数量,默认 5' }, category: { type: 'string', description: '按分类过滤 (View/Service/Network/Model 等)' }, language: { type: 'string', description: '按语言过滤 (swift/objectivec 等)' }, }, required: ['query'], }, handler: async (params, ctx) => { const query = params.query || params.search || params.keyword; const topK = Math.min(params.topK ?? 5, 20); const { category, language } = params; if (!query || typeof query !== 'string') { return { error: '参数错误: 请提供 query (自然语言搜索查询)' }; } let searchEngine = null; try { searchEngine = ctx.container?.get('searchEngine') ?? null; } catch { /* not available */ } if (!searchEngine) { let vectorStore = null; try { vectorStore = ctx.container?.get('vectorStore') ?? null; } catch { /* not available */ } if (!vectorStore) { return { error: '语义搜索不可用: SearchEngine 和 VectorStore 均未初始化。可使用 search_project_code 进行关键词搜索替代。', fallbackTool: 'search_project_code', }; } let aiProvider = null; try { aiProvider = ctx.container?.get('aiProvider') ?? null; } catch { /* not available */ } if (!aiProvider || typeof aiProvider.generateEmbedding !== 'function') { const filter = {}; if (category) { filter.category = category; } if (language) { filter.language = language; } const results = await vectorStore.hybridSearch([], query, { topK, filter }); return { mode: 'keyword-fallback', query, message: 'AI Provider 不支持 embedding,已降级到关键词匹配', results: results.map((r) => ({ id: r.item.id, content: (r.item.content || '').slice(0, 500), score: Math.round(r.score * 100) / 100, metadata: r.item.metadata || {}, })), }; } try { const embedding = await aiProvider.generateEmbedding(query); const filter = {}; if (category) { filter.category = category; } if (language) { filter.language = language; } const results = await vectorStore.hybridSearch(embedding, query, { topK, filter }); return { mode: 'vector', query, results: results.map((r) => ({ id: r.item.id, content: (r.item.content || '').slice(0, 500), score: Math.round(r.score * 100) / 100, metadata: r.item.metadata || {}, })), }; } catch (err) { return { error: `向量搜索失败: ${err.message}`, fallbackTool: 'search_project_code', }; } } // 使用 SearchEngine (BM25 + 可选向量) try { const result = await searchEngine.search(query, { mode: 'semantic', limit: topK * 2, groupByKind: true, }); let items = result?.items || []; const actualMode = result?.mode || 'bm25'; if (category) { items = items.filter((i) => (i.category || '').toLowerCase() === category.toLowerCase()); } if (language) { items = items.filter((i) => (i.language || '').toLowerCase() === language.toLowerCase()); } items = items.slice(0, topK); return { mode: actualMode, query, degraded: actualMode !== 'semantic', totalResults: items.length, results: items.map((item) => ({ id: item.id, title: item.title || '', content: (item.content || item.description || '').slice(0, 500), score: Math.round((item.score || 0) * 100) / 100, knowledgeType: item.knowledgeType || item.kind || '', category: item.category || '', language: item.language || '', })), }; } catch (err) { return { error: `搜索失败: ${err.message}`, fallbackTool: 'search_project_code' }; } }, };