n8n
Version:
n8n Workflow Automation Tool
484 lines • 23.9 kB
JavaScript
"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