UNPKG

n8n

Version:

n8n Workflow Automation Tool

374 lines 18.6 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(); } 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}`; } connectionTypeFromKey(key) { const parts = key.split(':'); return parts.length >= 3 ? parts[1] : undefined; } async connect(agentId, integration, projectId, options = {}) { const key = this.connectionKey(agentId, integration.type, integration.credentialId); 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, webhookUrlFor: (platform) => this.buildWebhookUrl(agentId, projectId, platform), }; if (integrationImpl.onBeforeConnect && !options.skipExternalHooks) { await integrationImpl.onBeforeConnect(ctx); } 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)(); let state; let chat; let bridge; let initializeStarted = false; try { state = this.chatSubscriptionStateService.createStateAdapter({ agentId, integration, delegate: createMemoryState(), }); chat = new Chat({ userName: `n8n-agent-${agentId}`, adapters: { [integration.type]: adapter }, state, }); 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 (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)}`); }); } bridge?.dispose(); throw error; } const chatInstance = chat; this.connections.set(key, { chat: chatInstance, bridge, context: ctx, }); this.logger.info(`[ChatIntegrationService] Connected: ${key}`); } async disconnect(agentId, integration, options = {}) { if (integration) { await this.disconnectOne(this.connectionKey(agentId, integration.type, integration.credentialId), options); } else { const keysToRemove = [...this.connections.keys()].filter((k) => k.startsWith(`${agentId}:`)); for (const k of keysToRemove) { await this.disconnectOne(k, options); } } } 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 = [...this.connections.keys()]; for (const key of keys) { await this.disconnectOne(key, { skipExternalHooks: true }); } } async disconnectLeaderOnlyIntegrations() { for (const key of [...this.connections.keys()]) { const type = this.connectionTypeFromKey(key); if (!type) continue; const integration = this.integrationRegistry.get(type); 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, }); } } } getStatus(agentId) { const integrations = []; for (const k of this.connections.keys()) { if (k.startsWith(`${agentId}:`)) { const parts = k.split(':'); if (parts.length >= 3) { integrations.push({ type: parts[1], credentialId: parts.slice(2).join(':') }); } } } return { status: integrations.length > 0 ? 'connected' : 'disconnected', connections: integrations.length, integrations, }; } 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; } getShortenCallback(agentId, integration) { return this.connections .get(this.connectionKey(agentId, integration.type, integration.credentialId)) ?.bridge.getShortenCallback(); } getWebhookHandler(agentId, platform) { for (const [key, conn] of this.connections) { if (key.startsWith(`${agentId}:${platform}:`)) { 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 disconnectOne(key, options = {}) { const conn = this.connections.get(key); if (!conn) return; if (!options.skipExternalHooks) { const type = this.connectionTypeFromKey(key); const integration = type ? this.integrationRegistry.get(type) : undefined; 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)}`); } conn.bridge.dispose(); this.connections.delete(key); this.logger.info(`[ChatIntegrationService] Disconnected: ${key}`); } 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