TongFlow代码架构完整剖析:从ABI合约到双语言类型生成的跨栈设计

原创2026-10-15 20:12:311,759 阅读

TongFlow代码架构完整剖析:从ABI合约到双语言类型生成的跨栈设计

TongFlow 是一款 Modality-First(模态优先)GenAI 工作流平台,这篇文章带你剖析它的核心代码架构:如何用一份 ABI 合约(tongflow.abi.json)作为唯一事实来源,通过构建时双语言类型生成(TypeScript + Python),让画布前端、服务端与 Python SDK 三端共享同一套节点输入/输出类型,彻底避免"前后端类型漂移"。无需任何背景知识,看完即可理解这套跨栈设计。

TongFlow 项目图标:TongFlow 多模态 AI 工作流平台的应用图标

什么是 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 / outputs JSON Schema

如果仓库里还没有合约,scripts/generate-tongflow-abi.mjs 可以生成一份初始骨架——但它非常保守,已存在的 ABI 永远不会被脚本改动,合约始终由人掌控。

TypeScript 端:一条命令生成全部类型

合约的 TypeScript 消费方由构建脚本 packages/tongflow/scripts/gen-abi-types.ts 负责,开发者只需执行:

pnpm gen:abi

这条命令做了四件事:

  1. 校验合约:version 必须是不小于 MIN_SUPPORTED_ABI_VERSION 的整数(定义在 packages/tongflow/src/core/schema/abi-version.ts,运行时解析与构建期代码生成共享同一个常量),nodes[] 非空且 nodeSlot 不重复
  2. 生成 TS 类型:借助 json-schema-to-ts 的 FromSchema 把每个槽位的 Schema 直接推断成原生类型,写入 packages/tongflow/src/core/generated/abi/index.ts(7000 余行,文件头标注 do not edit manually)
  3. 保留运行时 Schema:除了类型,还导出 ABI_DEFINITIONS 与 ABI_NODES 常量,供 AJV 在运行时校验真实数据——编译期类型 + 运行时校验,双保险
  4. 镜像到 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。连线规则也是从合约推导出来的,而不是硬编码的。

TongFlow 星标动图:TongFlow 项目文档中的星标宣传动画(TongFlow 项目展示图)

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。

登录后查看全文
tongflow