UNPKG

@holder-mcp/local-knowledge-base

Version:

Holder公司本地知识库MCP客户端,提供项目文档检索、模块信息查询和架构信息获取等工具

435 lines 17.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DocumentService = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const glob = __importStar(require("glob")); const chokidar = __importStar(require("chokidar")); /** * 文档服务类 * 负责文档收集、上传、文件监听等功能 */ class DocumentService { constructor() { this.watchers = new Map(); this.supportedExtensions = { MARKDOWN: ['.md', '.markdown', '.mdown', '.mkdn'], CODE: ['.js', '.ts', '.jsx', '.tsx', '.vue', '.java', '.py', '.go', '.rust', '.cpp', '.c', '.h', '.hpp', '.cs', '.php', '.rb', '.swift', '.kt', '.scala', '.clj', '.hs', '.elm', '.dart', '.sql', '.yml', '.yaml', '.json', '.xml', '.html', '.css', '.scss', '.less'], TEXT: ['.txt', '.log', '.cfg', '.conf', '.ini', '.env', '.gitignore', '.dockerignore'], PDF: ['.pdf'] }; } /** * 收集指定路径下的所有文档 */ async collectDocuments(documentPaths, workingDirectory) { const documents = []; // 确定工作目录 const baseDir = workingDirectory || this.getWorkingDirectory(); console.log(`📁 使用工作目录: ${baseDir}`); for (const docPath of documentPaths) { try { // 如果是绝对路径,直接使用;否则相对于工作目录解析 const resolvedPath = path.isAbsolute(docPath) ? docPath : path.resolve(baseDir, docPath); console.log(`🔍 处理路径: ${docPath} -> ${resolvedPath}`); const stat = await fs.stat(resolvedPath); if (stat.isFile()) { // 单个文件 const doc = await this.processFile(resolvedPath, baseDir); if (doc) { documents.push(doc); } } else if (stat.isDirectory()) { // 目录 - 递归扫描 const dirDocs = await this.scanDirectory(resolvedPath, baseDir); documents.push(...dirDocs); } } catch (error) { console.warn(`处理路径 ${docPath} 时出错: ${error.message}`); } } return documents; } /** * 获取工作目录 * 优先级:环境变量 PWD > INIT_CWD > process.cwd() */ getWorkingDirectory() { // 尝试从环境变量获取真实的工作目录 const pwd = process.env.PWD; const initCwd = process.env.INIT_CWD; const currentCwd = process.cwd(); console.log(`🔍 检测工作目录:`); console.log(` PWD: ${pwd}`); console.log(` INIT_CWD: ${initCwd}`); console.log(` process.cwd(): ${currentCwd}`); // 优先使用PWD(Unix系统的当前工作目录) if (pwd && fs.existsSync(pwd)) { return pwd; } // 其次使用INIT_CWD(npm/yarn启动时的初始目录) if (initCwd && fs.existsSync(initCwd)) { return initCwd; } // 最后使用process.cwd() return currentCwd; } /** * 扫描目录下的所有支持的文档文件 */ async scanDirectory(dirPath, baseDir) { const documents = []; const allExtensions = Object.values(this.supportedExtensions).flat(); // 构建glob模式 const patterns = allExtensions.map(ext => `**/*${ext}`); for (const pattern of patterns) { try { const files = await glob.glob(pattern, { cwd: dirPath, absolute: true, ignore: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/.vscode/**', '**/.idea/**', '**/target/**', '**/bin/**', '**/obj/**' ] }); for (const file of files) { const doc = await this.processFile(file, baseDir); if (doc) { documents.push(doc); } } } catch (error) { console.warn(`扫描目录 ${dirPath} 时出错: ${error.message}`); } } return documents; } /** * 处理单个文件 */ async processFile(filePath, baseDir) { try { const stat = await fs.stat(filePath); const ext = path.extname(filePath).toLowerCase(); const docType = this.getDocumentType(ext); if (!docType) { return null; // 不支持的文件类型 } // 检查文件大小(限制10MB) if (stat.size > 10 * 1024 * 1024) { console.warn(`文件过大,跳过: ${filePath} (${stat.size} bytes)`); return null; } // 读取文件内容 let content; let encoding = 'utf-8'; if (docType === 'PDF') { // PDF文件暂时跳过,后续可以集成PDF解析库 console.warn(`暂不支持PDF文件: ${filePath}`); return null; } else { // 文本文件 content = await fs.readFile(filePath, 'utf-8'); } // 计算文件校验和 const checksum = crypto.createHash('sha256').update(content).digest('hex'); // 计算相对路径,优先使用baseDir const relativePath = baseDir ? path.relative(baseDir, filePath) : path.relative(process.cwd(), filePath); return { filePath: relativePath, fileName: path.basename(filePath), content, encoding, size: stat.size, lastModified: stat.mtime, checksum, type: docType }; } catch (error) { console.warn(`处理文件 ${filePath} 时出错: ${error.message}`); return null; } } /** * 根据文件扩展名确定文档类型 */ getDocumentType(extension) { for (const [type, extensions] of Object.entries(this.supportedExtensions)) { if (extensions.includes(extension)) { return type; } } return null; } /** * 将DocumentInfo转换为ClientDocumentDto */ documentInfoToDto(doc) { return { filePath: doc.filePath, fileName: doc.fileName, content: doc.content, encoding: doc.encoding, size: doc.size, lastModified: doc.lastModified.toISOString(), checksum: doc.checksum, type: doc.type }; } /** * 启动文件监听 */ startWatching(projectName, documentPaths, debounceMs = 30000, serverUrl = 'http://localhost:8888') { return new Promise((resolve, reject) => { try { // 停止现有的监听器(如果存在) this.stopWatching(projectName); // 构建监听模式 const watchPatterns = []; const allExtensions = Object.values(this.supportedExtensions).flat(); for (const docPath of documentPaths) { const resolvedPath = path.resolve(docPath); // 为每个路径添加监听模式 for (const ext of allExtensions) { if (fs.existsSync(resolvedPath)) { const stat = fs.statSync(resolvedPath); if (stat.isDirectory()) { watchPatterns.push(path.join(resolvedPath, `**/*${ext}`)); } else if (stat.isFile() && resolvedPath.endsWith(ext)) { watchPatterns.push(resolvedPath); } } } } if (watchPatterns.length === 0) { reject(new Error('没有找到可监听的文件或目录')); return; } console.log(`开始监听项目 ${projectName} 的文档变化...`); console.log(`监听模式: ${watchPatterns.join(', ')}`); const watcher = chokidar.watch(watchPatterns, { ignored: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/.vscode/**', '**/.idea/**', '**/target/**', '**/bin/**', '**/obj/**' ], persistent: true, ignoreInitial: true, usePolling: false }); const watcherState = { projectName, watchedPaths: documentPaths, isActive: true }; this.watchers.set(projectName, watcherState); // 设置事件监听 const handleFileChange = (eventType, filePath) => { console.log(`检测到文件${eventType}: ${filePath}`); // 清除现有的防抖动定时器 if (watcherState.debounceTimer) { clearTimeout(watcherState.debounceTimer); } // 设置新的防抖动定时器 watcherState.debounceTimer = setTimeout(async () => { try { console.log(`防抖动时间到,开始更新项目 ${projectName} 的索引...`); watcherState.lastUpdateTime = new Date(); // 重新收集并上传文档 await this.uploadProjectDocuments(projectName, documentPaths, serverUrl); console.log(`项目 ${projectName} 索引更新完成`); } catch (error) { console.error(`更新项目 ${projectName} 索引时出错: ${error.message}`); } }, debounceMs); }; watcher .on('add', (filePath) => handleFileChange('add', filePath)) .on('change', (filePath) => handleFileChange('change', filePath)) .on('unlink', (filePath) => handleFileChange('unlink', filePath)) .on('error', (error) => { console.error(`文件监听出错: ${error.message}`); }) .on('ready', () => { console.log(`文件监听器已就绪,正在监听项目 ${projectName}`); resolve(`✅ 已启动对项目 ${projectName} 的文档监听\n` + `📁 监听路径: ${documentPaths.join(', ')}\n` + `⏱️ 防抖动延迟: ${debounceMs}ms (${debounceMs / 1000}秒)\n` + `🔄 当检测到文件变化时,将在 ${debounceMs / 1000} 秒后自动更新知识库索引`); }); // 存储watcher引用以便后续清理 watcherState.watcher = watcher; } catch (error) { reject(new Error(`启动文件监听失败: ${error.message}`)); } }); } /** * 停止文件监听 */ stopWatching(projectName) { const watcherState = this.watchers.get(projectName); if (watcherState) { if (watcherState.debounceTimer) { clearTimeout(watcherState.debounceTimer); } const watcher = watcherState.watcher; if (watcher) { watcher.close(); } watcherState.isActive = false; this.watchers.delete(projectName); console.log(`已停止对项目 ${projectName} 的文件监听`); } } /** * 获取监听状态 */ getWatcherStatus() { const status = {}; for (const [projectName, state] of this.watchers) { status[projectName] = { projectName: state.projectName, watchedPaths: state.watchedPaths, isActive: state.isActive, lastUpdateTime: state.lastUpdateTime }; } return status; } /** * 上传项目文档到服务端 */ async uploadProjectDocuments(projectName, documentPaths, serverUrl = 'http://localhost:8888', workingDirectory) { try { console.log(`开始收集项目 ${projectName} 的文档...`); // 收集文档 const documents = await this.collectDocuments(documentPaths, workingDirectory); if (documents.length === 0) { return `⚠️ 在指定路径中没有找到任何支持的文档文件\n路径: ${documentPaths.join(', ')}`; } console.log(`收集到 ${documents.length} 个文档,准备上传...`); // 转换为DTO格式 const documentDtos = documents.map(doc => this.documentInfoToDto(doc)); // 上传到服务端 const result = await this.callUploadAPI(serverUrl, projectName, documentDtos); return `✅ 项目 ${projectName} 文档上传成功\n` + `📄 文档数量: ${documents.length}\n` + `📁 扫描路径: ${documentPaths.join(', ')}\n` + `🔄 服务端响应: ${result}`; } catch (error) { throw new Error(`上传项目文档失败: ${error.message}`); } } /** * 调用服务端上传API */ async callUploadAPI(serverUrl, projectName, documents) { return new Promise((resolve, reject) => { const http = require('http'); const url = require('url'); const serverUrlObj = new url.URL(serverUrl); const postData = JSON.stringify(documents); const options = { hostname: serverUrlObj.hostname, port: serverUrlObj.port || 8888, path: `/api/v1/projects/${encodeURIComponent(projectName)}/documents/upload`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; const req = http.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { resolve(responseData); } else { reject(new Error(`HTTP ${res.statusCode}: ${responseData}`)); } } catch (error) { reject(new Error(`解析响应失败: ${error.message}`)); } }); }); req.on('error', (error) => { reject(new Error(`请求失败: ${error.message}`)); }); req.setTimeout(60000, () => { req.destroy(); reject(new Error('请求超时')); }); req.write(postData); req.end(); }); } } exports.DocumentService = DocumentService; //# sourceMappingURL=document-service.js.map