File size: 13,481 Bytes
dd480ef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
/**
 * Webhook Auto-Bind System β€” NEW
 * Automatically detects trigger type from intent and assigns correct trigger node
 * Auto-generates webhook paths, auto-configures trigger settings
 * User NEVER manually defines triggers
 */
import type { WorkflowIntent, WorkflowGraph } from '../types/workflow';
import { getNodeDef } from '../knowledge/nodeRegistry';

export interface TriggerConfig {
  nodeType: string;
  displayName: string;
  parameters: Record<string, unknown>;
  credentials?: Record<string, { id: string; name: string }>;
  webhookPath?: string;
  notes: string;
}

// ─── Trigger detection rules (ordered by specificity) ─────────────────────────
const TRIGGER_RULES: Array<{
  keywords: string[];
  integrations?: string[];
  domain?: string[];
  triggerType: string;
  nodeType: string;
  priority: number;
}> = [
  // Telegram
  {
    keywords: ['telegram', 'bot', 'message', 'chat'],
    integrations: ['telegram'],
    triggerType: 'telegram',
    nodeType: 'n8n-nodes-base.telegramTrigger',
    priority: 100,
  },
  // Slack
  {
    keywords: ['slack', 'channel', 'workspace'],
    integrations: ['slack'],
    triggerType: 'slack',
    nodeType: 'n8n-nodes-base.slackTrigger',
    priority: 100,
  },
  // GitHub
  {
    keywords: ['github', 'push', 'pull request', 'issue', 'commit', 'repository'],
    integrations: ['github'],
    triggerType: 'github',
    nodeType: 'n8n-nodes-base.githubTrigger',
    priority: 100,
  },
  // Email (IMAP)
  {
    keywords: ['email', 'inbox', 'imap', 'mail received', 'new email', 'receive email'],
    integrations: ['email', 'imap', 'gmail'],
    triggerType: 'email',
    nodeType: 'n8n-nodes-base.emailReadImap',
    priority: 90,
  },
  // Notion
  {
    keywords: ['notion', 'database page', 'notion page'],
    integrations: ['notion'],
    triggerType: 'notion',
    nodeType: 'n8n-nodes-base.notionTrigger',
    priority: 90,
  },
  // Schedule / Cron
  {
    keywords: [
      'every hour', 'every day', 'daily', 'hourly', 'weekly', 'monthly', 'schedule',
      'cron', 'at midnight', 'every morning', 'every night', 'periodic', 'interval',
      'every minute', 'every 5 minutes', 'nightly', 'scheduled',
    ],
    triggerType: 'schedule',
    nodeType: 'n8n-nodes-base.scheduleTrigger',
    priority: 80,
  },
  // Webhook (general HTTP)
  {
    keywords: [
      'webhook', 'api call', 'http request', 'post request', 'form submission',
      'when called', 'endpoint', 'receive data', 'api endpoint', 'rest api',
      'when a request', 'when triggered via',
    ],
    triggerType: 'webhook',
    nodeType: 'n8n-nodes-base.webhook',
    priority: 70,
  },
  // Manual (fallback)
  {
    keywords: ['manually', 'manual trigger', 'on demand', 'test', 'demo'],
    triggerType: 'manual',
    nodeType: 'n8n-nodes-base.manualTrigger',
    priority: 10,
  },
];

export class WebhookAutoBindSystem {

  /**
   * Detect the appropriate trigger node type from intent and request text
   */
  detectTrigger(
    userRequest: string,
    intent: WorkflowIntent,
  ): { nodeType: string; triggerType: string; confidence: 'high' | 'medium' | 'low' } {
    const requestLower = userRequest.toLowerCase();
    const integrationLower = intent.integrations.map((i) => i.toLowerCase());

    let bestMatch: (typeof TRIGGER_RULES)[0] | undefined;
    let bestScore = 0;

    for (const rule of TRIGGER_RULES) {
      let score = 0;

      // Keyword matches in request
      const keywordMatches = rule.keywords.filter((kw) => requestLower.includes(kw));
      score += keywordMatches.length * 10;

      // Integration matches
      if (rule.integrations) {
        const integrationMatches = rule.integrations.filter((i) =>
          integrationLower.some((intent_i) => intent_i.includes(i) || i.includes(intent_i)),
        );
        score += integrationMatches.length * 20;
      }

      // Exact trigger type hint from intent
      if (intent.triggerType && intent.triggerType === rule.triggerType) {
        score += 50;
      }

      // Apply rule priority as tiebreaker
      score += rule.priority * 0.1;

      if (score > bestScore) {
        bestScore = score;
        bestMatch = rule;
      }
    }

    // Default to webhook if nothing matched
    const nodeType = bestMatch?.nodeType ?? 'n8n-nodes-base.webhook';
    const triggerType = bestMatch?.triggerType ?? 'webhook';
    const confidence: 'high' | 'medium' | 'low' =
      bestScore >= 20 ? 'high' : bestScore >= 10 ? 'medium' : 'low';

    return { nodeType, triggerType, confidence };
  }

  /**
   * Generate complete trigger node configuration
   */
  buildTriggerConfig(
    nodeType: string,
    userRequest: string,
    intent: WorkflowIntent,
    workflowName: string,
  ): TriggerConfig {
    switch (nodeType) {
      case 'n8n-nodes-base.webhook':
        return this.buildWebhookConfig(workflowName, intent);

      case 'n8n-nodes-base.telegramTrigger':
        return this.buildTelegramTriggerConfig();

      case 'n8n-nodes-base.scheduleTrigger':
        return this.buildScheduleConfig(userRequest);

      case 'n8n-nodes-base.emailReadImap':
        return this.buildEmailTriggerConfig();

      case 'n8n-nodes-base.slackTrigger':
        return this.buildSlackTriggerConfig();

      case 'n8n-nodes-base.githubTrigger':
        return this.buildGitHubTriggerConfig();

      case 'n8n-nodes-base.notionTrigger':
        return this.buildNotionTriggerConfig();

      case 'n8n-nodes-base.manualTrigger':
        return this.buildManualTriggerConfig();

      default:
        return this.buildWebhookConfig(workflowName, intent);
    }
  }

  /**
   * Inject auto-configured trigger into a WorkflowGraph as the first node
   */
  injectTriggerIntoGraph(
    graph: WorkflowGraph,
    triggerConfig: TriggerConfig,
    triggerNodeType: string,
  ): WorkflowGraph {
    const def = getNodeDef(triggerNodeType);
    const triggerId = 'trigger-auto-' + Date.now();

    // Check if graph already has a trigger node from the detected type
    const existingTrigger = graph.nodes.find((n) => n.layer === 'trigger');

    if (existingTrigger) {
      // Update existing trigger with proper config
      const updatedNodes = graph.nodes.map((node) => {
        if (node.layer !== 'trigger') return node;
        return {
          ...node,
          n8nNodeType: triggerNodeType,
          label: triggerConfig.displayName,
          autoConfigured: true,
          webhookPath: triggerConfig.webhookPath,
        };
      });
      return { ...graph, nodes: updatedNodes };
    }

    // Insert new trigger at front
    const newTriggerNode = {
      id: triggerId,
      label: triggerConfig.displayName,
      n8nNodeType: triggerNodeType,
      layer: 'trigger' as const,
      description: triggerConfig.notes,
      isCritical: true,
      autoConfigured: true,
      webhookPath: triggerConfig.webhookPath,
      position: { x: 0, y: 300 },
      parameters: triggerConfig.parameters,
      retryPolicy: undefined,
      dataContract: {
        inputs: [],
        outputs: ['body', 'headers', 'query', 'params'],
      },
    };

    // Shift existing nodes to the right
    const shiftedNodes = graph.nodes.map((node) => ({
      ...node,
      position: { x: (node.position?.x ?? 0) + 220, y: node.position?.y ?? 300 },
    }));

    // Connect trigger to first non-trigger node
    const firstNode = shiftedNodes[0];
    const newEdge = firstNode ? {
      id: `edge-trigger-to-${firstNode.id}`,
      sourceNodeId: triggerId,
      targetNodeId: firstNode.id,
      outputIndex: 0,
      inputIndex: 0,
      label: 'on receive',
    } : undefined;

    return {
      ...graph,
      nodes: [newTriggerNode, ...shiftedNodes],
      edges: newEdge ? [newEdge, ...graph.edges] : graph.edges,
    };
  }

  // ─── Trigger config builders ──────────────────────────────────────────────

  private buildWebhookConfig(workflowName: string, intent: WorkflowIntent): TriggerConfig {
    const slug = workflowName
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 40);
    const path = `wfo/${slug}`;

    return {
      nodeType: 'n8n-nodes-base.webhook',
      displayName: 'Webhook Trigger',
      parameters: {
        httpMethod: intent.syncVsAsync === 'sync' ? 'POST' : 'POST',
        path,
        responseMode: intent.syncVsAsync === 'sync' ? 'lastNode' : 'onReceived',
        responseData: 'allEntries',
        options: {},
      },
      webhookPath: path,
      notes: `Auto-configured webhook trigger. Path: /webhook/${path}. Method: POST. Response: ${intent.syncVsAsync === 'sync' ? 'synchronous (after processing)' : 'immediate acknowledgement'}.`,
    };
  }

  private buildTelegramTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.telegramTrigger',
      displayName: 'Telegram Trigger',
      parameters: {
        updates: ['message', 'callback_query'],
      },
      credentials: {
        telegramApi: { id: '', name: 'Telegram Bot API' },
      },
      notes: 'Auto-configured Telegram trigger. Listens for messages and callback queries. Requires telegramApi credential.',
    };
  }

  private buildScheduleConfig(userRequest: string): TriggerConfig {
    // Detect schedule from request text
    const req = userRequest.toLowerCase();
    let interval: Record<string, unknown>[] = [{ field: 'hours', hoursInterval: 1 }];

    if (req.includes('every minute')) interval = [{ field: 'minutes', minutesInterval: 1 }];
    else if (req.includes('every 5 minute')) interval = [{ field: 'minutes', minutesInterval: 5 }];
    else if (req.includes('every 15 minute')) interval = [{ field: 'minutes', minutesInterval: 15 }];
    else if (req.includes('every 30 minute')) interval = [{ field: 'minutes', minutesInterval: 30 }];
    else if (req.includes('every hour') || req.includes('hourly')) interval = [{ field: 'hours', hoursInterval: 1 }];
    else if (req.includes('every 6 hour')) interval = [{ field: 'hours', hoursInterval: 6 }];
    else if (req.includes('every 12 hour')) interval = [{ field: 'hours', hoursInterval: 12 }];
    else if (req.includes('daily') || req.includes('every day') || req.includes('nightly') || req.includes('midnight')) {
      interval = [{ field: 'cronExpression', expression: '0 0 * * *' }];
    }
    else if (req.includes('weekly') || req.includes('every week')) {
      interval = [{ field: 'cronExpression', expression: '0 9 * * 1' }]; // Monday 9am
    }
    else if (req.includes('monthly') || req.includes('every month')) {
      interval = [{ field: 'cronExpression', expression: '0 9 1 * *' }]; // 1st of month 9am
    }

    return {
      nodeType: 'n8n-nodes-base.scheduleTrigger',
      displayName: 'Schedule Trigger',
      parameters: {
        rule: { interval },
      },
      notes: `Auto-configured schedule trigger. Interval detected from request: ${JSON.stringify(interval)}.`,
    };
  }

  private buildEmailTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.emailReadImap',
      displayName: 'Email Trigger (IMAP)',
      parameters: {
        mailbox: 'INBOX',
        action: 'read',
        downloadAttachments: false,
        format: 'simple',
        options: {},
      },
      credentials: {
        imap: { id: '', name: 'IMAP Account' },
      },
      notes: 'Auto-configured IMAP email trigger. Monitors INBOX for new emails. Requires IMAP credential.',
    };
  }

  private buildSlackTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.slackTrigger',
      displayName: 'Slack Trigger',
      parameters: {
        trigger: 'any_message',
      },
      credentials: {
        slackOAuth2Api: { id: '', name: 'Slack OAuth2' },
      },
      notes: 'Auto-configured Slack trigger. Listens for messages in configured channels. Requires Slack OAuth2 credential.',
    };
  }

  private buildGitHubTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.githubTrigger',
      displayName: 'GitHub Trigger',
      parameters: {
        owner: '',
        repository: '',
        events: ['push'],
      },
      credentials: {
        githubApi: { id: '', name: 'GitHub API' },
      },
      notes: 'Auto-configured GitHub trigger. Listens for push events. Set owner and repository in parameters.',
    };
  }

  private buildNotionTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.notionTrigger',
      displayName: 'Notion Trigger',
      parameters: {
        databaseId: '',
        event: 'page_added',
        simple: true,
      },
      credentials: {
        notionApi: { id: '', name: 'Notion API' },
      },
      notes: 'Auto-configured Notion trigger. Monitors a database for new pages. Set databaseId in parameters.',
    };
  }

  private buildManualTriggerConfig(): TriggerConfig {
    return {
      nodeType: 'n8n-nodes-base.manualTrigger',
      displayName: 'Manual Trigger',
      parameters: {},
      notes: 'Manual trigger β€” start this workflow by clicking "Execute Workflow" in n8n.',
    };
  }
}

// ─── Singleton export ─────────────────────────────────────────────────────────
export const webhookAutoBind = new WebhookAutoBindSystem();