fyflow-插件-sdk
类型和 definePlugin 写作 插件 对于fyflow-app, visual builder和Host为 fyflow-编排 流。插件提供节点类型(与运行其任务的工人),门类型,资源组类型和资源组。
npm install --save-peer fyflow-plugin-sdk
npm install --save-dev fyflow-orchestration # its types, for type checking; the Host brings its own at run time
合同
插件是一个模块,其默认导出是一个普通对象。 definePlugin 返回参数不变;它只提供类型检查。依赖SDK作为对等体,不要捆绑它:主机在运行时提供它(见下文),并且您自己的副本只会报告它自己的 SDK_VERSION而不是宿主的。
import { definePlugin } from "fyflow-plugin-sdk";
export default definePlugin({
id: "acme.signer", // stable and unique across Plugins
version: "1.0.0",
base: import.meta.url, // what Worker `url`s resolve against
// Set on the Host's Profiles, once for every Node Type that takes them (see Profiles and secrets).
profile: {
schema: {
type: "object",
properties: { key: { type: "string", writeOnly: true } }, // a secret
},
startup: ["key"], // read once, when a Worker starts
},
workers: {
sign: {
url: "./sign.worker.js",
settingsSchema: { type: "object", properties: { field: { type: "string" } } },
},
},
nodeTypes: {
Sign: {
worker: "sign",
inline: true,
profile: ["key"], // the fields of `profile` its Workers get
groups: ["acme.signing"],
icon: "signature", // from the app's icon set, below
description: "Signs a field with HMAC.",
},
},
groups: { "acme.signing": { type: "ConcurrentLimitGroup", config: { limit: 2 } } },
gateTypes: {
threshold: {
routes: ["below", "at-or-above"],
defaultRoute: "below",
fn: ({ key, settings }) => (Number(key) >= Number(settings.at) ? "at-or-above" : "below"),
},
},
});
| 场 | 它是什么? |
|---|---|
profile | 插件的Profile字段,从0.5.0开始:一个对象 schema, defaults 对于没有配置文件集的字段, startup 它的worker在启动时读取的字段,并且只有Profile可以设置每个任务字段(profileOnly). 看到 配置文件和秘密。 |
workers | 工作模块按键。Worker模块的默认导出是一个类:它的构造函数获取Node Type接受的启动字段,以及 run({ settings, input }, context) 执行一项任务 (请参见 工人)。一名工人宣布不 configSchema 从0.5.0开始:它的配置是插件的 profile。模式是fyflow子集中的JSON模式。一个工人的 outputSchema,什么 run 返回(被每个版本的SDK接受,作为fyflow自己的Worker条目)是一个人填写的表单,用于标记失败的任务完成,以及编辑器在任何运行之前为其节点边缘和门的条件建议的路径。给 inputSchema 和 outputSchema 每个字段的描述和一个 examples list:一个节点的面板显示了它的Worker接收和返回的内容,并检查到达节点的内容与接收的内容。在任何运行之前,首先运行 outputSchema的 examples 是尝试并检查下一个节点的转换的示例; 如果没有一个,编辑器将从模式中生成更粗略的值。 |
nodeTypes | 节点类型的名称,每个命名这个插件的一个 workers,使用fyflow的声明字段(inline, pool, settings 违约, retryPolicy, groups;否 config 自0.5.0起),自0.2.0起应用程序如何显示它 (可选 icon 一行字 description),并且自0.5.0起 profile 它占用的域。 |
dataDir | true 从0.6.0开始,要求主机提供插件自己的数据目录。见 插件的数据目录。 |
status | 一个函数说插件是如何的,显示在插件页面上的卡片上,自0.6.0以来。见 插件的状态。 |
gateTypes | 按名称的门类型: 它们的路由、默认路由、设置架构和决策 fn。 |
groupTypes | 按名称划分资源组类型:a create(config, key) 用于调度程序限制的工厂,以及 configSchema。 |
groups | fyflow内置类型的按键资源组(ConcurrentLimitGroup, RateLimitGroup, KeyedRateLimitGroup) 或这个插件自己的。节点类型可以使用其自己的插件的组,并且内置插件的 disk, cpu 和 gpu 组(下图)。 |
不要将设置命名为每个JavaScript对象的成员 (constructor, toString, valueOf, hasOwnProperty, __proto__). 主机将流定义的形状交给代理,作为一个JSON模式,其中包括您的 settingsSchema,并且一些JSON模式验证器误读了这样的属性: 他们拒绝或抛出有效的流。
插件提供的每个名称(节点类型,门类型,资源组类型,资源组)必须在主机加载的插件中是唯一的:主机首先看到的插件保留一个名称,之后重复的插件将被跳过。
插件目录
主机从它的插件目录(<dataDir>/plugins 除非 pluginDir, FYFLOW_PLUGIN_DIR 或 --plugin-dir 否则):
- 一个 文件夹 持有
plugin.ts或plugin.js,或包裹(package.json及其条目),因为主机布局了从应用程序安装的插件; - 一个 包 命名
fyflow-plugin-*或@scope/fyflow-plugin-*在目录的node_modules,用npm安装。一个包必须附带JavaScript: Node不会剥离TypeScriptnode_modules。
插件自己的导入从插件目录的 node_modules,所以 npm install 您在那里的其他依赖项(或捆绑它们)。SDK是主机提供的:在服务器上, npm install fyflow-plugin-sdk 在那里 (npm也安装它作为你的包的同行); 桌面应用程序把它自己在那里。更改在主机被告知时生效 重新加载。
对等范围
将插件使用的SDK版本声明为对等依赖:
{ "peerDependencies": { "fyflow-plugin-sdk": "^0.5.0" } }
主机实现其SDK版本(SDK_VERSION,在此导出)以及与之兼容的早期版本(SDK_COMPATIBLE)。它跳过,原因是,一个插件的范围不包括它们。0.5.0更改了配置文件,因此它与早期版本不兼容: 跳过为0.4或之前编写的插件,直到升级为止,因为 UPGRADING.md 描述.0.6.0和0.7.0仅添加到0.5.0,因此0.7上的主机加载一个声明插件 ^0.5.0 或 ^0.6.0. 从0.5开始,只对主机在运行时必须做的事情引发范围:请求数据目录的插件说 ^0.6.0,因为0.5上的主机会加载它并给它none。
图标
节点类型的 icon 命名应用程序的图标之一,从 Lucide: activity, archive, bell, bot, box, braces, brain, calculator, calendar, camera, chart-bar, check, clock, cloud, code, cpu, credit-card, database, download, file, file-text, filter, flag, folder, git-branch, globe, hash, image, key, link, list, lock, mail, map-pin, message-square, package, pen-tool, play, plug, repeat, search, send, server, settings, shield, shopping-cart, signature, sparkles, table, tag, terminal, timer, trash, upload, user, users, wand-sparkles, webhook, zap。集合之外的名称不是错误: 节点类型显示其种类的图标,插件页面显示警告。
工人
Worker模块的默认导出是一个类。主机用它的Node Type从配置文件中获取的启动字段来构建每个实例(池的每个worker:一个线程,或者在内联池中使用主机自己线程上的对象),并跨任务重用它,因此请将每个任务状态从字段中删除。 run 获取任务的信封(节点的信封) settings 和Task输入)和上下文;它返回的是Task的结果,而throw会带着错误消息使Task失败。自0.3.0以来,SDK类型的所有:
import type { TaskEnvelope, Worker, WorkerContext } from "fyflow-plugin-sdk";
type Config = { key?: string };
type Settings = { field?: string };
export default class Sign implements Worker<Settings> {
readonly #config: Config;
constructor(config: Config) {
this.#config = config;
}
async run({ settings, input }: TaskEnvelope<Settings>, context: WorkerContext) {
if (this.#config.key === undefined) throw new Error("No key: set `key` on this Node's Profile");
context.sendProgress(0, "signing"); // a fraction from 0 to 1, not a percentage
context.signal.throwIfAborted(); // aborted when the Task is Cancelled
const value = settings.field === undefined ? input : (input as Record<string, unknown>)[settings.field];
return { signed: await hmac(this.#config.key, value) }; // hmac: your own signing function
}
}
WorkerClass<Config, Settings, Input, Output> 类型类本身,包括构造函数。检查 context.signal 步骤之间,或将其传递给任何需要一个 (fetch,streams): 立即停止的Worker会更快地释放其插槽,并且在取消后返回的内容将被丢弃。使用 import type因此,Worker模块在运行时不需要来自SDK的任何东西。上下文提供了Host显示的内容(进度)和Cancel;fyflow的调度程序传递更多信息,但SDK只承诺传递这些信息。
工人的生命周期
从0.5.0开始,SDK还承诺了任务之间的实例发生的情况,对于拥有长期存在的东西(子进程,连接,加载的模型)的Worker:
- 建造的:
new Worker(config, pool),在那里config它的创业领域是什么pool一个PoolContext。 setup(),如果它有一个: 一次,在它的第一个任务之前等待。在这里启动它拥有的东西,而不是在构造函数中。run,用于任意数量的任务。teardown(),如果有的话:一次,当池退出实例时。那是当它已经闲置过了它的池子idleTimeout(池每5秒检查一次,所以最多5秒后),当它要求被替换时,以及当它的运行时关闭时: 主机停止,或者在一次重新加载后,一旦被替换的运行时上一次运行结束。
import type { PoolContext, Worker } from "fyflow-plugin-sdk";
export default class Thumbnails implements Worker {
readonly #pool: PoolContext | undefined;
#service: Service | undefined; // Service, start: your own
constructor(_config: object, pool?: PoolContext) {
this.#pool = pool;
}
async setup() {
// Port 0: another copy of this Worker may be running (see below).
this.#service = await start({ port: 0, onExit: (error) => this.#pool?.terminateWithError(error, { canRestart: true }) });
}
async teardown() {
await this.#service?.stop(); // undefined when setup() did not get that far
}
async run() {
return this.#service?.thumbnail();
}
}
依靠什么,不依靠什么:
- 一个
setup(),抛出 每个给定实例的Task都失败,包括它的消息和它的teardown()立即运行。它不会在该实例上再次尝试: 只有在重新加载时退役这一池时,或者一旦没有任务达到它的idleTimeout这是源源不断的任务所不允许的。所以一个setup()这可能会因为一个传递的原因(端口被占用,服务仍在启动)而失败,应该在抛出之前在自身内部重试teardown()必须应付一个setup()那永远不会结束。 pool.terminateWithError(error, { canRestart: true })要求池替换实例,说它拥有的进程死亡后。它一次返回,从不抛出,所以从回调或计时器调用它。实例的teardown()运行时,其仍在运行的任务回到队列中,在新实例上再次运行,并且该实例已构建和设置。与canRestart: false这些任务反而失败了。池仅多次替换实例 (3,fyflue-scheduler的maxWorkerRestarts); 之后,它不再构建,它的任务等待直到重新加载。因此,询问实例何时中断,而不是因为Task失败:throw fromrun的。- 永远不要假设一个副本。 每个概要文件都有它自己的池,一个池可以容纳几个实例(最多为它的实例)
maxThreads),并且在重新加载之后,被替换的运行时的实例保持运行它的运行,而新的运行时设置它自己的。绑定端口0,而不是固定端口,并保持Worker之外的任何共享。 idleTimeout默认为5秒。对于设置成本高昂的工作人员,请给出其节点类型的pool长一点的(pool: { idleTimeout: 60_000 }); 配置文件可以为该节点类型覆盖它。0保留实例,直到其运行时关闭。
插件的数据目录
一个保持状态超过一个主机进程的插件(它的服务需要一个缓存,一个禁止等待结束的记录)集 dataDir: true (自0.6.0起)。然后主机给它一个自己的文件夹, plugin-data/<plugin id>/ 在主机的data目录下,并在构建插件的Workers之前创建它。插件的每个Worker在它的配置中找到它的绝对路径,如 host.dataDir,在内联池或线程池中:
import { join } from "node:path";
import type { HostConfig, Worker } from "fyflow-plugin-sdk";
type Config = { host: HostConfig; ttlHours?: number };
export default class Lookup implements Worker {
readonly #cache: string;
constructor(config: Config) {
this.#cache = join(config.host.dataDir, "answers.sqlite");
}
// …
}
- 该文件夹保存在重新加载、更新和卸载然后重新安装之间。应用程序永远不会删除它:要重新开始,请在主机停止时手动删除它。
- 因此,路径在配置中有一个地方,一个请求一个的插件可能没有名为的启动字段
host;Host跳过了它,并给出了理由。一个插件,不要求得到没有文件夹和没有host在它的配置中,以及它自己的host领域是它自己的。 - 几个实例,并在运行时重新加载替换,可以一次运行: 写入文件夹作为一个共享的地方 (一个SQLite数据库,一个文件被原子替换),而不是作为一个工人自己的。
插件的状态
一个在任务(连接,会话,速率限制后的队列)之间保持运行的插件可以说明它是如何运行的 status() (自0.6.0版起)。主持人在其插件页面上的插件卡上显示答案,在页面打开时每5秒再次询问一次,并将其提供给代理。一个没有插件 status() 显示无。
import { definePlugin } from "fyflow-plugin-sdk";
import { session } from "./session.ts"; // your own module, shared with the inline Workers
export default definePlugin({
// …
status: () =>
session.blockedUntil === null
? { level: "ok", summary: `Logged in · ${session.queued} queued` }
: {
level: "warning",
summary: "Blocked after a ban",
details: [{ label: "Blocked until", value: session.blockedUntil.toLocaleTimeString() }],
},
});
level是ok,info,warning或error;summary是一行;details是可选的标签和值对。它是数据:主机绘制它,所以插件仍然没有提供UI代码。- 它运行在主机自己的线程上,主机加载了插件的模块。它看到该模块与共享的内容
inline工人,而不是线程工人持有的东西。 - 一秒钟内回答。一个
status()扔,拒绝,用别的东西回答,或者花更长的时间显示error有原因的状态。当一个答案仍然悬而未决时,主机不再询问。 - 它是手握的
{ dataDir }当插件请求数据目录时,并且{}否则。 - 主机从答案中删除其秘密,就像从错误中删除一样,但不要将其放在那里。
这个 disk, cpu 和 gpu 小组
主机的内置插件声明了三个资源组,任何插件的节点类型都可以命名,每个资源组都有一个主机可以在其资源组页面上覆盖的并发限制:
disk(出口DISK_GROUP从0.3.0开始),在读取或写入主机磁盘的任务上。它是4。当您的任务执行繁重的磁盘工作 (复制,转码,索引) 时,将其命名,以便它们与主机自己的文件系统节点类型共享一个预算,而不是在它们旁边打转磁盘。cpu(出口CPU_GROUP从0.4.0开始),用于使处理器忙碌的任务(散列,编码,图像工作)。这是主机的逻辑处理器计数,因此来自每个插件的任务加在一起不会占用更多的处理器。gpu(出口GPU_GROUP从0.7.0开始),在使用GPU (推理,渲染,GPU编码) 的任务上。它是1,因为主机不能计算它的gpu; 一个人在一台机器上提出更多。它只计数: 一个任务不告诉它持有哪个GPU,所以当限制高于1时,你的插件选择每个任务的设备 (从其设置,或通过跟踪本身)。
同时做这两件事的Task:
import { CPU_GROUP, DISK_GROUP, definePlugin } from "fyflow-plugin-sdk";
export default definePlugin({
// …
nodeTypes: { Transcode: { worker: "transcode", groups: [DISK_GROUP, CPU_GROUP] } },
});
其他插件的组保持遥不可及,所以禁用一个插件永远不会打破另一个。键是值,因此导入它们的插件在运行时需要SDK; 编写 "disk", "cpu" 或 "gpu" 而是在具有组的任何主机上工作。SDK 0.4.0之前的主机没有 cpu 并跳过一个插件命名它,所以一个插件,它的名字说 ^0.4.0 在其对等范围内;这同样适用于 gpu 和 ^0.7.0。
在工人生活时举行小组活动
groups 保存一个任务的每个组的一个单位,从开始到结束。一个Worker在它的生命周期内持有一些东西,比如一个在任务之间加载在GPU内存中的模型,在它的Node Type中命名这个组 pool.residentGroups 相反 (自0.7.0起): 池在构建实例时获取单元,并在实例退役时将其返回 (空闲超过其 idleTimeout(重新加载或主机停止)。另一个池只在它需要的东西还合适的时候才构建实例,它的任务同时等待:直到持有者的 idleTimeout 在持有者完成最后一个任务后5秒。因此,持有组驻留的节点类型需要一个 idleTimeout 以上0;主机拒绝0个插件或配置文件中的一个。
import { definePlugin, GPU_GROUP } from "fyflow-plugin-sdk";
export default definePlugin({
// …
groups: { "imagegen.vram": { type: "ConcurrentLimitGroup", config: { limit: 24 } } }, // GB
workers: { model: { url: "./model.worker.ts" } },
nodeTypes: {
// Keeps its model loaded for 10 minutes after its last Task, holding the GPU and 16 GB of its memory meanwhile.
Generate: {
worker: "model",
pool: { maxThreads: 1, idleTimeout: 600_000, residentGroups: { [GPU_GROUP]: 1, "imagegen.vram": 16 } },
},
},
});
residentGroups 是一个键列表 (每个键的一个单元) 或每个键包含多少个单元,并且可以命名相同的组 groups 五月: 插件自己的和 disk, cpu 和 gpu。主机拒绝命名任何其他插件。每个实例都有单位,所以一个带有 maxThreads: 2 有两个;并且每个概要文件都有自己的节点类型池,因此在两个概要文件下运行的任务的节点类型为每个保存单元。保持 maxThreads 对于持有整个设备的Worker,以及在多个配置文件下运行它的主机,需要的单元数量与一次有活动池的数量一样多。
配置文件和秘密
插件的配置存在于主机上,而不是流中: A 配置文件 是一个命名的配置与部分的每个插件,编辑在主机的用户界面的形式,从插件的 profile.schema. 每个主机都有一个默认配置文件。声明您的字段一次,在 profile,并在每个节点类型上列出它需要的节点: 几个节点类型需要的字段设置一次,用于所有节点类型。没有配置文件集的字段是其 defaults 价值。
一个 启动字段 (列在 profile.startup) 在Worker启动时读取: 节点类型采用的启动字段是其Worker的配置,构造函数的参数,并且更改在主机的下一次重新加载时生效。使用一个工人设置一次: 一个连接,一个加载的模型。
每个其他字段都是一个 每个任务字段:主机在每个任务被分派时读取它,并将节点类型占用的任务交给主机 run 作为 settings.profile ({} (当它不需要时),因此更改到达下一个任务时不需要重新加载。输入 TaskEnvelope<Settings, Input, ProfileFields>. 你的 settingsSchema 不能命名 profile. 列表中 profile.profileOnly 每个任务的字段只有一个配置文件可以设置: 一个授权,如文件夹任务可以达到,旁边的字段流可以设置为缩小它,您的工作人员检查授权。每个任务字段永远不会达到运行的记录,但什么 run 退货是记录的,所以不要退货。
标记的属性 profile.schema writeOnly: true 把它变成 秘密: 主机永远不会再次显示它,仅显示它是否已设置,并从错误和日志中编辑其值。只有架构的属性可以是机密。
版本控制
这个包遵循semver。 SDK_VERSION 始终等于包版本。