mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-04-05 09:03:07 +00:00
chore: add pre-built dist folder for npx usage
This commit is contained in:
committed by
Romuald Członkowski
parent
a70d96a373
commit
5057481e70
335
dist/services/node-similarity-service.js
vendored
Normal file
335
dist/services/node-similarity-service.js
vendored
Normal file
@@ -0,0 +1,335 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NodeSimilarityService = void 0;
|
||||
const logger_1 = require("../utils/logger");
|
||||
class NodeSimilarityService {
|
||||
constructor(repository) {
|
||||
this.nodeCache = null;
|
||||
this.cacheExpiry = 0;
|
||||
this.cacheVersion = 0;
|
||||
this.repository = repository;
|
||||
this.commonMistakes = this.initializeCommonMistakes();
|
||||
}
|
||||
initializeCommonMistakes() {
|
||||
const patterns = new Map();
|
||||
patterns.set('case_variations', [
|
||||
{ pattern: 'httprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
|
||||
{ pattern: 'webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
|
||||
{ pattern: 'slack', suggestion: 'nodes-base.slack', confidence: 0.9, reason: 'Missing package prefix' },
|
||||
{ pattern: 'gmail', suggestion: 'nodes-base.gmail', confidence: 0.9, reason: 'Missing package prefix' },
|
||||
{ pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.9, reason: 'Missing package prefix' },
|
||||
{ pattern: 'telegram', suggestion: 'nodes-base.telegram', confidence: 0.9, reason: 'Missing package prefix' },
|
||||
]);
|
||||
patterns.set('specific_variations', [
|
||||
{ pattern: 'HttpRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Incorrect capitalization' },
|
||||
{ pattern: 'HTTPRequest', suggestion: 'nodes-base.httpRequest', confidence: 0.95, reason: 'Common capitalization mistake' },
|
||||
{ pattern: 'Webhook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Incorrect capitalization' },
|
||||
{ pattern: 'WebHook', suggestion: 'nodes-base.webhook', confidence: 0.95, reason: 'Common capitalization mistake' },
|
||||
]);
|
||||
patterns.set('deprecated_prefixes', [
|
||||
{ pattern: 'n8n-nodes-base.', suggestion: 'nodes-base.', confidence: 0.95, reason: 'Full package name used instead of short form' },
|
||||
{ pattern: '@n8n/n8n-nodes-langchain.', suggestion: 'nodes-langchain.', confidence: 0.95, reason: 'Full package name used instead of short form' },
|
||||
]);
|
||||
patterns.set('typos', [
|
||||
{ pattern: 'htprequest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
|
||||
{ pattern: 'httpreqest', suggestion: 'nodes-base.httpRequest', confidence: 0.8, reason: 'Likely typo' },
|
||||
{ pattern: 'webook', suggestion: 'nodes-base.webhook', confidence: 0.8, reason: 'Likely typo' },
|
||||
{ pattern: 'slak', suggestion: 'nodes-base.slack', confidence: 0.8, reason: 'Likely typo' },
|
||||
{ pattern: 'googlesheets', suggestion: 'nodes-base.googleSheets', confidence: 0.8, reason: 'Likely typo' },
|
||||
]);
|
||||
patterns.set('ai_nodes', [
|
||||
{ pattern: 'openai', suggestion: 'nodes-langchain.openAi', confidence: 0.85, reason: 'AI node - incorrect package' },
|
||||
{ pattern: 'nodes-base.openai', suggestion: 'nodes-langchain.openAi', confidence: 0.9, reason: 'Wrong package - OpenAI is in LangChain package' },
|
||||
{ pattern: 'chatopenai', suggestion: 'nodes-langchain.lmChatOpenAi', confidence: 0.85, reason: 'LangChain node naming convention' },
|
||||
{ pattern: 'vectorstore', suggestion: 'nodes-langchain.vectorStoreInMemory', confidence: 0.7, reason: 'Generic vector store reference' },
|
||||
]);
|
||||
return patterns;
|
||||
}
|
||||
isCommonNodeWithoutPrefix(type) {
|
||||
const commonNodes = {
|
||||
'httprequest': 'nodes-base.httpRequest',
|
||||
'webhook': 'nodes-base.webhook',
|
||||
'slack': 'nodes-base.slack',
|
||||
'gmail': 'nodes-base.gmail',
|
||||
'googlesheets': 'nodes-base.googleSheets',
|
||||
'telegram': 'nodes-base.telegram',
|
||||
'discord': 'nodes-base.discord',
|
||||
'notion': 'nodes-base.notion',
|
||||
'airtable': 'nodes-base.airtable',
|
||||
'postgres': 'nodes-base.postgres',
|
||||
'mysql': 'nodes-base.mySql',
|
||||
'mongodb': 'nodes-base.mongoDb',
|
||||
};
|
||||
const normalized = type.toLowerCase();
|
||||
return commonNodes[normalized] || null;
|
||||
}
|
||||
async findSimilarNodes(invalidType, limit = 5) {
|
||||
if (!invalidType || invalidType.trim() === '') {
|
||||
return [];
|
||||
}
|
||||
const suggestions = [];
|
||||
const mistakeSuggestion = this.checkCommonMistakes(invalidType);
|
||||
if (mistakeSuggestion) {
|
||||
suggestions.push(mistakeSuggestion);
|
||||
}
|
||||
const allNodes = await this.getCachedNodes();
|
||||
const scores = allNodes.map(node => ({
|
||||
node,
|
||||
score: this.calculateSimilarityScore(invalidType, node)
|
||||
}));
|
||||
scores.sort((a, b) => b.score.totalScore - a.score.totalScore);
|
||||
for (const { node, score } of scores) {
|
||||
if (suggestions.some(s => s.nodeType === node.nodeType)) {
|
||||
continue;
|
||||
}
|
||||
if (score.totalScore >= NodeSimilarityService.SCORING_THRESHOLD) {
|
||||
suggestions.push(this.createSuggestion(node, score));
|
||||
}
|
||||
if (suggestions.length >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
checkCommonMistakes(invalidType) {
|
||||
const cleanType = invalidType.trim();
|
||||
const lowerType = cleanType.toLowerCase();
|
||||
const commonNodeSuggestion = this.isCommonNodeWithoutPrefix(cleanType);
|
||||
if (commonNodeSuggestion) {
|
||||
const node = this.repository.getNode(commonNodeSuggestion);
|
||||
if (node) {
|
||||
return {
|
||||
nodeType: commonNodeSuggestion,
|
||||
displayName: node.displayName,
|
||||
confidence: 0.9,
|
||||
reason: 'Missing package prefix',
|
||||
category: node.category,
|
||||
description: node.description
|
||||
};
|
||||
}
|
||||
}
|
||||
for (const [category, patterns] of this.commonMistakes) {
|
||||
if (category === 'deprecated_prefixes') {
|
||||
for (const pattern of patterns) {
|
||||
if (cleanType.startsWith(pattern.pattern)) {
|
||||
const actualSuggestion = cleanType.replace(pattern.pattern, pattern.suggestion);
|
||||
const node = this.repository.getNode(actualSuggestion);
|
||||
if (node) {
|
||||
return {
|
||||
nodeType: actualSuggestion,
|
||||
displayName: node.displayName,
|
||||
confidence: pattern.confidence,
|
||||
reason: pattern.reason,
|
||||
category: node.category,
|
||||
description: node.description
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [category, patterns] of this.commonMistakes) {
|
||||
if (category === 'deprecated_prefixes')
|
||||
continue;
|
||||
for (const pattern of patterns) {
|
||||
const match = category === 'specific_variations'
|
||||
? cleanType === pattern.pattern
|
||||
: lowerType === pattern.pattern.toLowerCase();
|
||||
if (match && pattern.suggestion) {
|
||||
const node = this.repository.getNode(pattern.suggestion);
|
||||
if (node) {
|
||||
return {
|
||||
nodeType: pattern.suggestion,
|
||||
displayName: node.displayName,
|
||||
confidence: pattern.confidence,
|
||||
reason: pattern.reason,
|
||||
category: node.category,
|
||||
description: node.description
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
calculateSimilarityScore(invalidType, node) {
|
||||
const cleanInvalid = this.normalizeNodeType(invalidType);
|
||||
const cleanValid = this.normalizeNodeType(node.nodeType);
|
||||
const displayNameClean = this.normalizeNodeType(node.displayName);
|
||||
const isShortSearch = invalidType.length <= NodeSimilarityService.SHORT_SEARCH_LENGTH;
|
||||
let nameSimilarity = Math.max(this.getStringSimilarity(cleanInvalid, cleanValid), this.getStringSimilarity(cleanInvalid, displayNameClean)) * 40;
|
||||
if (isShortSearch && (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid))) {
|
||||
nameSimilarity = Math.max(nameSimilarity, 10);
|
||||
}
|
||||
let categoryMatch = 0;
|
||||
if (node.category) {
|
||||
const categoryClean = this.normalizeNodeType(node.category);
|
||||
if (cleanInvalid.includes(categoryClean) || categoryClean.includes(cleanInvalid)) {
|
||||
categoryMatch = 20;
|
||||
}
|
||||
}
|
||||
let packageMatch = 0;
|
||||
const invalidParts = cleanInvalid.split(/[.-]/);
|
||||
const validParts = cleanValid.split(/[.-]/);
|
||||
if (invalidParts[0] === validParts[0]) {
|
||||
packageMatch = 15;
|
||||
}
|
||||
let patternMatch = 0;
|
||||
if (cleanValid.includes(cleanInvalid) || displayNameClean.includes(cleanInvalid)) {
|
||||
patternMatch = isShortSearch ? 45 : 25;
|
||||
}
|
||||
else if (this.getEditDistance(cleanInvalid, cleanValid) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
|
||||
patternMatch = 20;
|
||||
}
|
||||
else if (this.getEditDistance(cleanInvalid, displayNameClean) <= NodeSimilarityService.TYPO_EDIT_DISTANCE) {
|
||||
patternMatch = 18;
|
||||
}
|
||||
if (isShortSearch && (cleanValid.startsWith(cleanInvalid) || displayNameClean.startsWith(cleanInvalid))) {
|
||||
patternMatch = Math.max(patternMatch, 40);
|
||||
}
|
||||
const totalScore = nameSimilarity + categoryMatch + packageMatch + patternMatch;
|
||||
return {
|
||||
nameSimilarity,
|
||||
categoryMatch,
|
||||
packageMatch,
|
||||
patternMatch,
|
||||
totalScore
|
||||
};
|
||||
}
|
||||
createSuggestion(node, score) {
|
||||
let reason = 'Similar node';
|
||||
if (score.patternMatch >= 20) {
|
||||
reason = 'Name similarity';
|
||||
}
|
||||
else if (score.categoryMatch >= 15) {
|
||||
reason = 'Same category';
|
||||
}
|
||||
else if (score.packageMatch >= 10) {
|
||||
reason = 'Same package';
|
||||
}
|
||||
const confidence = Math.min(score.totalScore / 100, 1);
|
||||
return {
|
||||
nodeType: node.nodeType,
|
||||
displayName: node.displayName,
|
||||
confidence,
|
||||
reason,
|
||||
category: node.category,
|
||||
description: node.description
|
||||
};
|
||||
}
|
||||
normalizeNodeType(type) {
|
||||
return type
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '')
|
||||
.trim();
|
||||
}
|
||||
getStringSimilarity(s1, s2) {
|
||||
if (s1 === s2)
|
||||
return 1;
|
||||
if (!s1 || !s2)
|
||||
return 0;
|
||||
const distance = this.getEditDistance(s1, s2);
|
||||
const maxLen = Math.max(s1.length, s2.length);
|
||||
return 1 - (distance / maxLen);
|
||||
}
|
||||
getEditDistance(s1, s2, maxDistance = 5) {
|
||||
if (s1 === s2)
|
||||
return 0;
|
||||
const m = s1.length;
|
||||
const n = s2.length;
|
||||
const lengthDiff = Math.abs(m - n);
|
||||
if (lengthDiff > maxDistance)
|
||||
return maxDistance + 1;
|
||||
if (m === 0)
|
||||
return n;
|
||||
if (n === 0)
|
||||
return m;
|
||||
let prev = Array(n + 1).fill(0).map((_, i) => i);
|
||||
for (let i = 1; i <= m; i++) {
|
||||
const curr = [i];
|
||||
let minInRow = i;
|
||||
for (let j = 1; j <= n; j++) {
|
||||
const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
||||
const val = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
||||
curr.push(val);
|
||||
minInRow = Math.min(minInRow, val);
|
||||
}
|
||||
if (minInRow > maxDistance) {
|
||||
return maxDistance + 1;
|
||||
}
|
||||
prev = curr;
|
||||
}
|
||||
return prev[n];
|
||||
}
|
||||
async getCachedNodes() {
|
||||
const now = Date.now();
|
||||
if (!this.nodeCache || now > this.cacheExpiry) {
|
||||
try {
|
||||
const newNodes = this.repository.getAllNodes();
|
||||
if (newNodes && newNodes.length > 0) {
|
||||
this.nodeCache = newNodes;
|
||||
this.cacheExpiry = now + NodeSimilarityService.CACHE_DURATION_MS;
|
||||
this.cacheVersion++;
|
||||
logger_1.logger.debug('Node cache refreshed', {
|
||||
count: newNodes.length,
|
||||
version: this.cacheVersion
|
||||
});
|
||||
}
|
||||
else if (this.nodeCache) {
|
||||
logger_1.logger.warn('Node fetch returned empty, using stale cache');
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger_1.logger.error('Failed to fetch nodes for similarity service', error);
|
||||
if (this.nodeCache) {
|
||||
logger_1.logger.info('Using stale cache due to fetch error');
|
||||
return this.nodeCache;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return this.nodeCache || [];
|
||||
}
|
||||
invalidateCache() {
|
||||
this.nodeCache = null;
|
||||
this.cacheExpiry = 0;
|
||||
this.cacheVersion++;
|
||||
logger_1.logger.debug('Node cache invalidated', { version: this.cacheVersion });
|
||||
}
|
||||
async refreshCache() {
|
||||
this.invalidateCache();
|
||||
await this.getCachedNodes();
|
||||
}
|
||||
formatSuggestionMessage(suggestions, invalidType) {
|
||||
if (suggestions.length === 0) {
|
||||
return `Unknown node type: "${invalidType}". No similar nodes found.`;
|
||||
}
|
||||
let message = `Unknown node type: "${invalidType}"\n\nDid you mean one of these?\n`;
|
||||
for (const suggestion of suggestions) {
|
||||
const confidence = Math.round(suggestion.confidence * 100);
|
||||
message += `• ${suggestion.nodeType} (${confidence}% match)`;
|
||||
if (suggestion.displayName) {
|
||||
message += ` - ${suggestion.displayName}`;
|
||||
}
|
||||
message += `\n → ${suggestion.reason}`;
|
||||
if (suggestion.confidence >= 0.9) {
|
||||
message += ' (can be auto-fixed)';
|
||||
}
|
||||
message += '\n';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
isAutoFixable(suggestion) {
|
||||
return suggestion.confidence >= NodeSimilarityService.AUTO_FIX_CONFIDENCE;
|
||||
}
|
||||
clearCache() {
|
||||
this.invalidateCache();
|
||||
}
|
||||
}
|
||||
exports.NodeSimilarityService = NodeSimilarityService;
|
||||
NodeSimilarityService.SCORING_THRESHOLD = 50;
|
||||
NodeSimilarityService.TYPO_EDIT_DISTANCE = 2;
|
||||
NodeSimilarityService.SHORT_SEARCH_LENGTH = 5;
|
||||
NodeSimilarityService.CACHE_DURATION_MS = 5 * 60 * 1000;
|
||||
NodeSimilarityService.AUTO_FIX_CONFIDENCE = 0.9;
|
||||
//# sourceMappingURL=node-similarity-service.js.map
|
||||
Reference in New Issue
Block a user