跳至主要内容

创建插件

简介

Rivet插件是用JavaScript或TypeScript编写的。它们发布到NPM并安装到Rivet项目中。

Rivet插件有两个主要要求:

  1. 插件和节点定义必须是纯的、同构的JavaScript或TypeScript。这意味着你不能导入任何非同构的包,不能导入任何无法打包成单个文件的包,也不能导入任何Node.js包。
  2. 你不能将Rivet本身导入到插件中。你必须导出一个以Rivet库作为唯一参数的函数。 由于插件代码是动态导入的,它必须被打包成单个文件。因此,你不能使用任何无法打包到单个文件中的import语句。你不能导入Rivet,因为这会与已在Rivet中运行的Rivet安装产生冲突。因此,Rivet库是作为参数传入的。你的插件必须只导出一个以Rivet库作为唯一参数的函数。

示例/模板项目

有两个示例项目可以作为您插件的起点:

  • rivet-plugin-example - 这是一个纯TypeScript插件的示例,不使用任何Node.js代码。假设您不需要使用Node.js代码,这是推荐的入门起点。
  • rivet-plugin-example-python-exec - 如果需要在插件中运行Node.js代码(因此您的插件只能与Node执行器配合使用),请以此作为起点。该插件完整展示了如何编写使用Node.js代码的插件。

详细编写插件

重要说明

  • 您必须将插件打包,或将插件的所有代码包含在ESM文件中。插件是通过import(pluginUrl)加载的,因此必须遵循ESM模块的所有规则。这意味着您不能在插件代码中使用requiremodule.exports。如果需要使用外部库,必须将它们打包。唯一的例外是当您对插件进行双重打包时,以分离node.js和同构代码(如下所述)。建议使用ESBuild来打包您的插件。
  • 您不能在插件中导入或打包@ironclad/rivet-core@ironclad/rivet-node。rivet核心库会作为参数传递到您的默认导出函数中。请注意仅对核心库使用import type语句,否则您的插件将无法成功打包。
  • 如果您正在开发一个node.js插件,重要的是将插件分成两个独立的包 - 一个同构包(定义插件和所有节点)和一个仅包含节点实现的Node专用包。同构包允许动态导入node包,但不能静态导入它(当然类型除外)。

插件定义

您的主插件定义是插件的入口点。它必须导出一个函数,该函数以Rivet核心库作为唯一参数。当插件加载时,会调用此函数,并将Rivet核心库作为参数传入。

该函数的类型称为RivetPluginInitializer,大致定义如下:

import * as Rivet from '@ironclad/rivet-core';
export type RivetPluginInitializer = (rivet: typeof Rivet) => RivetPlugin;

它必须返回一个有效的RivetPlugin实例。RivetPlugin定义如下:

export type RivetPlugin = {
/** The unique identifier of the plugin. Should be unique across all plugins. */
id: string;

/** The display name of the plugin - what is shown in the UI fr the plugin. */
name?: string;

/** Registers new nodes for the plugin to add. */
register?: (register: <T extends ChartNode>(definition: PluginNodeDefinition<T>) => void) => void;

/** The available configuration items and their specification, for configuring a plugin in the UI. */
configSpec?: RivetPluginConfigSpecs;

/** Defines additional context menu groups that the plugin adds. */
contextMenuGroups?: Array<{
id: string;
label: string;
}>;
};
  • id 是必填项。这必须是您插件的唯一标识符(在所有插件中)
  • name 是可选的但建议填写 - 这是在Rivet UI中显示时您的插件的展示名称。
  • register 是您为插件注册新节点的地方。下文将对此进行更详细的说明。
  • configSpec 是您定义插件配置项的地方。下文将对此进行更详细的说明。
  • contextMenuGroups 允许您在Rivet的右键菜单中添加额外的上下文菜单组。

以下是最简单的TypeScript插件定义:

import type { RivetPluginInitializer } from '@ironclad/rivet-core';

const plugin: RivetPluginInitializer = (rivet) => ({
id: 'my-plugin',
name: 'My Plugin',
});

export default plugin;

以下是最简单的JavaScript插件定义:

const plugin = (rivet) => ({
id: 'my-plugin',
name: 'My Plugin',
});

export default plugin;

注册节点

节点还必须遵循一个规则,即它们必须导出一个函数,该函数以Rivet库作为其唯一参数。要创建节点的实例,您需要在插件初始化函数内部调用此函数。例如,在TypeScript中:

import type { RivetPlugin, RivetPluginInitializer } from '@ironclad/rivet-core';
import myNode from './nodes/myNode';

const plugin: RivetPluginInitializer = (rivet) => {
const myPlugin: RivetPlugin = {
id: 'my-plugin',
name: 'My Plugin',
register: (register) => {
register(myNode(rivet));
},
};

return myPlugin;
};

在JavaScript中:

import myNode from './nodes/myNode';

const plugin = (rivet) => ({
id: 'my-plugin',
name: 'My Plugin',
register: (register) => {
register(myNode(rivet));
},
});

节点定义

节点定义是一个函数,它以Rivet库作为唯一参数,并返回一个PluginNodeDefinition对象。该对象定义如下:

export type PluginNodeDefinition<T extends ChartNode> = {
impl: PluginNodeImpl<T>;
displayName: string;
};

export interface PluginNodeImpl<T extends ChartNode> {
getInputDefinitions(
data: T['data'],
connections: NodeConnection[],
nodes: Record<NodeId, ChartNode>,
project: Project,
): NodeInputDefinition[];

getOutputDefinitions(
data: T['data'],
connections: NodeConnection[],
nodes: Record<NodeId, ChartNode>,
project: Project,
): NodeOutputDefinition[];

process(data: T['data'], inputData: Inputs, context: InternalProcessContext): Promise<Outputs>;

getEditors(data: T['data'], context: RivetUIContext): EditorDefinition<T>[] | Promise<EditorDefinition<T>[]>;

getBody(data: T['data'], context: RivetUIContext): NodeBody | Promise<NodeBody>;

create(): T;

getUIData(context: RivetUIContext): NodeUIData | Promise<NodeUIData>;
}

可以使用pluginNodeDefinition函数创建一个有效的插件节点定义。例如:

import type { Rivet } from '@ironclad/rivet-core';

export function myExamplePlugin(rivet: typeof Rivet) {
return rivet.pluginNodeDefinition({
displayName: 'My Example Plugin',
impl: {
getInputDefinitions: () => [],
getOutputDefinitions: () => [],
process: async () => ({}),
getEditors: () => [],
getBody: () => ({ type: 'text', text: 'Hello World' }),
create: () => ({ data: {} }),
getUIData: () => ({}),
},
});
}

以下节点实现对象取自rivet-plugin-example项目。这应作为您创建新节点的起点:

// **** IMPORTANT ****
// Make sure you do `import type` and do not pull in the entire Rivet core library here.
// Export a function that takes in a Rivet object, and you can access rivet library functionality
// from there.
import type {
ChartNode,
EditorDefinition,
Inputs,
InternalProcessContext,
NodeBodySpec,
NodeConnection,
NodeId,
NodeInputDefinition,
NodeOutputDefinition,
NodeUIData,
Outputs,
PluginNodeImpl,
PortId,
Project,
Rivet,
} from '@ironclad/rivet-core';

// This defines your new type of node.
export type ExamplePluginNode = ChartNode<'examplePlugin', ExamplePluginNodeData>;

// This defines the data that your new node will store.
export type ExamplePluginNodeData = {
someData: string;

// It is a good idea to include useXInput fields for any inputs you have, so that
// the user can toggle whether or not to use an import port for them.
useSomeDataInput?: boolean;
};

// Make sure you export functions that take in the Rivet library, so that you do not
// import the entire Rivet core library in your plugin.
export function examplePluginNode(rivet: typeof Rivet) {
// This is your main node implementation. It is an object that implements the PluginNodeImpl interface.
const ExamplePluginNodeImpl: PluginNodeImpl<ExamplePluginNode> = {
// This should create a new instance of your node type from scratch.
create(): ExamplePluginNode {
const node: ExamplePluginNode = {
// Use rivet.newId to generate new IDs for your nodes.
id: rivet.newId<NodeId>(),

// This is the default data that your node will store
data: {
someData: 'Hello World',
},

// This is the default title of your node.
title: 'Example Plugin Node',

// This must match the type of your node.
type: 'examplePlugin',

// X and Y should be set to 0. Width should be set to a reasonable number so there is no overflow.
visualData: {
x: 0,
y: 0,
width: 200,
},
};
return node;
},

// This function should return all input ports for your node, given its data, connections, all other nodes, and the project. The
// connection, nodes, and project are for advanced use-cases and can usually be ignored.
getInputDefinitions(
data: ExamplePluginNodeData,
_connections: NodeConnection[],
_nodes: Record<NodeId, ChartNode>,
_project: Project,
): NodeInputDefinition[] {
const inputs: NodeInputDefinition[] = [];

if (data.useSomeDataInput) {
inputs.push({
id: 'someData' as PortId,
dataType: 'string',
title: 'Some Data',
});
}

return inputs;
},

// This function should return all output ports for your node, given its data, connections, all other nodes, and the project. The
// connection, nodes, and project are for advanced use-cases and can usually be ignored.
getOutputDefinitions(
_data: ExamplePluginNodeData,
_connections: NodeConnection[],
_nodes: Record<NodeId, ChartNode>,
_project: Project,
): NodeOutputDefinition[] {
return [
{
id: 'someData' as PortId,
dataType: 'string',
title: 'Some Data',
},
];
},

// This returns UI information for your node, such as how it appears in the context menu.
getUIData(): NodeUIData {
return {
contextMenuTitle: 'Example Plugin',
group: 'Example',
infoBoxBody: 'This is an example plugin node.',
infoBoxTitle: 'Example Plugin Node',
};
},

// This function defines all editors that appear when you edit your node.
getEditors(_data: ExamplePluginNodeData): EditorDefinition<ExamplePluginNode>[] {
return [
{
type: 'string',
dataKey: 'someData',
useInputToggleDataKey: 'useSomeDataInput',
label: 'Some Data',
},
];
},

// This function returns the body of the node when it is rendered on the graph. You should show
// what the current data of the node is in some way that is useful at a glance.
getBody(data: ExamplePluginNodeData): string | NodeBodySpec | NodeBodySpec[] | undefined {
return rivet.dedent`
Example Plugin Node
Data: ${data.useSomeDataInput ? '(Using Input)' : data.someData}
`;
},

// This is the main processing function for your node. It can do whatever you like, but it must return
// a valid Outputs object, which is a map of port IDs to DataValue objects. The return value of this function
// must also correspond to the output definitions you defined in the getOutputDefinitions function.
async process(data: ExamplePluginNodeData, inputData: Inputs, _context: InternalProcessContext): Promise<Outputs> {
const someData = rivet.getInputOrData(data, inputData, 'someData', 'string');

return {
['someData' as PortId]: {
type: 'string',
value: someData,
},
};
},
};

// Once a node is defined, you must pass it to rivet.pluginNodeDefinition, which will return a valid
// PluginNodeDefinition object.
const examplePluginNode = rivet.pluginNodeDefinition(ExamplePluginNodeImpl, 'Example Plugin Node');

// This definition should then be used in the `register` function of your plugin definition.
return examplePluginNode;
}

再次强调,节点定义必须导出一个以Rivet作为唯一参数的函数,这样才能将Rivet库动态注入到插件中。

Node.js 代码

查看代码和自述文件在[rivet-plugin-example-python-exec](https://github.com/abrenneke/rivet-plugin-example-python-exec]项目中,了解如何编写一个node.js插件。

配置

插件可以定义配置设置,以便您可以在Rivet的设置菜单中配置插件。在这里,您可以配置诸如API密钥或其他特定于您插件的全局设置等内容。

要定义配置设置,您必须在插件定义中定义一个configSpec对象。该对象的定义如下:

export type RivetPluginConfigSpecs = Record<string, PluginConfigurationSpec>;

export type PluginConfigurationSpecBase<T> = {
/** The type of the config value, how it should show as an editor in the UI. */
type: string;

/** The default value of the config item if unset. */
default?: T;

/** A description to show in the UI for the config setting. */
description?: string;

/** The label of the setting in the UI. */
label: string;
};

export type StringPluginConfigurationSpec = {
type: 'string';
default?: string;
label: string;
description?: string;
pullEnvironmentVariable?: true | string;
helperText?: string;
};

export type SecretPluginConfigurationSpec = {
type: 'secret';
default?: string;
label: string;
description?: string;
pullEnvironmentVariable?: true | string;
helperText?: string;
};

export type PluginConfigurationSpec =
| StringPluginConfigurationSpec
| SecretPluginConfigurationSpec
| PluginConfigurationSpecBase<number>
| PluginConfigurationSpecBase<boolean>;

您的configSpec对象的键是配置项的名称。值则是定义配置项的对象。以下是一个带有配置项的插件定义示例:

import type { RivetPluginInitializer, RivetPlugin } from '@ironclad/rivet-core';

const plugin: RivetPluginInitializer = (rivet) => {
const myPlugin: RivetPlugin = {
id: 'my-plugin',
name: 'My Plugin',
configSpec: {
apiKey: {
type: 'secret',
label: 'API Key',
description: 'The API key to use for this plugin',
},
someSetting: {
type: 'string',
label: 'Some Setting',
description: 'Some setting for this plugin',
},
someOtherSetting: {
type: 'number',
label: 'Some Other Setting',
description: 'Some other setting for this plugin',
},
someBooleanSetting: {
type: 'boolean',
label: 'Some Boolean Setting',
description: 'Some boolean setting for this plugin',
},
},
};

return myPlugin;
};

读取配置项

节点process方法的第三个参数是InternalProcessContext。该对象包含一个getPluginConfig方法,可用于读取插件的配置项。例如:

import type { RivetPluginInitializer, RivetPlugin } from '@ironclad/rivet-core';

const plugin: RivetPluginInitializer = (rivet) => {
const myPlugin: RivetPlugin = {
id: 'my-plugin',
name: 'My Plugin',
configSpec: {
apiKey: {
type: 'secret',
label: 'API Key',
description: 'The API key to use for this plugin',
},
},
register: (register) => {
register(
rivet.pluginNodeDefinition({
displayName: 'My Example Plugin',
impl: {
...etc,
process: async (_data, _inputData, context) => {
const apiKey = context.getPluginConfig('apiKey');
// Do something with the API key
},
},
}),
);
},
};

return myPlugin;
};

开发插件

开发插件的推荐方式是在Rivet的plugins目录内创建您的代码仓库。这样,当您重新构建插件时,只需重启Rivet即可看到变更生效。

tip

您的rivet插件目录显示在Rivet的插件覆盖层底部,可通过屏幕顶部的Plugins选项卡访问。

要做到这一点,

  1. 在Rivet的插件目录中创建一个名为-latest的目录。

  2. 将您的代码仓库克隆到当前目录下名为package的文件夹中。例如,git clone package。您也可以创建一个package目录,然后将您的代码仓库复制到其中。

  3. 您的最终路径应包含plugins/-latest/package/.git。包含.git文件夹很重要,因为这是Rivet识别您的插件为本地安装的方式。

  4. 在rivet中使用Add NPM Plugin功能,并传入作为包名。这将从本地目录安装您的插件。

  5. 如果您正在使用示例插件,可以在package文件夹中运行yarn dev来自动监视更改并重新构建您的插件。然后,您只需在每次更改后重启Rivet,就能在Rivet中看到您的更改。

    注意:如果您使用本文档前面链接的任一入门插件仓库,则无需手动创建这些目录。这些仓库会自动为您创建适当的目录。

更多帮助

如需更多帮助,请加入Rivet Discord,我们很乐意协助您的插件开发!