n8n
Version:
n8n Workflow Automation Tool
374 lines • 18.6 kB
JavaScript
;
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