创建插件
简介
Rivet插件是用JavaScript或TypeScript编写的。它们发布到NPM并安装到Rivet项目中。
Rivet插件有两个主要要求:
- 插件和节点定义必须是纯的、同构的JavaScript或TypeScript。这意味着你不能导入任何非同构的包,不能导入任何无法打包成单个文件的包,也不能导入任何Node.js包。
- 你不能将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模块的所有规则。这意味着您不能在插件代码中使用require或module.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即可看到变更生效。
您的rivet插件目录显示在Rivet的插件覆盖层底部,可通过屏幕顶部的Plugins选项卡访问。
要做到这一点,
在Rivet的插件目录中创建一个名为
的目录。-latest 将您的代码仓库克隆到当前目录下名为
package的文件夹中。例如,git clone。您也可以创建一个package package目录,然后将您的代码仓库复制到其中。您的最终路径应包含
plugins/。包含-latest/package/.git .git文件夹很重要,因为这是Rivet识别您的插件为本地安装的方式。在rivet中使用
Add NPM Plugin功能,并传入作为包名。这将从本地目录安装您的插件。如果您正在使用示例插件,可以在
package文件夹中运行yarn dev来自动监视更改并重新构建您的插件。然后,您只需在每次更改后重启Rivet,就能在Rivet中看到您的更改。注意:如果您使用本文档前面链接的任一入门插件仓库,则无需手动创建这些目录。这些仓库会自动为您创建适当的目录。
更多帮助
如需更多帮助,请加入Rivet Discord,我们很乐意协助您的插件开发!