@xiaohui-wang/mcpadvisor
Version:
MCP Advisor & Installation - Find the right MCP server for your needs
193 lines (192 loc) • 6.96 kB
JavaScript
import { RerankMcpServer } from './core/search/RerankMcpService.js';
import { OfflineSearchProvider } from './core/search/OfflineSearchProvider.js';
import logger from '../utils/logger.js';
/**
* 提供者优先级配置
*/
const PROVIDER_PRIORITIES = {
OfflineSearchProvider: 5,
GetMcpSearchProvider: 10,
CompassSearchProvider: 8,
MeilisearchSearchProvider: 9,
};
/**
* 默认搜索选项
*/
const DEFAULT_SEARCH_OPTIONS = {
limit: 5,
minSimilarity: 0.4,
};
/**
* 默认离线模式配置
*/
const DEFAULT_OFFLINE_CONFIG = {
enabled: true,
minSimilarity: 0.3,
};
/**
* Search service that can use multiple search providers
*/
export class SearchService {
providers = [];
offlineProvider;
offlineConfig;
reranker;
/**
* Create a new search service with the specified providers
* @param providers - Array of search providers to use
* @param offlineConfig - 离线模式配置,默认启用
*/
constructor(providers = [], offlineConfig = {}) {
this.providers = providers;
this.reranker = new RerankMcpServer(PROVIDER_PRIORITIES);
// 合并离线模式配置
this.offlineConfig = {
...DEFAULT_OFFLINE_CONFIG,
...offlineConfig,
};
// 如果启用了离线模式,初始化离线搜索提供者
if (this.offlineConfig.enabled) {
this.initOfflineProvider();
}
logger.info(`SearchService initialized with ${providers.length} providers`, {
providerCount: providers.length,
offlineMode: this.offlineConfig.enabled,
});
}
/**
* 初始化离线搜索提供者
*/
initOfflineProvider() {
try {
this.offlineProvider = new OfflineSearchProvider({
fallbackDataPath: this.offlineConfig.fallbackDataPath,
minSimilarity: this.offlineConfig.minSimilarity,
});
logger.info('Offline search provider initialized');
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error(`Failed to initialize offline search provider: ${message}`, {
error,
});
}
}
/**
* Add a new search provider
* @param provider - The search provider to add
*/
addProvider(provider) {
this.providers.push(provider);
logger.info(`New provider added, total providers: ${this.providers.length}`);
}
/**
* Remove a search provider
* @param index - The index of the provider to remove
*/
removeProvider(index) {
if (index >= 0 && index < this.providers.length) {
this.providers.splice(index, 1);
logger.info(`Provider removed, total providers: ${this.providers.length}`);
}
else {
logger.warn(`Invalid provider index: ${index}`);
}
}
/**
* Get all current providers
* @returns Array of search providers
*/
getProviders() {
return [...this.providers];
}
async search(arg, options) {
// 解析参数
const params = typeof arg === 'string'
? { taskDescription: arg }
: arg;
if (this.providers.length === 0 && !this.offlineProvider) {
logger.warn('No search providers available');
return [];
}
try {
// 合并默认选项
const mergedOptions = { ...DEFAULT_SEARCH_OPTIONS, ...options };
logger.info(`Searching with ${this.providers.length} providers for task: ${params.taskDescription}`, 'SearchService', { providerCount: this.providers.length });
const allProviders = [...this.providers];
let offlineProviderIndex = -1;
// 如果启用了离线模式,添加离线提供者
if (this.offlineConfig.enabled && this.offlineProvider) {
offlineProviderIndex = allProviders.length;
allProviders.push(this.offlineProvider);
}
// Collect results from all providers in parallel
const providerPromises = this.searchAllProviders(allProviders, params);
const namedProviderResults = await Promise.all(providerPromises);
this.logProviderResults(namedProviderResults);
return this.reranker.reRank(namedProviderResults, mergedOptions);
}
catch (error) {
logger.error(`Error in search service: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Search all providers in parallel
* @param allProviders - Array of all providers to search
* @param params - Search parameters
*/
searchAllProviders(allProviders, params) {
return allProviders.map((provider, index) => {
const providerName = provider.constructor.name;
logger.info(`Starting search with provider ${index + 1}/${this.providers.length}: ${providerName}`, 'Provider', {
providerName,
providerIndex: index,
params,
});
return provider
.search(params)
.then(results => {
logger.info(`Provider ${providerName} returned ${results.length} results`, 'Provider', {
providerName,
resultCount: results.length,
topResults: results.slice(0, 3).map(r => ({
title: r.title,
similarity: r.similarity,
github_url: r.sourceUrl,
})),
});
// Log full results at debug level
if (results.length > 0) {
logger.debug(`Full results from provider ${providerName}:`, 'Provider', {
providerName,
results,
});
}
return {
providerName,
results,
};
})
.catch(error => {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Provider ${providerName} search failed: ${errorMessage}`, 'Provider', {
providerName,
error: errorMessage,
});
return {
providerName,
results: [],
};
});
});
}
logProviderResults(namedProviderResults) {
namedProviderResults.forEach(({ providerName, results }) => {
logger.info(`Provider ${providerName} found ${results.length} results`, 'SearchSummary', {
providerName,
resultCount: results.length,
});
});
}
}