@holder-mcp/local-knowledge-base
Version:
Holder公司本地知识库MCP客户端,提供项目文档检索、模块信息查询和架构信息获取等工具
435 lines • 17.8 kB
JavaScript
;
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