2024-09-12 09:07:18 -07:00
|
|
|
import { GlobalConfig } from '@n8n/config';
|
2023-03-24 09:04:26 -07:00
|
|
|
import glob from 'fast-glob';
|
2023-10-09 07:09:23 -07:00
|
|
|
import fsPromises from 'fs/promises';
|
2023-12-27 02:50:43 -08:00
|
|
|
import type { Class, DirectoryLoader, Types } from 'n8n-core';
|
2019-06-23 03:35:23 -07:00
|
|
|
import {
|
2022-11-23 07:20:28 -08:00
|
|
|
CUSTOM_EXTENSION_ENV,
|
2024-12-11 06:36:17 -08:00
|
|
|
ErrorReporter,
|
2023-10-23 04:39:35 -07:00
|
|
|
InstanceSettings,
|
2022-11-23 07:20:28 -08:00
|
|
|
CustomDirectoryLoader,
|
|
|
|
PackageDirectoryLoader,
|
|
|
|
LazyPackageDirectoryLoader,
|
2024-12-10 05:48:39 -08:00
|
|
|
UnrecognizedCredentialTypeError,
|
|
|
|
UnrecognizedNodeTypeError,
|
2024-12-23 04:46:13 -08:00
|
|
|
Logger,
|
2022-11-23 07:20:28 -08:00
|
|
|
} from 'n8n-core';
|
|
|
|
import type {
|
|
|
|
KnownNodesAndCredentials,
|
2024-09-04 03:06:17 -07:00
|
|
|
INodeTypeBaseDescription,
|
2023-02-03 04:14:59 -08:00
|
|
|
INodeTypeDescription,
|
2023-10-09 07:09:23 -07:00
|
|
|
INodeTypeData,
|
|
|
|
ICredentialTypeData,
|
2024-12-10 05:48:39 -08:00
|
|
|
LoadedClass,
|
|
|
|
ICredentialType,
|
|
|
|
INodeType,
|
|
|
|
IVersionedNodeType,
|
2024-12-12 04:54:44 -08:00
|
|
|
INodeProperties,
|
2019-06-23 03:35:23 -07:00
|
|
|
} from 'n8n-workflow';
|
2024-12-12 04:54:44 -08:00
|
|
|
import { ApplicationError, NodeConnectionType } from 'n8n-workflow';
|
2024-09-12 09:07:18 -07:00
|
|
|
import path from 'path';
|
2024-09-26 11:28:57 -07:00
|
|
|
import picocolors from 'picocolors';
|
2024-09-12 09:07:18 -07:00
|
|
|
import { Container, Service } from 'typedi';
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-03 04:14:59 -08:00
|
|
|
import {
|
|
|
|
CUSTOM_API_CALL_KEY,
|
|
|
|
CUSTOM_API_CALL_NAME,
|
2023-02-10 05:59:20 -08:00
|
|
|
inTest,
|
2023-03-24 09:04:26 -07:00
|
|
|
CLI_DIR,
|
2023-08-31 07:40:20 -07:00
|
|
|
inE2ETests,
|
2023-02-03 04:14:59 -08:00
|
|
|
} from '@/constants';
|
2024-10-08 07:04:45 -07:00
|
|
|
import { isContainedWithin } from '@/utils/path-util';
|
2023-10-09 07:09:23 -07:00
|
|
|
|
|
|
|
interface LoadedNodesAndCredentials {
|
|
|
|
nodes: INodeTypeData;
|
|
|
|
credentials: ICredentialTypeData;
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
@Service()
|
2023-10-09 07:09:23 -07:00
|
|
|
export class LoadNodesAndCredentials {
|
|
|
|
private known: KnownNodesAndCredentials = { nodes: {}, credentials: {} };
|
2021-06-17 22:58:26 -07:00
|
|
|
|
2024-09-04 03:06:17 -07:00
|
|
|
// This contains the actually loaded objects, and their source paths
|
2022-11-23 07:20:28 -08:00
|
|
|
loaded: LoadedNodesAndCredentials = { nodes: {}, credentials: {} };
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2024-09-04 03:06:17 -07:00
|
|
|
// For nodes, this only contains the descriptions, loaded from either the
|
|
|
|
// actual file, or the lazy loaded json
|
2022-11-23 07:20:28 -08:00
|
|
|
types: Types = { nodes: [], credentials: [] };
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
loaders: Record<string, DirectoryLoader> = {};
|
|
|
|
|
2024-07-23 04:32:50 -07:00
|
|
|
excludeNodes = this.globalConfig.nodes.exclude;
|
2021-08-29 11:58:11 -07:00
|
|
|
|
2024-07-23 04:32:50 -07:00
|
|
|
includeNodes = this.globalConfig.nodes.include;
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
private postProcessors: Array<() => Promise<void>> = [];
|
|
|
|
|
2023-10-25 07:35:22 -07:00
|
|
|
constructor(
|
|
|
|
private readonly logger: Logger,
|
2024-12-11 06:36:17 -08:00
|
|
|
private readonly errorReporter: ErrorReporter,
|
2023-10-25 07:35:22 -07:00
|
|
|
private readonly instanceSettings: InstanceSettings,
|
2024-07-23 04:32:50 -07:00
|
|
|
private readonly globalConfig: GlobalConfig,
|
2023-10-25 07:35:22 -07:00
|
|
|
) {}
|
2023-10-23 04:39:35 -07:00
|
|
|
|
2019-08-08 11:38:25 -07:00
|
|
|
async init() {
|
2023-11-29 03:25:10 -08:00
|
|
|
if (inTest) throw new ApplicationError('Not available in tests');
|
2023-10-09 07:09:23 -07:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
// Make sure the imported modules can resolve dependencies fine.
|
2022-08-03 09:10:59 -07:00
|
|
|
const delimiter = process.platform === 'win32' ? ';' : ':';
|
|
|
|
process.env.NODE_PATH = module.paths.join(delimiter);
|
2022-11-23 07:20:28 -08:00
|
|
|
|
2022-07-20 07:24:03 -07:00
|
|
|
// @ts-ignore
|
2022-11-23 07:20:28 -08:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
2023-10-09 07:09:23 -07:00
|
|
|
module.constructor._initPaths();
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-08-31 07:40:20 -07:00
|
|
|
if (!inE2ETests) {
|
|
|
|
this.excludeNodes = this.excludeNodes ?? [];
|
|
|
|
this.excludeNodes.push('n8n-nodes-base.e2eTest');
|
|
|
|
}
|
|
|
|
|
2023-06-07 04:58:14 -07:00
|
|
|
// Load nodes from `n8n-nodes-base`
|
|
|
|
const basePathsToScan = [
|
2023-04-03 03:14:41 -07:00
|
|
|
// In case "n8n" package is in same node_modules folder.
|
|
|
|
path.join(CLI_DIR, '..'),
|
|
|
|
// In case "n8n" package is the root and the packages are
|
|
|
|
// in the "node_modules" folder underneath it.
|
|
|
|
path.join(CLI_DIR, 'node_modules'),
|
|
|
|
];
|
|
|
|
|
2023-06-07 04:58:14 -07:00
|
|
|
for (const nodeModulesDir of basePathsToScan) {
|
|
|
|
await this.loadNodesFromNodeModules(nodeModulesDir, 'n8n-nodes-base');
|
2023-11-29 03:13:55 -08:00
|
|
|
await this.loadNodesFromNodeModules(nodeModulesDir, '@n8n/n8n-nodes-langchain');
|
2023-04-03 03:14:41 -07:00
|
|
|
}
|
2023-03-24 09:04:26 -07:00
|
|
|
|
2023-06-07 04:58:14 -07:00
|
|
|
// Load nodes from any other `n8n-nodes-*` packages in the download directory
|
|
|
|
// This includes the community nodes
|
2023-10-23 04:39:35 -07:00
|
|
|
await this.loadNodesFromNodeModules(
|
|
|
|
path.join(this.instanceSettings.nodesDownloadDir, 'node_modules'),
|
|
|
|
);
|
2023-06-07 04:58:14 -07:00
|
|
|
|
2022-11-23 07:20:28 -08:00
|
|
|
await this.loadNodesFromCustomDirectories();
|
2023-02-08 09:57:43 -08:00
|
|
|
await this.postProcessLoaders();
|
2022-11-23 07:20:28 -08:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
addPostProcessor(fn: () => Promise<void>) {
|
|
|
|
this.postProcessors.push(fn);
|
|
|
|
}
|
2023-01-04 09:16:48 -08:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
isKnownNode(type: string) {
|
|
|
|
return type in this.known.nodes;
|
|
|
|
}
|
2023-01-04 09:16:48 -08:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
get loadedCredentials() {
|
|
|
|
return this.loaded.credentials;
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
get loadedNodes() {
|
|
|
|
return this.loaded.nodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
get knownCredentials() {
|
|
|
|
return this.known.credentials;
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
get knownNodes() {
|
|
|
|
return this.known.nodes;
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2023-06-07 04:58:14 -07:00
|
|
|
private async loadNodesFromNodeModules(
|
|
|
|
nodeModulesDir: string,
|
|
|
|
packageName?: string,
|
|
|
|
): Promise<void> {
|
2023-07-31 08:55:16 -07:00
|
|
|
const globOptions = {
|
2023-06-07 04:58:14 -07:00
|
|
|
cwd: nodeModulesDir,
|
|
|
|
onlyDirectories: true,
|
|
|
|
deep: 1,
|
2023-07-31 08:55:16 -07:00
|
|
|
};
|
|
|
|
const installedPackagePaths = packageName
|
|
|
|
? await glob(packageName, globOptions)
|
|
|
|
: [
|
|
|
|
...(await glob('n8n-nodes-*', globOptions)),
|
|
|
|
...(await glob('@*/n8n-nodes-*', { ...globOptions, deep: 2 })),
|
2024-03-26 06:22:57 -07:00
|
|
|
];
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-03-24 09:04:26 -07:00
|
|
|
for (const packagePath of installedPackagePaths) {
|
2022-07-20 07:24:03 -07:00
|
|
|
try {
|
2023-03-24 09:04:26 -07:00
|
|
|
await this.runDirectoryLoader(
|
|
|
|
LazyPackageDirectoryLoader,
|
|
|
|
path.join(nodeModulesDir, packagePath),
|
|
|
|
);
|
2022-11-04 09:34:47 -07:00
|
|
|
} catch (error) {
|
2024-09-26 11:28:57 -07:00
|
|
|
this.logger.error((error as Error).message);
|
2024-12-11 06:36:17 -08:00
|
|
|
this.errorReporter.error(error);
|
2022-11-04 09:34:47 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
resolveIcon(packageName: string, url: string): string | undefined {
|
|
|
|
const loader = this.loaders[packageName];
|
2024-10-08 07:04:45 -07:00
|
|
|
if (!loader) {
|
|
|
|
return undefined;
|
2023-10-09 07:09:23 -07:00
|
|
|
}
|
2024-10-08 07:04:45 -07:00
|
|
|
const pathPrefix = `/icons/${packageName}/`;
|
|
|
|
const filePath = path.resolve(loader.directory, url.substring(pathPrefix.length));
|
|
|
|
|
|
|
|
return isContainedWithin(loader.directory, filePath) ? filePath : undefined;
|
2023-10-09 07:09:23 -07:00
|
|
|
}
|
|
|
|
|
2023-01-05 04:28:40 -08:00
|
|
|
getCustomDirectories(): string[] {
|
2023-10-23 04:39:35 -07:00
|
|
|
const customDirectories = [this.instanceSettings.customExtensionDir];
|
2019-06-23 03:35:23 -07:00
|
|
|
|
|
|
|
if (process.env[CUSTOM_EXTENSION_ENV] !== undefined) {
|
2023-01-05 04:28:40 -08:00
|
|
|
const customExtensionFolders = process.env[CUSTOM_EXTENSION_ENV].split(';');
|
2022-09-09 09:08:08 -07:00
|
|
|
customDirectories.push(...customExtensionFolders);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
|
2023-01-05 04:28:40 -08:00
|
|
|
return customDirectories;
|
|
|
|
}
|
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
private async loadNodesFromCustomDirectories(): Promise<void> {
|
2023-01-05 04:28:40 -08:00
|
|
|
for (const directory of this.getCustomDirectories()) {
|
2022-11-23 07:20:28 -08:00
|
|
|
await this.runDirectoryLoader(CustomDirectoryLoader, directory);
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
async loadPackage(packageName: string) {
|
2023-10-23 04:39:35 -07:00
|
|
|
const finalNodeUnpackedPath = path.join(
|
|
|
|
this.instanceSettings.nodesDownloadDir,
|
|
|
|
'node_modules',
|
|
|
|
packageName,
|
|
|
|
);
|
2024-01-17 07:08:50 -08:00
|
|
|
return await this.runDirectoryLoader(PackageDirectoryLoader, finalNodeUnpackedPath);
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
async unloadPackage(packageName: string) {
|
2023-02-15 07:09:53 -08:00
|
|
|
if (packageName in this.loaders) {
|
|
|
|
this.loaders[packageName].reset();
|
|
|
|
delete this.loaders[packageName];
|
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
|
|
|
|
2023-02-03 04:14:59 -08:00
|
|
|
/**
|
|
|
|
* Whether any of the node's credential types may be used to
|
|
|
|
* make a request from a node other than itself.
|
|
|
|
*/
|
|
|
|
private supportsProxyAuth(description: INodeTypeDescription) {
|
|
|
|
if (!description.credentials) return false;
|
|
|
|
|
|
|
|
return description.credentials.some(({ name }) => {
|
|
|
|
const credType = this.types.credentials.find((t) => t.name === name);
|
|
|
|
if (!credType) {
|
2023-10-25 07:35:22 -07:00
|
|
|
this.logger.warn(
|
2023-02-03 04:14:59 -08:00
|
|
|
`Failed to load Custom API options for the node "${description.name}": Unknown credential name "${name}"`,
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (credType.authenticate !== undefined) return true;
|
|
|
|
|
|
|
|
return (
|
|
|
|
Array.isArray(credType.extends) &&
|
|
|
|
credType.extends.some((parentType) =>
|
|
|
|
['oAuth2Api', 'googleOAuth2Api', 'oAuth1Api'].includes(parentType),
|
|
|
|
)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Inject a `Custom API Call` option into `resource` and `operation`
|
|
|
|
* parameters in a latest-version node that supports proxy auth.
|
|
|
|
*/
|
|
|
|
private injectCustomApiCallOptions() {
|
|
|
|
this.types.nodes.forEach((node: INodeTypeDescription) => {
|
|
|
|
const isLatestVersion =
|
|
|
|
node.defaultVersion === undefined || node.defaultVersion === node.version;
|
|
|
|
|
|
|
|
if (isLatestVersion) {
|
|
|
|
if (!this.supportsProxyAuth(node)) return;
|
|
|
|
|
|
|
|
node.properties.forEach((p) => {
|
|
|
|
if (
|
|
|
|
['resource', 'operation'].includes(p.name) &&
|
|
|
|
Array.isArray(p.options) &&
|
|
|
|
p.options[p.options.length - 1].name !== CUSTOM_API_CALL_NAME
|
|
|
|
) {
|
|
|
|
p.options.push({
|
|
|
|
name: CUSTOM_API_CALL_NAME,
|
|
|
|
value: CUSTOM_API_CALL_KEY,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-06-17 22:58:26 -07:00
|
|
|
/**
|
2022-11-23 07:20:28 -08:00
|
|
|
* Run a loader of source files of nodes and credentials in a directory.
|
2021-06-17 22:58:26 -07:00
|
|
|
*/
|
2022-11-23 07:20:28 -08:00
|
|
|
private async runDirectoryLoader<T extends DirectoryLoader>(
|
2023-12-27 02:50:43 -08:00
|
|
|
constructor: Class<T, ConstructorParameters<typeof DirectoryLoader>>,
|
2022-11-23 07:20:28 -08:00
|
|
|
dir: string,
|
|
|
|
) {
|
|
|
|
const loader = new constructor(dir, this.excludeNodes, this.includeNodes);
|
2024-10-07 03:09:47 -07:00
|
|
|
if (loader instanceof PackageDirectoryLoader && loader.packageName in this.loaders) {
|
2024-09-26 11:28:57 -07:00
|
|
|
throw new ApplicationError(
|
|
|
|
picocolors.red(
|
|
|
|
`nodes package ${loader.packageName} is already loaded.\n Please delete this second copy at path ${dir}`,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2022-11-23 07:20:28 -08:00
|
|
|
await loader.loadAll();
|
2023-02-15 07:09:53 -08:00
|
|
|
this.loaders[loader.packageName] = loader;
|
2023-02-08 09:57:43 -08:00
|
|
|
return loader;
|
|
|
|
}
|
2022-11-23 07:20:28 -08:00
|
|
|
|
2024-09-04 03:06:17 -07:00
|
|
|
/**
|
|
|
|
* This creates all AI Agent tools by duplicating the node descriptions for
|
|
|
|
* all nodes that are marked as `usableAsTool`. It basically modifies the
|
|
|
|
* description. The actual wrapping happens in the langchain code for getting
|
|
|
|
* the connected tools.
|
|
|
|
*/
|
|
|
|
createAiTools() {
|
|
|
|
const usableNodes: Array<INodeTypeBaseDescription | INodeTypeDescription> =
|
|
|
|
this.types.nodes.filter((nodetype) => nodetype.usableAsTool === true);
|
|
|
|
|
|
|
|
for (const usableNode of usableNodes) {
|
|
|
|
const description: INodeTypeBaseDescription | INodeTypeDescription =
|
|
|
|
structuredClone(usableNode);
|
2024-12-12 04:54:44 -08:00
|
|
|
const wrapped = this.convertNodeToAiTool({ description }).description;
|
2024-09-04 03:06:17 -07:00
|
|
|
|
|
|
|
this.types.nodes.push(wrapped);
|
|
|
|
this.known.nodes[wrapped.name] = structuredClone(this.known.nodes[usableNode.name]);
|
|
|
|
|
|
|
|
const credentialNames = Object.entries(this.known.credentials)
|
|
|
|
.filter(([_, credential]) => credential?.supportedNodes?.includes(usableNode.name))
|
|
|
|
.map(([credentialName]) => credentialName);
|
|
|
|
|
|
|
|
credentialNames.forEach((name) =>
|
|
|
|
this.known.credentials[name]?.supportedNodes?.push(wrapped.name),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
async postProcessLoaders() {
|
2023-02-15 07:09:53 -08:00
|
|
|
this.known = { nodes: {}, credentials: {} };
|
|
|
|
this.loaded = { nodes: {}, credentials: {} };
|
|
|
|
this.types = { nodes: [], credentials: [] };
|
|
|
|
|
|
|
|
for (const loader of Object.values(this.loaders)) {
|
2023-02-08 09:57:43 -08:00
|
|
|
// list of node & credential types that will be sent to the frontend
|
2024-12-10 05:48:39 -08:00
|
|
|
const { known, types, directory, packageName } = loader;
|
|
|
|
this.types.nodes = this.types.nodes.concat(
|
|
|
|
types.nodes.map(({ name, ...rest }) => ({
|
|
|
|
...rest,
|
|
|
|
name: `${packageName}.${name}`,
|
|
|
|
})),
|
|
|
|
);
|
2023-02-08 09:57:43 -08:00
|
|
|
this.types.credentials = this.types.credentials.concat(types.credentials);
|
|
|
|
|
|
|
|
// Nodes and credentials that have been loaded immediately
|
|
|
|
for (const nodeTypeName in loader.nodeTypes) {
|
2024-12-10 05:48:39 -08:00
|
|
|
this.loaded.nodes[`${packageName}.${nodeTypeName}`] = loader.nodeTypes[nodeTypeName];
|
2023-02-08 09:57:43 -08:00
|
|
|
}
|
2022-07-20 07:24:03 -07:00
|
|
|
|
2023-02-08 09:57:43 -08:00
|
|
|
for (const credentialTypeName in loader.credentialTypes) {
|
|
|
|
this.loaded.credentials[credentialTypeName] = loader.credentialTypes[credentialTypeName];
|
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
|
2023-07-10 08:57:26 -07:00
|
|
|
for (const type in known.nodes) {
|
|
|
|
const { className, sourcePath } = known.nodes[type];
|
2024-12-10 05:48:39 -08:00
|
|
|
this.known.nodes[`${packageName}.${type}`] = {
|
2023-07-10 08:57:26 -07:00
|
|
|
className,
|
|
|
|
sourcePath: path.join(directory, sourcePath),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const type in known.credentials) {
|
|
|
|
const {
|
|
|
|
className,
|
|
|
|
sourcePath,
|
2023-11-13 03:11:16 -08:00
|
|
|
supportedNodes,
|
2023-07-10 08:57:26 -07:00
|
|
|
extends: extendsArr,
|
|
|
|
} = known.credentials[type];
|
|
|
|
this.known.credentials[type] = {
|
|
|
|
className,
|
|
|
|
sourcePath: path.join(directory, sourcePath),
|
2023-11-13 03:11:16 -08:00
|
|
|
supportedNodes:
|
2023-07-10 08:57:26 -07:00
|
|
|
loader instanceof PackageDirectoryLoader
|
2023-11-13 03:11:16 -08:00
|
|
|
? supportedNodes?.map((nodeName) => `${loader.packageName}.${nodeName}`)
|
2023-07-10 08:57:26 -07:00
|
|
|
: undefined,
|
|
|
|
extends: extendsArr,
|
|
|
|
};
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|
|
|
|
}
|
2023-10-09 07:09:23 -07:00
|
|
|
|
2024-09-04 03:06:17 -07:00
|
|
|
this.createAiTools();
|
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
this.injectCustomApiCallOptions();
|
|
|
|
|
|
|
|
for (const postProcessor of this.postProcessors) {
|
|
|
|
await postProcessor();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-10 05:48:39 -08:00
|
|
|
getNode(fullNodeType: string): LoadedClass<INodeType | IVersionedNodeType> {
|
|
|
|
const [packageName, nodeType] = fullNodeType.split('.');
|
|
|
|
const { loaders } = this;
|
|
|
|
const loader = loaders[packageName];
|
|
|
|
if (!loader) {
|
|
|
|
throw new UnrecognizedNodeTypeError(packageName, nodeType);
|
|
|
|
}
|
|
|
|
return loader.getNode(nodeType);
|
|
|
|
}
|
|
|
|
|
|
|
|
getCredential(credentialType: string): LoadedClass<ICredentialType> {
|
|
|
|
const { loadedCredentials } = this;
|
|
|
|
|
|
|
|
for (const loader of Object.values(this.loaders)) {
|
|
|
|
if (credentialType in loader.known.credentials) {
|
|
|
|
const loaded = loader.getCredential(credentialType);
|
|
|
|
loadedCredentials[credentialType] = loaded;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (credentialType in loadedCredentials) {
|
|
|
|
return loadedCredentials[credentialType];
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new UnrecognizedCredentialTypeError(credentialType);
|
|
|
|
}
|
|
|
|
|
2024-12-12 04:54:44 -08:00
|
|
|
/**
|
|
|
|
* Modifies the description of the passed in object, such that it can be used
|
|
|
|
* as an AI Agent Tool.
|
|
|
|
* Returns the modified item (not copied)
|
|
|
|
*/
|
|
|
|
convertNodeToAiTool<
|
|
|
|
T extends object & { description: INodeTypeDescription | INodeTypeBaseDescription },
|
|
|
|
>(item: T): T {
|
|
|
|
// quick helper function for type-guard down below
|
|
|
|
function isFullDescription(obj: unknown): obj is INodeTypeDescription {
|
|
|
|
return typeof obj === 'object' && obj !== null && 'properties' in obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isFullDescription(item.description)) {
|
|
|
|
item.description.name += 'Tool';
|
|
|
|
item.description.inputs = [];
|
|
|
|
item.description.outputs = [NodeConnectionType.AiTool];
|
|
|
|
item.description.displayName += ' Tool';
|
|
|
|
delete item.description.usableAsTool;
|
|
|
|
|
|
|
|
const hasResource = item.description.properties.some((prop) => prop.name === 'resource');
|
|
|
|
const hasOperation = item.description.properties.some((prop) => prop.name === 'operation');
|
|
|
|
|
|
|
|
if (!item.description.properties.map((prop) => prop.name).includes('toolDescription')) {
|
|
|
|
const descriptionType: INodeProperties = {
|
|
|
|
displayName: 'Tool Description',
|
|
|
|
name: 'descriptionType',
|
|
|
|
type: 'options',
|
|
|
|
noDataExpression: true,
|
|
|
|
options: [
|
|
|
|
{
|
|
|
|
name: 'Set Automatically',
|
|
|
|
value: 'auto',
|
|
|
|
description: 'Automatically set based on resource and operation',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'Set Manually',
|
|
|
|
value: 'manual',
|
|
|
|
description: 'Manually set the description',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
default: 'auto',
|
|
|
|
};
|
|
|
|
|
|
|
|
const descProp: INodeProperties = {
|
|
|
|
displayName: 'Description',
|
|
|
|
name: 'toolDescription',
|
|
|
|
type: 'string',
|
|
|
|
default: item.description.description,
|
|
|
|
required: true,
|
|
|
|
typeOptions: { rows: 2 },
|
|
|
|
description:
|
|
|
|
'Explain to the LLM what this tool does, a good, specific description would allow LLMs to produce expected results much more often',
|
|
|
|
placeholder: `e.g. ${item.description.description}`,
|
|
|
|
};
|
|
|
|
|
|
|
|
const noticeProp: INodeProperties = {
|
|
|
|
displayName:
|
|
|
|
"Use the expression {{ $fromAI('placeholder_name') }} for any data to be filled by the model",
|
|
|
|
name: 'notice',
|
|
|
|
type: 'notice',
|
|
|
|
default: '',
|
|
|
|
};
|
|
|
|
|
|
|
|
item.description.properties.unshift(descProp);
|
|
|
|
|
|
|
|
// If node has resource or operation we can determine pre-populate tool description based on it
|
|
|
|
// so we add the descriptionType property as the first property
|
|
|
|
if (hasResource || hasOperation) {
|
|
|
|
item.description.properties.unshift(descriptionType);
|
|
|
|
|
|
|
|
descProp.displayOptions = {
|
|
|
|
show: {
|
|
|
|
descriptionType: ['manual'],
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
item.description.properties.unshift(noticeProp);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const resources = item.description.codex?.resources ?? {};
|
|
|
|
|
|
|
|
item.description.codex = {
|
|
|
|
categories: ['AI'],
|
|
|
|
subcategories: {
|
|
|
|
AI: ['Tools'],
|
|
|
|
Tools: ['Other Tools'],
|
|
|
|
},
|
|
|
|
resources,
|
|
|
|
};
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
async setupHotReload() {
|
|
|
|
const { default: debounce } = await import('lodash/debounce');
|
|
|
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
|
|
const { watch } = await import('chokidar');
|
2023-10-27 05:15:02 -07:00
|
|
|
|
2023-10-09 07:09:23 -07:00
|
|
|
const { Push } = await import('@/push');
|
|
|
|
const push = Container.get(Push);
|
|
|
|
|
|
|
|
Object.values(this.loaders).forEach(async (loader) => {
|
|
|
|
try {
|
|
|
|
await fsPromises.access(loader.directory);
|
|
|
|
} catch {
|
|
|
|
// If directory doesn't exist, there is nothing to watch
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const realModulePath = path.join(await fsPromises.realpath(loader.directory), path.sep);
|
|
|
|
const reloader = debounce(async () => {
|
|
|
|
const modulesToUnload = Object.keys(require.cache).filter((filePath) =>
|
|
|
|
filePath.startsWith(realModulePath),
|
|
|
|
);
|
|
|
|
modulesToUnload.forEach((filePath) => {
|
|
|
|
delete require.cache[filePath];
|
|
|
|
});
|
|
|
|
|
|
|
|
loader.reset();
|
|
|
|
await loader.loadAll();
|
|
|
|
await this.postProcessLoaders();
|
2024-12-20 10:45:04 -08:00
|
|
|
push.broadcast({ type: 'nodeDescriptionUpdated', data: {} });
|
2023-10-09 07:09:23 -07:00
|
|
|
}, 100);
|
|
|
|
|
|
|
|
const toWatch = loader.isLazyLoaded
|
|
|
|
? ['**/nodes.json', '**/credentials.json']
|
|
|
|
: ['**/*.js', '**/*.json'];
|
2024-11-08 07:54:06 -08:00
|
|
|
const files = await glob(toWatch, {
|
|
|
|
cwd: realModulePath,
|
|
|
|
ignore: ['node_modules/**'],
|
|
|
|
});
|
|
|
|
const watcher = watch(files, {
|
|
|
|
cwd: realModulePath,
|
|
|
|
ignoreInitial: true,
|
|
|
|
});
|
|
|
|
watcher.on('add', reloader).on('change', reloader).on('unlink', reloader);
|
2023-10-09 07:09:23 -07:00
|
|
|
});
|
2022-07-20 07:24:03 -07:00
|
|
|
}
|
2019-06-23 03:35:23 -07:00
|
|
|
}
|