@type32 /工作流
一个可视化的节点脚本工具包。语言开发者在TypeScript中定义类型、节点和效果;最终用户从画布上的这些节点构建文档;引擎验证并运行它们。文档是不可信的数据:引擎从不评估文档中的代码。
设计: 节点引擎规格。Vertex是第一种语言。
状态:M5。
@type32/workflows/core具有类型系统、具有类型化节点上下文的语言定义、validate、运行时、函数和变量;migrate和hashProgram;@type32/workflows/core/std有标准节点;@type32/workflows/core/testing有测试助手;@type32/workflows/vue具有画布编辑器。
使用
import { compile, defineLanguage, t, validate } from "@type32/workflows/core";
import { std } from "@type32/workflows/core/std";
// Effects keyed by capability id, and the TypeScript value type of each of the language's own types.
const vertex = defineLanguage<
{ fs: { rename(file: string, name: string): Promise<string> } },
{ "vertex.File": string }
>({
id: "vertex",
version: 1,
include: [std], // control, operators (logic, math, compare, dates), text, lists (filter, map, sort, …), dictionaries
})
.type({ id: "vertex.File", label: "File", schema: { type: "string", pattern: "^/" } }) // a schema allows literals
.capability({ id: "fs", mutates: true, label: "Modify files" })
.node({
type: "vertex.onManual",
version: 1,
mode: "event",
label: "On Run",
exec: { out: ["then"] },
})
.node({
type: "vertex.rename",
version: 1,
mode: "exec",
label: "Rename",
exec: { out: ["then"] },
inputs: {
file: { type: t("vertex.File"), required: true },
newName: { type: t("string"), required: true },
},
outputs: { file: { type: t("vertex.File") } },
requires: ["fs"],
async run(ctx) {
const { file, newName } = await ctx.inputs(); // file: string, newName: string
ctx.output("file", await ctx.effects.fs.rename(file, newName)); // ctx.effects has only `fs`
return "then"; // only declared exec outputs compile
},
})
.build(); // throws on any manifest problem
// Editors need only the JSON manifest.
const analysis = validate(document, vertex.manifest);
analysis.diagnostics; // [{ code: "E_TYPE_MISMATCH", message: "e7: string → vertex.File", at: { edge: "e7" } }, …]
analysis.typeOf("n3", "item", "out"); // resolved port type, e.g. { kind: "con", id: "vertex.File" }
analysis.capabilities; // Set { "fs", … } reachable from event nodes
// Hosts run it. compile throws CompileError on any error diagnostic; dispatch never rejects for program errors.
const result = await compile(document, vertex).dispatch("vertex.onManual", undefined, {
effects: { fs: { rename: async (file, name) => `/vault/${name}.md` } }, // each node sees only its `requires`
signal, // abort → { status: "aborted" }
limits: { steps: 1_000, timeoutMs: 10_000 },
trace: "nodes",
onTrace: (event) => console.log(event), // { t: "enter" | "exit" | "fire" | "value" | "fault", node, … }
});
result; // { status: "ok" | "fault" | "aborted", fault?: { code: "E_UNSET_VALUE", message, node }, steps }
ctx 根据定义键入:端口名称,值类型(List → readonly T[], Dictionary → ReadonlyMap、通用件 unknown),在编译时检查执行输出和功能。输入没有 required 或者一个 default 可能是 undefined.
测试一门语言
import { buildDocument, formatTrace, record, run } from "@type32/workflows/core/testing";
const doc = buildDocument(vertex, {
nodes: {
go: "vertex.onManual",
r: { type: "vertex.rename", literals: { file: "/v/a.md", newName: "b" } },
},
edges: ["go.then > r.exec"],
});
const rec = record({ fs: { rename: async (file: string, name: string) => `/v/${name}.md` } });
const { result, trace } = await run(vertex, doc, {
event: "vertex.onManual",
effects: rec.effects,
});
rec.log(); // ["fs.rename(/v/a.md, b)"]
formatTrace(trace); // [">go", "<go.then", ">r", "<r.then"]; `~n3.body` is a fire
assertValid(doc, language) 抛出一个错误,列出每个诊断。这些助手不依赖于测试框架。
通用节点声明 typeParams: [{ name: "T", bound?: t("vertex.Entry") }] 和使用 tvar("T") 在端口类型中; validate 推断每个实例的 T 从它的边缘。
运行时的值: List 是一个数组, Dictionary 是一个 ReadonlyMap (Dictionary 字面量被解码为 Map). 极限故障和中止是致命的; std.try 只捕获节点错误和其他错误。
加载文件: migrate(JSON.parse(text), language) 是不可信输入的边界。它返回 { doc, changed } (一个干净 DocumentV1 只有已知字段(已迁移到当前格式、语言和节点版本)或 { diagnostics } 当文件无效或比代码更新时,在这种情况下,主机以只读方式打开它。 hashProgram(doc) 是程序的SHA-256,忽略view和meta;由它托管密钥信任记录。
函数存在于 doc.program.functions。一个身体有一个 core.entry 和一个 core.return; core.call 调用它, core.get/core.set 读取和写入文档变量或函数的局部变量。声明泛型函数 typeParams (具有可选边界),使用那些作为刚性类型检查一次,并且在每次调用时都被实例化。达到 core.return 立即结束通话;一个从未到达它的exec函数是错误的 E_NO_RETURN.
在画布上编辑
同行依赖性: vue, @vue-flow/core 和 @nuxt/ui。组件是从next UI构建的,因此在主机中设置它 @nuxt/ui 模块,或者它的Vite和Vue插件 <UApp>),然后让Tailwind从你的CSS条目中扫描这个包:
@import "tailwindcss";
@import "@nuxt/ui";
@source "../node_modules/@type32/workflows/dist"; /* relative to this file */
<script setup lang="ts">
import "@type32/workflows/vue/style.css";
import { useWorkflowEditor, WorkflowWidget, WorkflowWorkbench } from "@type32/workflows/vue";
const editor = useWorkflowEditor(vertex.manifest, document); // the manifest is enough to edit
async function start() {
const { trace } = await run(vertex, editor.snapshot(), {
event: "vertex.onManual",
effects,
trace: "values",
});
editor.trace.value = trace; // run counts, values, taken edges and faults on the canvas
}
</script>
<template>
<!-- canvas, node library and inspector; or compose WorkflowCanvas, WorkflowPalette, WorkflowInspector yourself -->
<WorkflowWorkbench :editor="editor">
<template #toolbar><UButton label="Run" @click="start" /></template>
<!-- optional: replace one type's literal editor (on the canvas and in the inspector), keep the default for the rest -->
<template #widget="w">
<MyFilePicker v-if="w.type?.kind === 'con' && w.type.id === 'vertex.File'" v-bind="w" />
<WorkflowWidget v-else v-bind="w" />
</template>
</WorkflowWorkbench>
</template>
analysis 重播 validate 每一次改变。将添加接线或类型错误的拖动将被拒绝,并显示原因。连接使用的数据输入或执行输出取代其边缘。节点遵循主机的next UI主题;端口和电线的颜色是 --wf-* CSS变量。 bun run play 打开一个带有§10语言和内存库的游乐场。
语言将它们的节点分组为类别,这些类别为节点库提供部分,并为节点提供标题颜色。标签可以嵌入输入,节点内联显示为一个句子:
defineLanguage({ id: "notes", version: 1, include: [std] })
.category({ id: "notes.files", label: "Files", color: "teal", icon: "i-lucide-folder" })
.node({
type: "notes.rename",
label: "Rename {file} to {newName}", // inputs named here render inline
category: "notes.files",
// …
});
颜色(TypeDef, CategoryDef,以及节点自己的 color)取一个调色板名称(sky, rose,…)或任何CSS颜色。变量和My Blocks在类别之后 order 0或更小,所以 order: 1 将语言自己的部分放在它们后面,就像在Scratch中一样。
要翻译语言,请在将其交给editor: node之前重写其清单的字符串 label (保持 {input} 占位符)和 description、港口 label年代, exec.labels 对于执行输出,以及类别、类型和功能标签。
为每个项目计算一次输入的节点,例如 std.filter,在子作用域中读取它,其中项是它自己的输出: await ctx.input("keep", { scope: { item, index } }) 重新计算后面的纯节点 keep 对于那个项目。
在画布上,拖动空白区域绘制一个选择框;滚动,两个手指,空格+拖动或中间按钮平移,并⌘/Ctrl+滚动或缩放。快捷方式:⌘Z /⌘⌘Z撤消和重做(除视窗外的每个文档编辑),⌘C /⌘X /⌘V /⌘D复制,剪切,粘贴在光标处并复制,⌘A选择全部,Tab或双击在光标处打开节点搜索。在空白处放置一根电线,打开搜索,过滤到可以接受它的节点,并连接到你选择的那个节点。 右键单击画布、节点、边缘或端口将打开菜单; WorkflowCanvas的 menu 道具可以改变它的物品,而它的 #selection Slot将按钮添加到多节点选择下显示的栏中。
const editor = useWorkflowEditor(vertex.manifest, document, {
messages: () => ({ undo: t("undo"), selected: (n) => t("selected", { n }) }), // translate the built-in strings
clipboard: { read: readText, write: writeText }, // default: navigator.clipboard
});
副本以JSON格式(parseClip 读取它们),因此它们粘贴到相同语言的另一个文档中。当剪贴板无法读取(被拒绝的权限)时,粘贴会返回到应用程序中的最后一个副本。
程序从库和选择栏中增长结构。节点库中的变量和我的块创建变量和块(函数);对于Get,单击或拖动变量,对于Set,单击或拖动变量;检查器编辑所选节点后面的声明(块的名称、输入、输出和退出)。一个执行块可能有几个出口,每个Return指定一个出口,因此调用可以像 std.if。选择节点并选择Make block将它们移动到一个名为in their place的新块中(extractFunction in core完成工作),或者Group构建它们;拖拽组的标题会移动其中的内容,画布菜单会添加粘性注释。组和评论存在于 view,所以它们不会改变程序散列。双击一个调用打开它的代码块;面包屑通向后面。
通过编辑器在画布上观看: editor.run 递给你 onTrace, signal 和 beforeStep 展开到运行中,画布显示一个运行条(Pause、Step、Continue、Cancel),并显示正在运行的节点。
editor.stepping.value = stepThrough; // stop before the first node
const { result } = await editor.run((live) =>
run(vertex, editor.snapshot(), { event: "vertex.onManual", effects, trace: "values", ...live }),
);
花在 beforeStep 在上面提到的能力中 interactive (提示,拾取者)不计入 limits.timeoutMs.
脚本
| 脚本 | 做 |
|---|---|
bun run build | 包 src/core 和 src/vue → dist/ + .d.mts |
bun run play | 维特游乐场(playground/) |
bun run test | 运行Vitest一次(test:watch 观看) |
bun run typecheck | ui:prepare (生成next UI的主题类型),vue-tsc (TypeScript 6.0.3), TypeScript 7 on src/core |
bun run lint | Oxlint |
bun run format | Oxfmt (format:check 仅供核实) |
bun run check | Lint + format:check + typecheck + test |
bun run changeset | 记录下一个版本的变更 |
消费者支持的最小TypeScript是 6.0.3,被钉住的dev typescript,所以 typecheck 与它背道而驰。vue-tsc需要TypeScript API,而7.0没有;TypeScript 7 (typescript7)检查 src/core 没有DOM库。子路径导出是手工列出的 package.json.
释放
运行 bun run changeset 在你的PR中,在合并到 main,发布工作流打开“版本包”PR;合并发布到npm。
许可证
麻省理工学院