UNPKG

n8n

Version:

n8n Workflow Automation Tool

484 lines 23.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChatIntegrationService = void 0; const backend_common_1 = require("@n8n/backend-common"); const config_1 = require("@n8n/config"); const decorators_1 = require("@n8n/decorators"); const di_1 = require("@n8n/di"); const n8n_core_1 = require("n8n-core"); const credentials_service_1 = require("../../../credentials/credentials.service"); const publisher_service_1 = require("../../../scaling/pubsub/publisher.service"); const url_service_1 = require("../../../services/url.service"); const agent_chat_bridge_1 = require("./agent-chat-bridge"); const agent_chat_integration_1 = require("./agent-chat-integration"); const agent_chat_subscription_state_service_1 = require("./agent-chat-subscription-state.service"); const component_mapper_1 = require("./component-mapper"); const esm_loader_1 = require("./esm-loader"); const integration_tools_1 = require("./integration-tools"); const channel_integration_recorder_1 = require("./recording/channel-integration-recorder"); const recording_adapter_1 = require("./recording/recording-adapter"); const agent_repository_1 = require("../repositories/agent.repository"); async function getAgentExecutionOrchestratorService() { const { AgentExecutionOrchestratorService } = await import('../agent-execution-orchestrator.service.js'); return di_1.Container.get(AgentExecutionOrchestratorService); } let ChatIntegrationService = class ChatIntegrationService { constructor(logger, agentRepository, credentialsService, urlService, integrationRegistry, instanceSettings, publisher, globalConfig, chatSubscriptionStateService) { this.logger = logger; this.agentRepository = agentRepository; this.credentialsService = credentialsService; this.urlService = urlService; this.integrationRegistry = integrationRegistry; this.instanceSettings = instanceSettings; this.publisher = publisher; this.globalConfig = globalConfig; this.chatSubscriptionStateService = chatSubscriptionStateService; this.connections = new Map(); this.outboundConnections = new Map(); this.outboundConnectionInitializations = new Map(); } async broadcastIntegrationChange(agentId, integration, action) { if (!this.globalConfig.multiMainSetup.enabled) return; try { const payload = { agentId, integration, action }; await this.publisher.publishCommand({ command: 'agent-chat-integration-changed', payload, }); } catch (error) { this.logger.warn(`[ChatIntegrationService] Failed to publish ${action} for ${integration.type} on agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`); } } connectionKey(agentId, type, credentialId) { return `${agentId}:${type}:${credentialId}`; } integrationFromConnectionKey(key) { const type = key.split(':')[1]; return type ? this.integrationRegistry.get(type) : undefined; } async validateBeforeConnect(agentId, integration, projectId) { const implementation = this.integrationRegistry.require(integration.type); implementation.validateConfig?.(integration); if (!implementation.onBeforeConnect) return; const credential = await this.decryptCredentialForProject(integration.credentialId, projectId); await implementation.onBeforeConnect({ agentId, projectId, credentialId: integration.credentialId, credential, ingressEnabled: true, webhookUrlFor: (platform) => this.buildWebhookUrl(agentId, projectId, platform), }); } async connect(agentId, integration, projectId, options = {}) { const key = this.connectionKey(agentId, integration.type, integration.credentialId); const ingressEnabled = options.ingressEnabled ?? true; if (ingressEnabled) { await this.disconnectOutboundOne(key); if (this.connections.has(key)) { await this.disconnectOne(key); } } const integrationImpl = this.integrationRegistry.require(integration.type); const decryptedData = await this.decryptCredentialForProject(integration.credentialId, projectId); const ctx = { agentId, projectId, credentialId: integration.credentialId, credential: decryptedData, ingressEnabled, webhookUrlFor: (platform) => this.buildWebhookUrl(agentId, projectId, platform), }; if (ingressEnabled && integrationImpl.onBeforeConnect && !options.skipExternalHooks && !options.skipBeforeConnect) { await integrationImpl.onBeforeConnect(ctx); } let state; let chat; let bridge; let initializeStarted = false; try { const adapter = (0, recording_adapter_1.recordAdapterCalls)(integration.type, await integrationImpl.createAdapter(ctx)); channel_integration_recorder_1.channelIntegrationRecorder.startFetchRecording(); const { Chat } = await (0, esm_loader_1.loadChatSdk)(); const { createMemoryState } = await (0, esm_loader_1.loadMemoryState)(); const memoryState = createMemoryState(); state = ingressEnabled ? this.chatSubscriptionStateService.createStateAdapter({ agentId, integration, delegate: memoryState, }) : memoryState; chat = new Chat({ userName: `n8n-agent-${agentId}`, adapters: { [integration.type]: adapter }, state, }); if (ingressEnabled) { const componentMapper = new component_mapper_1.ComponentMapper(); const agentExecutionOrchestratorService = await getAgentExecutionOrchestratorService(); bridge = agent_chat_bridge_1.AgentChatBridge.create(chat, agentId, agentExecutionOrchestratorService, componentMapper, this.logger, projectId, integration); } initializeStarted = true; await chat.initialize(); if (ingressEnabled && integrationImpl.onAfterConnect && !options.skipExternalHooks) { await integrationImpl.onAfterConnect(ctx); } } catch (error) { if (initializeStarted) { await chat.shutdown().catch((shutdownError) => { this.logger.warn(`[ChatIntegrationService] Shutdown after failed connect threw: ${shutdownError instanceof Error ? shutdownError.message : String(shutdownError)}`); }); } else { await state?.disconnect().catch((disconnectError) => { this.logger.warn(`[ChatIntegrationService] State cleanup after failed setup threw: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); }); } await this.runDisconnectedHook(integrationImpl, ctx, `${key} after failed connect`); throw error; } const chatInstance = chat; const targetConnections = ingressEnabled ? this.connections : this.outboundConnections; targetConnections.set(key, { chat: chatInstance, bridge, context: ctx, }); if (integrationImpl.onConnected) { try { await integrationImpl.onConnected(ctx); } catch (error) { this.logger.warn(`[ChatIntegrationService] onConnected failed for ${key}: ${error instanceof Error ? error.message : String(error)}`); } } this.logger.info(`[ChatIntegrationService] ${ingressEnabled ? 'Connected' : 'Outbound connected'}: ${key}`); } async disconnect(agentId, integration, options = {}) { if (integration) { const key = this.connectionKey(agentId, integration.type, integration.credentialId); await this.disconnectOne(key, options); await this.disconnectOutboundOne(key); } else { const keysToRemove = new Set([ ...this.connections.keys(), ...this.outboundConnections.keys(), ...this.outboundConnectionInitializations.keys(), ].filter((key) => key.startsWith(`${agentId}:`))); for (const k of keysToRemove) { await this.disconnectOne(k, options); await this.disconnectOutboundOne(k); } } } async disconnectChannel(agentId, integration, options = {}) { const { deleteSubscriptions = true } = options; try { await this.disconnect(agentId, integration); await this.broadcastIntegrationChange(agentId, integration, 'disconnect'); } catch (error) { this.logger.warn(`[ChatIntegrationService] Disconnect failed for ${integration.type} on agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`); } if (!deleteSubscriptions) return; try { await this.chatSubscriptionStateService.deleteSubscriptionsForIntegration(agentId, integration); } catch (error) { this.logger.warn(`[ChatIntegrationService] Subscription cleanup failed for ${integration.type} on agent ${agentId}: ${error instanceof Error ? error.message : String(error)}`); } } async disconnectAll() { const keys = new Set([ ...this.connections.keys(), ...this.outboundConnections.keys(), ...this.outboundConnectionInitializations.keys(), ]); for (const key of keys) { await this.disconnectOne(key, { skipExternalHooks: true }); await this.disconnectOutboundOne(key); } } async disconnectLeaderOnlyIntegrations() { for (const key of [...this.connections.keys()]) { const integration = this.integrationFromConnectionKey(key); if (integration?.requiresLeader()) { await this.disconnectOne(key, { skipExternalHooks: true }); } } } async syncToConfig(agent, previous, next) { const previousKeys = new Set(previous.map(integration_tools_1.buildIntegrationConnectionId)); const nextKeys = new Set(next.map(integration_tools_1.buildIntegrationConnectionId)); for (const integration of previous) { if (!nextKeys.has((0, integration_tools_1.buildIntegrationConnectionId)(integration))) { await this.disconnectChannel(agent.id, integration); } } const additions = next.filter((i) => !previousKeys.has((0, integration_tools_1.buildIntegrationConnectionId)(i))); if (additions.length > 0 && !agent.activeVersionId) { this.logger.debug('[ChatIntegrationService] Skipping connect for unpublished agent — entry persisted, will connect on publish', { agentId: agent.id, pendingTypes: additions.map((i) => i.type) }); return; } for (const integration of additions) { const key = this.connectionKey(agent.id, integration.type, integration.credentialId); if (this.connections.has(key)) continue; try { await this.connect(agent.id, integration, agent.projectId); await this.broadcastIntegrationChange(agent.id, integration, 'connect'); } catch (error) { this.logger.warn('[ChatIntegrationService] Could not connect integration during sync', { agentId: agent.id, type: integration.type, credentialId: integration.credentialId, error, }); } } } getChatInstance(agentId, integration) { if (integration) { return this.connections.get(this.connectionKey(agentId, integration.type, integration.credentialId))?.chat; } for (const [k, conn] of this.connections) { if (k.startsWith(`${agentId}:`)) return conn.chat; } return undefined; } async getChatInstanceForTools(agentId, integration) { const live = this.getChatInstance(agentId, integration); if (live) return live; const key = this.connectionKey(agentId, integration.type, integration.credentialId); const handleInitializationError = (error) => { this.logger.warn('[ChatIntegrationService] Could not initialize outbound integration for Preview', { agentId, type: integration.type, credentialId: integration.credentialId, error, }); return undefined; }; const agent = await this.agentRepository .findOne({ where: { id: agentId } }) .catch(handleInitializationError); const persistedIntegration = agent?.integrations?.find((candidate) => candidate.type === integration.type && candidate.credentialId === integration.credentialId); if (!agent || agent.activeVersionId !== null || !persistedIntegration) { await this.disconnectOutboundOne(key); return undefined; } const currentLive = this.getChatInstance(agentId, integration); if (currentLive) return currentLive; const outbound = this.outboundConnections.get(key)?.chat; if (outbound) return outbound; const pending = this.outboundConnectionInitializations.get(key); if (pending) return await pending; const initialization = this.connect(agentId, persistedIntegration, agent.projectId, { ingressEnabled: false, }) .then(() => this.outboundConnections.get(key)?.chat) .catch(handleInitializationError); this.outboundConnectionInitializations.set(key, initialization); try { return await initialization; } finally { if (this.outboundConnectionInitializations.get(key) === initialization) { this.outboundConnectionInitializations.delete(key); } } } getShortenCallback(agentId, integration, metadata) { return this.connections .get(this.connectionKey(agentId, integration.type, integration.credentialId)) ?.bridge?.getShortenCallback(metadata); } getWebhookHandler(agentId, platform, connectionSelector) { const integration = this.integrationRegistry.get(platform); for (const [key, conn] of this.connections) { if (!key.startsWith(`${agentId}:${platform}:`)) continue; if (connectionSelector !== undefined && !integration?.matchesWebhookConnection?.(conn.context.credential, connectionSelector)) { continue; } return conn.chat.webhooks[platform]; } return undefined; } async reconnectAll() { const agents = await this.agentRepository.findPublished(); for (const agent of agents) { if (!agent.integrations || agent.integrations.length === 0) continue; for (const integration of agent.integrations) { const definition = this.integrationRegistry.get(integration.type); if (definition?.requiresLeader() && !this.instanceSettings.isLeader) { this.logger.debug(`[ChatIntegrationService] Skipping ${integration.type} for agent ${agent.id} — leader-only and this main is a follower`); continue; } const key = this.connectionKey(agent.id, integration.type, integration.credentialId); if (this.connections.has(key)) continue; const skipExternalHooks = !this.instanceSettings.isLeader; const options = this.connectOptionsFor(integration, skipExternalHooks); try { await this.connect(agent.id, integration, agent.projectId, options); } catch (error) { this.logger.error(`[ChatIntegrationService] Failed to reconnect ${integration.type} for agent ${agent.id} — credential not accessible to the project: ${error instanceof Error ? error.message : String(error)}`); } } } } async handleIntegrationChanged(payload) { const { agentId, integration, action } = payload; const { type, credentialId } = integration; if (action === 'disconnect') { await this.disconnect(agentId, integration, { skipExternalHooks: true }); return; } const definition = this.integrationRegistry.get(type); if (definition?.requiresLeader() && !this.instanceSettings.isLeader) { this.logger.debug(`[ChatIntegrationService] Ignoring connect for ${type} on agent ${agentId} — leader-only integration on follower`); return; } const key = this.connectionKey(agentId, type, credentialId); if (this.connections.has(key)) return; const agent = await this.agentRepository.findOne({ where: { id: agentId } }); if (!agent) { this.logger.warn(`[ChatIntegrationService] Cannot connect ${type} — agent ${agentId} not found`); return; } try { const options = { skipExternalHooks: true }; await this.connect(agentId, integration, agent.projectId, options); } catch (error) { this.logger.error(`[ChatIntegrationService] Failed to connect ${type} for agent ${agentId} — credential not accessible to the project: ${error instanceof Error ? error.message : String(error)}`); } } async disconnectOutboundOne(key) { await this.outboundConnectionInitializations.get(key); await this.disposeOutboundConnection(key); } async disposeOutboundConnection(key) { const conn = this.outboundConnections.get(key); if (!conn) return; try { await conn.chat.shutdown(); } catch (error) { this.logger.warn(`[ChatIntegrationService] Error during outbound shutdown for ${key}: ${error instanceof Error ? error.message : String(error)}`); } this.outboundConnections.delete(key); await this.runDisconnectedHook(this.integrationFromConnectionKey(key), conn.context, `outbound ${key}`); this.logger.info(`[ChatIntegrationService] Outbound disconnected: ${key}`); } async disconnectOne(key, options = {}) { const conn = this.connections.get(key); if (!conn) return; if (!options.skipExternalHooks) { const integration = this.integrationFromConnectionKey(key); if (integration?.onBeforeDisconnect) { try { await integration.onBeforeDisconnect(conn.context); } catch (error) { this.logger.warn(`[ChatIntegrationService] onBeforeDisconnect failed for ${key}: ${error instanceof Error ? error.message : String(error)}`); } } } try { await conn.chat.shutdown(); } catch (error) { this.logger.warn(`[ChatIntegrationService] Error during shutdown for ${key}: ${error instanceof Error ? error.message : String(error)}`); } this.connections.delete(key); await this.runDisconnectedHook(this.integrationFromConnectionKey(key), conn.context, key); this.logger.info(`[ChatIntegrationService] Disconnected: ${key}`); } async runDisconnectedHook(integration, context, label) { if (!integration?.onDisconnected) return; try { await integration.onDisconnected(context); } catch (error) { this.logger.warn(`[ChatIntegrationService] onDisconnected failed for ${label}: ${error instanceof Error ? error.message : String(error)}`); } } async decryptCredentialForProject(credentialId, projectId) { const projectCredentials = await this.credentialsService.findAllCredentialIdsForProject(projectId); const globalCredentials = await this.credentialsService.findAllGlobalCredentialIds(true); const credential = projectCredentials.find((c) => c.id === credentialId) ?? globalCredentials.find((c) => c.id === credentialId); if (!credential) { throw new Error(`Credential ${credentialId} not found or not accessible to project ${projectId}`); } const decrypted = await this.credentialsService.decrypt(credential, true); return decrypted; } buildWebhookUrl(agentId, projectId, platform) { const base = this.urlService.getWebhookBaseUrl(); return `${base}rest/projects/${projectId}/agents/v2/${agentId}/webhooks/${platform}`; } connectOptionsFor(integration, skipExternalHooks) { return 'settings' in integration ? { skipExternalHooks, settings: integration.settings } : { skipExternalHooks }; } }; exports.ChatIntegrationService = ChatIntegrationService; __decorate([ (0, decorators_1.OnLeaderStepdown)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], ChatIntegrationService.prototype, "disconnectLeaderOnlyIntegrations", null); __decorate([ (0, decorators_1.OnLeaderTakeover)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], ChatIntegrationService.prototype, "reconnectAll", null); __decorate([ (0, decorators_1.OnPubSubEvent)('agent-chat-integration-changed', { instanceType: 'main' }), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], ChatIntegrationService.prototype, "handleIntegrationChanged", null); exports.ChatIntegrationService = ChatIntegrationService = __decorate([ (0, di_1.Service)(), __metadata("design:paramtypes", [backend_common_1.Logger, agent_repository_1.AgentRepository, credentials_service_1.CredentialsService, url_service_1.UrlService, agent_chat_integration_1.ChatIntegrationRegistry, n8n_core_1.InstanceSettings, publisher_service_1.Publisher, config_1.GlobalConfig, agent_chat_subscription_state_service_1.AgentChatSubscriptionStateService]) ], ChatIntegrationService); //# sourceMappingURL=chat-integration.service.js.map