TongFlow代码架构完整剖析:从ABI合约到双语言类型生成的跨栈设计
TongFlow代码架构完整剖析:从ABI合约到双语言类型生成的跨栈设计
TongFlow 是一款 Modality-First(模态优先)GenAI 工作流平台,这篇文章带你剖析它的核心代码架构:如何用一份 ABI 合约(tongflow.abi.json)作为唯一事实来源,通过构建时双语言类型生成(TypeScript + Python),让画布前端、服务端与 Python SDK 三端共享同一套节点输入/输出类型,彻底避免"前后端类型漂移"。无需任何背景知识,看完即可理解这套跨栈设计。
什么是 TongFlow 的 ABI 合约?
先理解一个概念:在 TongFlow 中,每个 AI 能力都被封装成一个画布节点(文生图、语音转写、视频拼接……),而"节点长什么样"完全由一份 JSON 合约描述——这就是 ABI 合约。
合约文件位于 packages/tongflow/abi/tongflow.abi.json,结构非常直观:
{
"version": 1,
"$defs": { "Asset": { "type": "object", "required": ["bytesBase64"], ... } },
"nodes": [
{ "nodeSlot": "gen-text", "inputs": { "...": "..." }, "outputs": { "...": "..." } }
]
}
合约包含三部分关键内容:
version:ABI 版本号,当前为1,所有运行端都会校验它是否受支持$defs:6 个公共类型定义——Asset(字节级媒体数据)、FileRef、ImageRef、VideoRef、AudioRef、ModelRef(文件引用),是全部节点共用的"模态语汇"nodes:当前共 63 个节点槽位(nodeSlot),每个槽位声明自己的inputs/outputsJSON Schema
如果仓库里还没有合约,scripts/generate-tongflow-abi.mjs 可以生成一份初始骨架——但它非常保守,已存在的 ABI 永远不会被脚本改动,合约始终由人掌控。
TypeScript 端:一条命令生成全部类型
合约的 TypeScript 消费方由构建脚本 packages/tongflow/scripts/gen-abi-types.ts 负责,开发者只需执行:
pnpm gen:abi
这条命令做了四件事:
- 校验合约:
version必须是不小于MIN_SUPPORTED_ABI_VERSION的整数(定义在 packages/tongflow/src/core/schema/abi-version.ts,运行时解析与构建期代码生成共享同一个常量),nodes[]非空且nodeSlot不重复 - 生成 TS 类型:借助
json-schema-to-ts的FromSchema把每个槽位的 Schema 直接推断成原生类型,写入 packages/tongflow/src/core/generated/abi/index.ts(7000 余行,文件头标注do not edit manually) - 保留运行时 Schema:除了类型,还导出
ABI_DEFINITIONS与ABI_NODES常量,供 AJV 在运行时校验真实数据——编译期类型 + 运行时校验,双保险 - 镜像到 Python SDK:把合约 JSON 同步拷贝到
sdk/tongflow/_data/,防止两端 ABI 副本漂移
生成产物长这样(节选):
export type GenTextInput = FromSchema<typeof _slot_gen_text_inputs>;
export type NodeSlot = "gen-text" | "split-text" | /* ...63 个槽位 */;
export type SlotInput<S extends NodeSlot> = SlotInputsMap[S];
其中 SlotInput<S> / SlotOutput<S> 是按槽位分派的映射表——拿到任意一个 NodeSlot,类型系统就能立刻推出它的输入输出形状,写错字段直接编译报错。
运行时的另一半是 packages/tongflow/src/core/schema/tongflow-abi.ts:它用 zod 校验 ABI 文件本身,并通过 resolveAbiOutputMappings 读取输出 Schema 里的 $ref 自动推导下游节点类型——比如 VideoRef 输出自动路由到 videoNode,ImageRef 到 imageNode。连线规则也是从合约推导出来的,而不是硬编码的。
Python SDK 端:同一份合约,生成 Pydantic 模型
Python 端(位于 sdk/tongflow/,可独立 pip 安装运行工作流引擎)用同样一份合约做了对称的三件事:
| 脚本 | 产物 | 作用 |
|---|---|---|
| sdk/tongflow/gen_models.py | sdk/tongflow/models/*.py(每节点一个文件) |
把 JSON Schema 翻译成 Pydantic BaseModel |
| sdk/tongflow/gen_node_slots.py | sdk/tongflow/node_slots.py | 生成 63 个槽位的常量类 NodeSlots |
| sdk/tongflow/abi.py | 运行时加载器 | 校验 MIN_SUPPORTED_ABI_VERSION,与 TS 端逻辑对齐 |
生成的 Pydantic 模型(sdk/tongflow/models/gen_text.py)与 TS 类型一一对应,且开启 extra="forbid" 严格拒绝多余字段:
class GenTextInput(BaseModel):
model_config = ConfigDict(extra="forbid")
text: str
userPrompt: str | None = None
槽位常量表则让插件开发者写 @node_slot(NodeSlots.GEN_TEXT) 而不是手敲字符串:
class NodeSlots:
"""ABI nodeSlot constants. Use these in @node_slot(...)"""
GEN_TEXT: Final[str] = 'gen-text'
TRANSCRIBE: Final[str] = 'transcribe'
两个容易被忽略的架构细节
1. 输出侧的"不对称"处理。 ABI 声明输出为 VideoRef 等类型,但插件实际吐出的是 Asset(bytesBase64 字节流),由服务端上传后改写成 { file_key } 再交给下游。sdk/tongflow/gen_models.py 中特意把输出侧的 *Ref 类型标注为插件真实产出的 Asset 形状——类型系统忠实反映数据流的真实形态,而不是理想化的声明。
2. ABI 文件的多级查找。 引擎侧 sdk/tongflow/engine/abi_schema.py 按"显式参数 → TONGFLOW_ABI_PATH 环境变量 → pip 包内置副本 → 仓库内路径"的顺序定位合约。这意味着 pip install tongflow 之后,ABI 随包分发,Python 引擎可以完全脱离 npm 包独立运行,而内置副本正是前面 pnpm gen:abi 自动镜像保证新鲜的。
跨栈设计小结
| 关注点 | TongFlow 的做法 |
|---|---|
| 事实来源 | 唯一 JSON 合约 tongflow.abi.json,人工维护 |
| TS 类型安全 | pnpm gen:abi 生成 generated/abi/index.ts,编译期报错 |
| 运行时校验 | 同一份 Schema 导出为 ABI_NODES,AJV 运行时兜底 |
| Python 类型安全 | 同一份合约生成 Pydantic 模型 + 槽位常量 |
| 版本管控 | 两端共享 MIN_SUPPORTED_ABI_VERSION = 1,加载时统一拦截不兼容版本 |
| 节点连线规则 | 从输出 $ref 自动推导节点类型,零硬编码 |
整套架构的核心思想一句话概括:节点能力只描述一次,其余全部分层派生——类型、常量表、运行时校验、连线规则都是这份 ABI 合约的"编译产物"。新增一个节点只需改一处 JSON,TypeScript 与 Python 两端重新生成即同步,这就是 TongFlow 能在多模态节点频繁扩展下仍保持代码库整洁的关键。想进一步了解节点编排规则,可阅读 docs/agent-workflow-manual.md。
