跳到主要内容

词汇表:节点、数据值与图模型

30 秒导读: 讲引擎之前,得先认字。Rivet 是一个"可视化 AI 编程"工具——你拖节点、连线,就搭出一个跑 LLM 的程序。这一章只干一件事:把 Rivet 里那几个最基础的名词讲清楚——一个"节点"作为数据长什么样、端口上流动的"值"有哪些类型、节点靠什么被系统认识。不讲引擎怎么调度这些节点(那是 02),也不讲那个诡异的 control-flow-excluded 值背后的语义(那是 03)。读完你就能看懂后面几章在说什么。

本章属于 Rivet 系列的第 1 章。想先看"Rivet 是什么、整体怎么转",去 index;想看这些数据被谁消费,去 02 引擎


1. 为什么先学词汇

Rivet 把一个 AI 工作流表示成一张有向图:方框是节点(node),节点边缘的小圆点是端口(port),端口之间的连线是连接(connection)。运行时,数据从一个节点的输出端口"流"到下一个节点的输入端口。

这里的关键直觉是:图本身只是数据,不是代码。一张图存到磁盘上,就是一坨 JSON——里面有节点列表、连接列表,每个节点记着自己的 type(类型字符串)、位置、配置数据。真正"这个节点该怎么运行"的逻辑,不在图里,而在一个type 查出来的实现类里。

所以这一章要认的字,分成三组:

名词一句话
图的骨架ChartNode · NodeConnection · PortId节点、连线、端口的纯数据形状
节点的行为NodeImpl · PluginNodeImpl抽象类:定义端口、执行 process
端口上的值DataValue · coerceType带类型标签的值,以及类型间的强制转换

外加两个"装东西的容器"——NodeGraph / Project(图和项目),和一个"电话簿"——NodeRegistration(把 type 字符串映射到实现类)。


2. 顶层全景:这些名词怎么拼在一起

下面这张图是本章所有名词的"合影"。从左到右是从数据到行为的路径:一张图里的一个 ChartNode 记着 type,引擎拿 typeNodeRegistration 查出 NodeImpl 实现,实现类通过端口定义和 process 与外界交换 DataValue

Project(一个项目 = 多张图 + 引用)
└─ graphs: Record<GraphId, NodeGraph>
└─ NodeGraph { nodes[], connections[] }

┌───────┴─────────────────────────────┐
│ │
ChartNode NodeConnection
{ type:'if', id, data, visualData } (outputNodeId.outputId
│ type = 'if' → inputNodeId.inputId)

▼ 查电话簿
NodeRegistration ── 'if' ──▶ IfNodeImpl (一个 NodeImpl 子类)

┌────────────────┼────────────────────┐
▼ ▼ ▼
getInputDefinitions getOutputDefinitions process(inputs)
(声明入端口) (声明出端口) (真正干活)
│ │
▼ ▼
NodeInputDefinition[] Inputs / Outputs
(每个端口有 PortId + dataType) = Record<PortId, DataValue>


DataValue { type, value }
'string' | 'number' | 'chat-message'
| 'control-flow-excluded' | …

各部件一句话职责:

部件干什么在哪个文件
ChartNode图里一个节点的纯数据(type/id/data/位置)packages/core/src/model/NodeBase.ts:68
NodeConnection一条连线:从哪个节点的哪个出端口,到哪个入端口NodeBase.ts:143
PortId端口的唯一标识(节点内唯一)NodeBase.ts:9
NodeInputDefinition / NodeOutputDefinition节点声明自己有哪些端口、各是什么类型NodeBase.ts:95 / :122
NodeImpl抽象类:一个节点类型的行为(定义端口 + process)NodeImpl.ts:42
PluginNodeImpl插件作者实现的"扁平"接口,被桥接成 NodeImplNodeImpl.ts:16
DataValue端口上流动的值,带 type 标签的判别联合DataValue.ts:155
coerceType把一个 DataValue 强制转成想要的类型utils/coerceType.ts:61
NodeGraph / Project图 / 项目容器NodeGraph.ts:8 / Project.ts:10
NodeRegistrationtype 字符串 → 实现类的注册表(电话簿)NodeRegistration.ts:8

3. 一个节点作为数据:ChartNode

3.1 NodeBase——所有节点共有的字段

每个节点,不管什么类型,都带一组公共字段。这组字段定义在 NodeBase 接口里(NodeBase.ts:12-65)。挑重点看:

字段含义
type: string节点类型字符串,如 'if''chat'这是查实现类的钥匙。
id: NodeId图内唯一 id(用 nanoid 生成)
title: stringUI 上显示的标题
data: unknown该节点的配置数据(基类里是 unknown,子类里收紧类型)
visualData画布上的位置/尺寸/颜色/z-index——纯 UI,不影响运行
disabled?为真时该节点"当作没运行"
isConditional?为真时节点多出一个 if 端口,可条件运行
isSplitRun? / splitRunMax?是否把一次执行"拆分"成多次(数组扇出)

注意 NodeIdPortId 都是 Opaque<string, ...>(NodeBase.ts:6:9)——底层就是字符串,但用 type-fest 的 Opaque 打了个"品牌"标签,让 TypeScript 不允许把普通字符串直接当 id 用。这是零运行时开销的类型安全技巧。

3.2 ChartNode——给节点加上具体类型

NodeBase.dataunknown,不好用。ChartNode<Type, Data> 是它的带类型版本(NodeBase.ts:68-77):

// packages/core/src/model/NodeBase.ts:68 —— 真实源码(节选)
export type ChartNode<Type extends string = string, Data = unknown> = NodeBase & {
type: Type; // 收紧成字面量类型,如 'if'
data: Data; // 收紧成该节点自己的配置类型
};

每个节点类型都会 export type XxxNode = ChartNode<'xxx', XxxNodeData>。例如 IfNode 就是 ChartNode<'if', IfNodeData>(nodes/IfNode.ts:22)。这样 node.type === 'if' 时,TypeScript 就知道 node.dataIfNodeData


4. 端口与连接

4.1 端口"定义"是节点自己算出来的

一个关键设计:节点有哪些端口,不是写死的数据,而是节点实现动态算出来的。 节点实现要给出两个方法:getInputDefinitions() 返回 NodeInputDefinition[],getOutputDefinitions() 返回 NodeOutputDefinition[]

NodeInputDefinition 的形状(NodeBase.ts:95-119):

字段含义
id: PortId端口标识,节点内唯一
title: stringUI 上端口名
dataType: DataType | DataType[]端口接受的数据类型(可多选)
required?是否必须连接,否则节点不能运行
defaultValue?未连接时的默认值
coerced?是否尝试把传入值强转成目标类型(见 §6.4)

NodeOutputDefinition(:122-140)类似,但没有 required/coerced——输出是往外给的。

为什么端口要"动态算"?因为很多节点的端口取决于配置。比如一个"模板"节点,你在文本里写了几个 {{变量}},它就长出几个对应的输入端口。所以 getInputDefinitions 收到 connectionsnodesproject 做参数——它可以看着当前的图状态来决定端口。

4.2 连接就是"四元组"

NodeConnection 极简(NodeBase.ts:143-155):它只是记"哪个节点的哪个出端口,连到哪个节点的哪个入端口"。

// packages/core/src/model/NodeBase.ts:143 —— 真实源码
export type NodeConnection = {
outputNodeId: NodeId; // 起点节点
outputId: PortId; // 起点的出端口
inputNodeId: NodeId; // 终点节点
inputId: PortId; // 终点的入端口
};

一张图 = 一堆 ChartNode + 一堆 NodeConnection,如此而已。

4.3 内建的 if 端口

有一个特殊端口 IF_PORT(NodeBase.ts:157-164):id 是 $if,布尔类型,coerced: true。当节点的 isConditional 为真时,基类会自动把这个端口追加到入端口列表末尾(见 §5.2)。它的语义(条件为假就不运行)属于控制流,留给 03


5. 一个节点的行为:NodeImpl

数据(ChartNode)只是躯壳,行为在 NodeImpl 抽象类里(NodeImpl.ts:42-107)。

5.1 核心契约:三个抽象方法

NodeImpl 持有一个 chartNode(那份数据),并暴露 id/type/title/data 等 getter。它强制子类实现三件事:

抽象方法签名要点干什么
getInputDefinitions(connections, nodes, project, referencedProjects)返回 NodeInputDefinition[]声明入端口
getOutputDefinitions(...)返回 NodeOutputDefinition[]声明出端口
process(inputData, context)async,返回 Promise<Outputs>真正干活:读输入,产出输出

其中 process 的输入输出类型都是"端口 → 值"的字典(GraphProcessor.ts:141-142):

// packages/core/src/model/GraphProcessor.ts:141 —— 真实源码
export type Inputs = Record<PortId, DataValue | undefined>;
export type Outputs = Record<PortId, DataValue | undefined>;

也就是说,process 收到"每个入端口现在是什么值",返回"每个出端口应该是什么值"。全是 DataValue(见 §6)。此外还有两个非抽象的可选方法,默认返回空:getEditors(节点的配置面板)和 getBody(节点框里显示的小字),子类按需覆盖(NodeImpl.ts:100-106)。

5.2 内建端口的自动拼接

子类的 getInputDefinitions 只管声明"业务端口"。基类另有一个 getInputDefinitionsIncludingBuiltIn(NodeImpl.ts:76-89),它先拿子类的结果,再在 isConditional 为真时把 IF_PORT 追加进去。引擎实际用的是这个"含内建"的版本,子类不用操心 $if 端口。

5.3 NodeImplConstructor——类身上还有静态成员

注意一个容易忽略的点:getUIDatacreate 不是实例方法,而是静态成员,由 NodeImplConstructor 类型约束(NodeImpl.ts:156-162):

// packages/core/src/model/NodeImpl.ts:156 —— 真实源码
export type NodeImplConstructor<T extends ChartNode> = {
new (chartNode: T, pluginImpl: PluginNodeImpl<T> | undefined): NodeImpl<T>;
create(pluginImpl?: PluginNodeImpl<T>): T; // 静态:造一个新节点数据
getUIData(context: RivetUIContext): NodeUIData | Promise<NodeUIData>; // 静态:菜单/信息框元数据
};
  • create()工厂:返回一份全新的 ChartNode 数据(带默认 data、默认位置)。你在 UI 里新建一个节点,调的就是它。
  • getUIData() 返回上下文菜单标题、信息框文字、所属分组等(NodeUIData,NodeImpl.ts:148-154)——纯给编辑器用的元数据。

为什么是静态?因为造节点、显示菜单时还没有节点实例,只有类。所以这些必须挂在类上而非实例上。

5.4 插件的桥:PluginNodeImplPluginNodeImplClass

内建节点写成 NodeImpl 的子类(要处理 this、继承、静态方法)。但让插件作者去继承一个抽象类、还要搞对静态方法,门槛偏高。于是 Rivet 给插件提供了一个扁平的对象接口 PluginNodeImpl(NodeImpl.ts:16-40)——插件作者只需写一个普通对象,把 getInputDefinitions / getOutputDefinitions / process / create / getUIData 等都写成对象的方法即可,不用碰 class。

两个接口的方法签名有个细微但重要的差别:PluginNodeImpl 的方法第一个参数显式收 data(因为对象里没有 this.data),而 NodeImpl 子类直接用 this.data。对照:

NodeImpl 子类: getInputDefinitions(connections, nodes, project, referencedProjects)
PluginNodeImpl 对象: getInputDefinitions(data, connections, nodes, project)

桥接由 PluginNodeImplClass(NodeImpl.ts:111-146)完成:它是 NodeImpl 的一个具体子类,内部握着那个插件对象 impl,把每个实例方法转发给对象、并自动把 this.data 补进第一个参数。例如:

// packages/core/src/model/NodeImpl.ts:135 —— 真实源码
process(inputData: Inputs, context: InternalProcessContext): Promise<Outputs> {
return this.impl.process(this.data, inputData, context); // 把 this.data 补进去,转发给插件对象
}

于是引擎眼里,插件节点和内建节点长得一模一样(都是 NodeImpl),差别被这层桥吃掉了。

NodeDefinition / PluginNodeDefinition 只是把"实现 + 显示名"打包(NodeDefinition.ts:4-34),配套两个便捷构造函数 nodeDefinition(impl, displayName)pluginNodeDefinition(impl, displayName)


6. 端口上流动的值:DataValue

现在到最核心的词汇。端口之间流动的每一个值,都是一个 DataValue——一个带类型标签的判别联合(discriminated union)。

6.1 基本形状:{ type, value }

所有 DataValue 都长成 { type, value } 两个字段(DataValue.ts:5-8):

// packages/core/src/model/DataValue.ts:5 —— 真实源码
export type DataValueDef<Type extends string, RuntimeType> = {
type: Type; // 判别标签,如 'string'
value: RuntimeType; // 对应的真实值,如 "hello"
};

type 是判别标签,value 是真实数据。看到 type: 'string' 就知道 valuestring;看到 type: 'number' 就知道 valuenumber。这样一个统一信封,就能在端口上传任何东西,而消费方靠 switch (value.type) 精确取用。

6.2 标量类型:从 string 到 chat-message

标量(scalar)是最基本的、不可再分的值类型。全集在 ScalarDataValue 联合里(DataValue.ts:118-135)。列几个代表:

typevalue 的运行时类型说明
'string' / 'number' / 'boolean'string / number / boolean标量三件套
'date' / 'time' / 'datetime'string(ISO)时间用字符串存
'object'Record<string, unknown>任意对象
'any'unknown逃生舱:啥都行
'vector'number[]向量(嵌入)
'image' / 'audio' / 'binary' / 'document'Uint8Array 的结构二进制媒体
'gpt-function'GptFunctionLLM 的函数/工具定义
'chat-message'ChatMessage对话消息,LLM 生态的核心
'control-flow-excluded'undefined | 'loop-not-broken'控制流哨兵值(见 §6.5)

6.3 ChatMessage——LLM 那一支的核心值

chat-message 装的 ChatMessage 本身也是个判别联合(DataValue.ts:64),按 type 分四种角色:

ChatMessage.type谁说的特有字段
'system'系统提示
'user'用户
'assistant'模型function_calls(模型要调的工具)
'function'工具返回name(哪个函数的结果)

每条消息的 message 不是纯字符串,而是 ChatMessageMessagePart | ...[](DataValue.ts:66-77)——可以是文本,也可以是 image / url / document 片段,支持多模态。这些类型如何被真正喂给模型,是 04 LLM 节点的内容。

6.4 数组、函数与强制转换

数组类型由标量类型派生:ArrayDataValue<T>type 是模板字符串 `${T['type']}[]`(DataValue.ts:139)。所以 string 的数组版就是 type: 'string[]'value: string[]

函数类型 FunctionDataValue<T>(:141)的 type`fn<${T['type']}>`,value 是一个 () => T['value'] 的惰性函数——用于按需求值(端口上放一个"还没算的值")。unwrapDataValue(coerceType.ts 引用的 DataValue.ts:394)会在需要时调用它拿到真值。

把这些拼起来,DataValue = 标量 | 数组 | 函数(DataValue.ts:155),而 DataType = DataValue['type'] 就是所有合法 type 字符串的联合。文件末尾还有一个 dataTypes 元组(:170-239)用 exhaustiveTuple 把每一个类型字符串都列一遍——exhaustiveTuple 是个编译期技巧,漏掉任何一个类型都会编译报错,保证这份清单永远和联合类型同步。

coerceType(utils/coerceType.ts:61-67)是端口连接的"变压器":把一个 DataValue 强转成调用方想要的 DataType,返回裸的运行时值(不是包着 type 的信封)。它委托给 coerceTypeOptional(:15-59),后者用 ts-patternmatch 按目标类型分派:转 stringcoerceToString、转 numbercoerceToNumber……例如 chat-messagestring 会把所有 message part 拼成一段文本(:150-161)。端口定义里那个 coerced: true(§4.1)就是在说"连进来的值先过一遍 coerceType"。配套还有 inferType(:69)做反向的事:看一个裸 JS 值,猜它该是哪个 DataValue 类型。

6.5 埋一个伏笔:control-flow-excluded

标量里有一个特别的类型,先在这里只认字,语义留到 03:

// packages/core/src/model/DataValue.ts:116 —— 真实源码
export type ControlFlowExcludedDataValue =
DataValueDef<'control-flow-excluded', undefined | 'loop-not-broken'>;

它是一个哨兵值(sentinel):当一个端口的值是 control-flow-excluded 时,意思是"这条路被剪掉了,下游不该运行"。它就是靠伪装成一种普通 DataValue,让分支、循环、竞速这些控制流不需要引擎里的特殊 if/else,而是用数据流本身表达控制流。这正是 Rivet 最精妙的设计,03 专门讲它。本章只需记住:它是一个合法的 DataValue 类型,能在端口上流动。


7. 装节点的容器:NodeGraphProject

7.1 NodeGraph——一张图

NodeGraph 的定义朴素得让人安心(NodeGraph.ts:8-19):就是节点数组 + 连接数组,外加可选的 metadata(id/名字/描述)。emptyNodeGraph()(:21-31)造一张空图,顺手用 nanoid 生成 GraphId

7.2 Project——一堆图 + 引用

Project 是更大的容器(Project.ts:10-21):

字段含义
graphs: Record<GraphId, NodeGraph>这个项目里所有的图
metadatamainGraphId(入口图)、title、可选的 mcpServer 配置
plugins?项目用到的插件加载规格
data?大数据的外置存储(id → 内容)
references?其它项目的引用

ProjectReference(Project.ts:34-43)让一个项目引用另一个项目的图——注释明确写了"引用不能有环"。真正把被引用项目加载进来的活,由一个接口 ProjectReferenceLoader 承担(ProjectReferenceLoader.ts:3-6):它只有一个方法 loadProject(currentProjectPath, reference)。这是一个注入点——core 只定义接口,不实现;具体怎么按路径找文件、读进来,交给宿主(桌面端/Node 端)去实现。跨项目引用的实际调度属于运行时,见 05


8. 节点如何被系统认识:NodeRegistration

最后一块拼图:引擎手里只有一个 type: 'if' 字符串,它怎么找到 IfNodeImpl?答案是 NodeRegistration——一本"电话簿"(NodeRegistration.ts:8)。

8.1 register:登记一个内建节点

register(definition, plugin?) 把一个 NodeDefinition 存进内部表(NodeRegistration.ts:26-52)。它的做法很巧:

// packages/core/src/model/NodeRegistration.ts:32 —— 真实源码(节选)
const typeStr = definition.impl.create(undefined).type as T['type'];
if (newRegistration.#infos[typeStr]) {
throw new Error(`Duplicate node type: ${typeStr}`);
}

调一次 create() 造个样品节点,读出它的 type 字符串当键——不用你手动再写一遍类型名,类型名的唯一真源就是节点自己的 create()。重复登记同一个 type 会直接抛错。

register 的返回类型也值得一提:它返回 NodeRegistration<NodeTypes | T['type'], Nodes | T>——每 register 一个节点,类型参数里就多累积一个 type。于是 .register(a).register(b)... 链式调用完,TypeScript 在类型层面就知道"这个注册表里到底有哪些节点类型",BuiltInNodes / BuiltInNodeType 就是这么推导出来的(Nodes.ts:344-346)。

8.2 registerPluginNode:把插件对象包成类

插件走 registerPluginNode(NodeRegistration.ts:54-91)。因为插件给的是扁平对象 PluginNodeImpl(§5.4),注册时会当场生成一个匿名的 PluginNodeImplClass 子类,把插件对象的静态 create / getUIData 搬到类上,再存进表。这样插件节点在表里和内建节点同构,后续 createImpl 等方法一视同仁。

8.3 从注册表取东西

拿到 type 后,注册表提供两组操作:

方法作用
create(type)造一份新的 ChartNode 数据(调实现类的静态 create)
createImpl(node)给一个已有节点,new 出它的 NodeImpl 实例(引擎运行时用)
getDisplayName(type) / getNodeTypes()取显示名 / 列出所有已注册类型

createImpl(:129-146)是引擎每次要运行一个节点前调的:按 node.type 查到实现类,new ImplClass(node, pluginImpl),得到一个能 process 的实例。

8.4 全局注册表与内建节点

那 84 个内建节点在哪里登记?Nodes.ts 底部有一个巨大的链式调用 registerBuiltInNodes(registry)(Nodes.ts:253-338),把 userInputNodechatNodeifNode…… 一路 .register() 下去。然后:

// packages/core/src/model/Nodes.ts:340 —— 真实源码
let globalRivetNodeRegistry = registerBuiltInNodes(new NodeRegistration());
export { globalRivetNodeRegistry };

globalRivetNodeRegistry 就是那个默认全局电话簿,开箱即带全部内建节点。resetGlobalRivetNodeRegistry()(:351-353)能把它重置成"只有内建节点"的干净状态——测试时清插件很有用。插件通过 registry.registerPlugin(plugin)(NodeRegistration.ts:105-110)接入,它会回调插件的 register 把插件节点逐个登记进来。


9. 全景合一:IfNode 从头到尾

把前八节的词汇串起来,看一个真实、完整的节点长什么样。IfNode(nodes/IfNode.ts)是 Rivet 里最简单的节点之一——它做的就是"条件为真,值走 True 口;否则走 False 口"——但五脏俱全。

① 类型声明(IfNode.ts:22-26):

// packages/core/src/model/nodes/IfNode.ts:22 —— 真实源码
export type IfNode = ChartNode<'if', IfNodeData>;
export type IfNodeData = {
unconnectedControlFlowExcluded?: boolean;
};

一个 ChartNode,type 定死为 'if',data 收紧为 IfNodeData(就一个配置开关)。对应 §3.2。

② 实现类 + 静态工厂(IfNode.ts:28-45):IfNodeImpl extends NodeImpl<IfNode>,静态 create() 返回一份新节点数据(默认位置、默认 data)。对应 §5.3。

③ 声明端口(IfNode.ts:46-79):getInputDefinitions 返回两个入端口 ifvalue(都 any 类型);getOutputDefinitions 返回两个出端口 output(标题 "True")和 falseOutput(标题 "False")。对应 §4.1。

process——真正干活(IfNode.ts:103-164),这里能看到 DataValuecontrol-flow-excluded 一起登场:

// packages/core/src/model/nodes/IfNode.ts:103 —— 真实源码(节选)
async process(inputData: Inputs): Promise<Outputs> {
const ifValue = inputData['if' as PortId];
const value = inputData['value' as PortId] ?? unconnectedValue;

const isFalse = {
output: { type: 'control-flow-excluded', value: undefined } as ControlFlowExcludedDataValue,
falseOutput: value,
};
if (!ifValue) return isFalse; // 没条件 → 走 False
if (ifValue.type === 'control-flow-excluded') return isFalse; // 上游被剪 → 继续剪
// …各种"假"的判断…
return { // 条件为真
['output' as PortId]: value, // value 走 True 口
['falseOutput' as PortId]: { type: 'control-flow-excluded', value: undefined }, // False 口被剪
};
}

看清楚这个模式:它不"跳过"任何下游节点,只是把不该走的那个出端口的值设成 control-flow-excluded True 分支活跃时,False 口输出哨兵值;反之亦然。下游节点收到哨兵值就自动"熄火"。这就是 §6.5 埋的伏笔在真实节点里的样子——控制流完全由 DataValue 表达,引擎本身不含 if/else。它到底怎么让下游熄火,是 03 的主题。

⑤ 登记(IfNode.ts:167):export const ifNode = nodeDefinition(IfNodeImpl, 'If'),然后在 Nodes.ts:263.register(ifNode) 收进全局注册表。闭环完成。

至此,一个节点"是什么数据 → 有什么端口 → 怎么运行 → 怎么被认识"全部对上了本章的词汇。下一章 02谁在驱动这些 process、按什么顺序拉数据


10. 代码地图

主题文件路径符号名
节点公共字段 / 带类型节点packages/core/src/model/NodeBase.tsNodeBase · ChartNode · NodeId · PortId
端口定义 / 连接 / 内建 if 口packages/core/src/model/NodeBase.tsNodeInputDefinition · NodeOutputDefinition · NodeConnection · IF_PORT
节点行为抽象类 + 契约packages/core/src/model/NodeImpl.tsNodeImpl · getInputDefinitionsIncludingBuiltIn · NodeImplConstructor · NodeUIData
插件扁平接口 + 桥接packages/core/src/model/NodeImpl.tsPluginNodeImpl · PluginNodeImplClass
实现 + 显示名打包packages/core/src/model/NodeDefinition.tsnodeDefinition · pluginNodeDefinition
值的判别联合 / 标量 / 数组 / 函数packages/core/src/model/DataValue.tsDataValueDef · DataValue · ScalarDataValue · ArrayDataValue · dataTypes
对话消息类型packages/core/src/model/DataValue.tsChatMessage · ChatMessageMessagePart
控制流哨兵值(语义见 03)packages/core/src/model/DataValue.tsControlFlowExcludedDataValue
类型强制转换 / 推断packages/core/src/utils/coerceType.tscoerceType · coerceTypeOptional · inferType
图 / 项目 / 引用packages/core/src/model/NodeGraph.ts · Project.ts · ProjectReferenceLoader.tsNodeGraph · Project · ProjectReference · ProjectReferenceLoader
注册表(电话簿)packages/core/src/model/NodeRegistration.tsNodeRegistration · register · registerPluginNode · createImpl
内建节点登记 / 全局注册表packages/core/src/model/Nodes.tsregisterBuiltInNodes · globalRivetNodeRegistry · resetGlobalRivetNodeRegistry
完整节点范例packages/core/src/model/nodes/IfNode.tsIfNode · IfNodeImpl · ifNode

相邻章节: index(Rivet 全景) · 02 引擎(谁驱动 process) · 03 控制流(control-flow-excluded 的语义) · 04 LLM 节点(ChatMessage 怎么喂模型) · 05 运行时(项目引用加载、序列化、测试)