首页
/ Langflow BUNDLE API 契约详解:Extension Bundle 的稳定 API 表面、Manifest 规范与热重载管线

Langflow BUNDLE API 契约详解:Extension Bundle 的稳定 API 表面、Manifest 规范与热重载管线

2026-09-04 18:03:37作者:裘晴惠Vivianne

本文基于仓库根目录的 BUNDLE_API.md 展开,系统讲解 Langflow Extension Bundle 所消费的稳定 API 契约:BUNDLE_API_VERSION 版本机制、v0 契约面的全部符号、extension.json / [tool.langflow.extension] Manifest 的字段约束与校验规则、发现与加载管线(loader/discovery)、Bundle 热重载管线、类型化错误码集合,以及官方试点 Bundle lfx-duckduckgo 的完整落地方式。读完后,你可以按契约独立开发、校验并热加载一个 Langflow Extension Bundle,并理解每次契约变更为何必须伴随版本号递增与 Changelog。

契约的定位:为什么要有 BUNDLE_API

Langflow 的扩展体系以 Extension 为分发单元(一个可 pip install 的分发),其中装着一个或多个 Bundle(命名组件组)和可选的模型 provider。Bundle 编写者依赖的公共符号——Component 基类、各类 Input/Output、Schema 类型、懒加载导入助手、Manifest 模型、加载器入口、重载管线、错误类型——构成了一张"契约表面"(surface)。

BUNDLE_API.md 对这张表面做出的核心承诺是:

下面列出的每一个公共符号都是契约的一部分:对其名称、签名、语义或可见性的任何修改,都需要协调版本递增,并添加一条 ## Changelog 条目。

文档与该契约配套的整数版本号 BUNDLE_API_VERSION 声明在 lfx/extension/manifest.py

BUNDLE_API_VERSION: int = 1
"""...Manifests declare the contract versions they support via
``lfx.compat`` (a list of stringified integers); a manifest that does not
include ``str(BUNDLE_API_VERSION)`` is rejected at install time with
``version-constraint-unsatisfied``."""

注意三个容易混淆的"版本"概念(manifest.py 源码注释中明确区分):

概念 位置 含义
BUNDLE_API_VERSION = 1 manifest.py BUNDLE_API.md 契约本身的整数版本号,从第一天起就是 1,而非 0
SCHEMA_VERSION = 1 manifest.py Manifest 文件自身形态(shape)的版本,仅在 v0 manifest 对新版形态失效时才递增
文档中的 "v0" BUNDLE_API.md 契约的初始状态标记,属于文档表述,与整数版本无关

Bundle 通过 Manifest 中的 lfx.compat 字段声明自己支持的契约版本,例如 "lfx": {"compat": ["1"]}。安装时加载器将 str(BUNDLE_API_VERSION) 与该列表比对,不含当前版本的分发会被拒绝,错误码为 version-constraint-unsatisfied。对应模型是 LfxCompat

class LfxCompat(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    compat: list[StrictStr] = Field(..., min_length=1)

compat 条目必须是"字符串化的正整数"(正则 ^[1-9]\d*$,不允许前导零),且不可重复;v0 只接受 ["1"],字段设计为列表以便未来 Bundle 声明 ["1", "2"] 这样的前向兼容支持。

CI 门禁:任何修改了契约内文件(in-scope surface)的 PR,MUST 添加描述变更的 ## Changelog 条目,由 CI 守卫脚本 scripts/migrate/check_bundle_api_changelog.py 强制。保持所有公共符号名称与签名不变的纯内部重构不要求 Changelog,但审查者应保持怀疑态度——这条门禁是"契约即代码"的最后一道防线。

v0 契约面全景:十组公共符号

以下按 BUNDLE_API.md 的"Surface (v0)"章节逐组完整枚举,并给出对应的仓库源码落点。

1. Component 基类

符号 来源
Component lfx.custom.custom_component.component.Component
Component.build()(在子类上声明) 每个被加载 Bundle 模块的调用点
Component.inputs 声明式输入列表
Component.outputs 声明式输出列表
Component.display_name / description / icon / documentation 调色板(palette)读取的元数据
Component.name 对注册表中类名的可选覆盖

Bundle 组件即一个 Component 子类:用 inputs/outputs 声明式描述端口,用 build() 组装运行逻辑,用 display_name 等元数据驱动前端渲染。校验器对"直接继承根 Component"的子类要求内联声明 buildoutputs(AST 检查),而对派生自 derived 基类(如 LCVectorStoreComponent / LCToolComponent)的子类放宽了这一要求——因为它们继承类级 outputs 声明、仅覆写输出方法,跨模块的 AST 检查无法解析(见下文 Changelog 部分)。

2. Inputs 与 Outputs

符号 来源
Input(基类) lfx.io
MessageTextInput / MultilineInput / SecretStrInput lfx.io
IntInput / FloatInput / BoolInput lfx.io
DropdownInput / TabInput lfx.io
DictInput / NestedDictInput lfx.io
FileInput / LinkInput lfx.io
HandleInput lfx.io
Output lfx.io

这些是组件参数面板的全部类型化端口。Bundle 作者只能在这套词汇表内声明输入输出;新增端口类型属于契约面变更。

3. Schema 类型

符号 来源
Data lfx.schema.data
DataFrame lfx.schema.dataframe
Message lfx.schema.message

这三者是组件间传递的核心数据类型,也是保存/反序列化流程(saved flow)时 payload 的形态基础。

4. 懒加载导入助手

符号 来源
import_mod(attr_name, module_name, package) lfx.utils.lazy_import(规范位置;lfx.components._importing 向后兼容地再导出)

Bundle 包的 __init__.py 通常采用基于 __getattr__ 的懒导出形态:import_mod 是该形态的运行时支撑。lfx/utils/lazy_import.py 中的实现先尝试 import_module(f".{attr_name}", package=package),失败后再退化为导入 module_name 子模块——Changelog 中专门有一条"import_mod 提升为稳定公共位置":它从内部的 lfx.components._importing 迁移到 lfx.utils.lazy_import 并进入契约面(名称、签名、语义契约稳定),旧路径不变地再导出,属于纯增量变更。

5. Manifest 契约(由加载器消费)

符号 来源
Manifest schema(extension.json / [tool.langflow.extension] lfx.extension.manifest.ExtensionManifest
BundleRef(可选 bundles[] 的一个条目;Bundle 名必须唯一) lfx.extension.manifest.BundleRef
ProviderManifestEntry(可选 providers[] 的一个条目) lfx.extension.manifest.ProviderManifestEntry
LfxCompat(声明为 manifest.lfx lfx.extension.manifest.LfxCompat
BUNDLE_API_VERSION(本 lfx 携带的整数) lfx.extension.manifest
EXTENSION_SCHEMA_URL / SCHEMA_VERSION lfx.extension.manifest

Slot 词汇表official(pip 安装的分发与种子目录)与 extraLANGFLOW_COMPONENTS_PATH 声明的路径)。运行期组件 ID 形如 ext:<bundle>:<Class>@<slot>。这一点在 loader/_types.py 中得到印证:

SLOT_OFFICIAL: Literal["official"] = "official"
SLOT_EXTRA: Literal["extra"] = "extra"

而 Bundle 名采用 snake_case 的原因(manifest.py 源码注释)正是为了能不加引号地编入上述注册表地址:BUNDLE_NAME_RE = ^[a-z][a-z0-9_]{1,63}$

6. 发现与加载入口

符号 来源
load_extension(root, *, bundle_name=None) lfx.extension.loader
load_extension_bundles(root) lfx.extension.loader
load_installed_extensions() lfx.extension.loader
discover_inline_bundles() lfx.extension.loader
discover_installed_extensions() / discover_seed_extensions() / discover_all_extensions() lfx.extension.discovery
LoadedComponent lfx.extension.loader(frozen dataclass,注册表实际存储的内容)
LoadResult lfx.extension.loader
SLOT_OFFICIAL / SLOT_EXTRA lfx.extension.loader

加载器内部按职责拆分为 loader 子包_orchestrator.py_discovery.py_detection.py_startup.py 等),其中 load_installed_extensions / load_seed_extensions 位于 loader/_startup.py——Changelog 将其列为"internal-only 文件拆分",但两个符号都从原导入路径再导出,外部导入路径不受影响。load_extension(..., bundle_name=...) 允许调用方在单扩展内选择某一个 Bundle;启动发现则通过 load_extension_bundles() 加载声明的全部 Bundle。

7. 热重载(Reload)管线

符号 来源
reload_bundle(registry, bundle_name) lfx.extension.reload
BundleRegistry lfx.extension.bundle_registry
BundleRecord lfx.extension.bundle_registry
ReloadInProgressError lfx.extension.bundle_registry
POST /api/v1/extensions/{id}/bundles/{name}/reload langflow.api.v1.extensions

extension/reload.pyreload_bundle 是替换单个 Bundle 的唯一入口;其结果类型 ReloadResult 携带 okbundlereload_idcomponents_added/removed/changedwarningserrors 等字段(这些字段名在下文"事件 payload 对齐"一节再次出现,因为它们同时是轮询客户端可见的线协议)。

8. 类型化错误

符号 来源
ExtensionError lfx.extension.errors
ExtensionErrorCollection lfx.extension.errors
format_extension_error(error) lfx.extension.errors
ERROR_CODES(全部类型化 code 的 frozenset) lfx.extension.errors

完整的小写短横线(kebab-case)判别码集合本身就是契约:新增一个 code 是向后兼容的;删除或重命名一个 code 是破坏性变更,必须递增 BUNDLE_API_VERSIONerrors.py 中的 ERROR_CODES 实际收录了按子系统分组的完整集合,例如:

  • Schema/manifest 发现类:manifest-not-foundmanifest-invalidmanifest-unreadablefield-deferred-in-this-milestonemulti-bundle-unsupported(含旧码 multi-bundle-deferred-in-this-milestone 的弃用别名);
  • Validate 类:path-escapebundle-emptysyntax-errorno-component-subclassbuild-method-missingversion-constraint-unsatisfied 等;
  • Loader 类:module-import-failedoptional-dependency-missingduplicate-component-nameduplicate-distributionbundle-shadowedseed-bundle-shadowedduplicate-bundle-name 等;
  • Reload 类:reload-in-progressreload-source-missingreload-bundle-not-installedreload-class-retag-failedreload-manifestless-unsupportedextension-reload-disabled 等;
  • Migration 类:component-not-found-with-hintcomponent-name-ambiguous 等;
  • Provider 注册类(warning-only):provider-invalidprovider-skipped

format_extension_error 是把错误渲染成人类可读字符串的唯一位置——整个扩展系统里没有任何其他代码格式化错误文本,这既保证快照测试稳定,也给下游消费者提供了错误渲染的单一事实源。

9. 校验 / 编写 CLI

符号 来源
validate_extension(root, *, execute_imports=False) lfx.extension.validate
ValidateReport lfx.extension.validate
lfx extension validate(CLI) lfx.cli._extension_commands
lfx extension schema(CLI) lfx.cli._extension_commands
lfx extension init(CLI) lfx.cli._extension_commands
lfx extension dev(CLI,注册本地路径并 exec langflow run lfx.cli._extension_commands
lfx extension list(CLI) lfx.cli._extension_commands
lfx extension reload(CLI) lfx.cli._extension_commands
register_dev_extension / unregister_dev_extension(Python API) lfx.extension.dev_registry

这些命令面都实现在 lfx/cli/_extension_commands.pylfx extension init 背后是 init_template.py 脚手架:写入单 Bundle 的 basic 模板(extension.json + README.md + components/ 下一个最小 Component 子类 + 一个 pytest 测试 + .gitignore)。生成结果保证"零错误通过 manifest 校验器",且输出对给定 InitOptions 完全确定(有快照测试)。fullserviceroutemulti-bundlestarter-projects 等模板被显式拒绝,错误码 template-deferred-in-this-milestone——脚手架直接复用运行时的 _EXTENSION_ID_RE / BUNDLE_NAME_RE 正则,避免规则漂移。

10. 迁移(Migration)

符号 来源
Migration-table 文件 src/lfx/src/lfx/extension/migration/migration_table.json
MigrationEntry lfx.extension.migration.schema
MigrationTable lfx.extension.migration.schema
migrate_flow_payload(payload, table) lfx.extension.migration.rewrite
MIGRATION_SCHEMA_VERSION lfx.extension.migration.schema

迁移机制解决的是"Bundle 从核心 lfx.components 毕业外迁后,旧保存的流程还能否被反序列化"的问题。MigrationTable.ambiguous_bare_names 字段登记"同名类存在于 2 个以上 Bundle"的裸类名(每条为 {name, candidates: [canonical IDs]});反序列化器对其中任何裸名会报 component-name-ambiguous(附带候选目标),而不是落入泛化的 component-not-found-with-hint。种子数据即经典的回归用例:MergeDataComponentSplitTextComponentSubFlowComponent

配套的 CI 守卫(都在 scripts/migrate/ 下):

  • check_bare_names.py:验证 2+ 个 Bundle 文件夹中出现的每个 Component 类都有对应 marker,未来任何 Bundle 搬移引入的新歧义会在 PR 时被抓到;
  • check_migration_append_only.py:迁移表是只追加的——ambiguous_bare_names marker 一经发布不可删除,candidates 列表只可增长不可收缩(收缩会让流程从 component-name-ambiguous 静默退化为 component-not-found-with-hint);
  • check_router_trust.py:router 信任守卫,用 AST 跨文件解析(支持 include_router(child.api.router, prefix=".../extensions...") 的点号属性链、from M import Nimport M [as alias]、以及 __init__.py 感知的相对导入)扫描所有挂载 /extensions 前缀路由的文件,禁止其中出现安装/卸载/注册表变更类处理器。非字面量前缀的文件可用 # router-trust: in-scope marker 显式纳入。

Manifest 契约的字段级细节

BUNDLE_API.md 将 Manifest 契约收敛到 ExtensionManifest 等模型;阅读 manifest.py 可以得到字段级的完整约束。

两种等价的声明形态

load_manifest(root)manifest.py)的发现顺序是:extension.json,再 pyproject.toml 中的 [tool.langflow.extension]。两者同时存在时 extension.json 获胜——这样作者可以把 $schema 指针只放在 JSON 里获得编辑器自动补全,而不用在 pyproject 中重复。返回值是 ManifestSource(manifest + 来源路径 + kind),保证错误能精确归因到用户实际编辑的那个文件。$schema 指向由常量 EXTENSION_SCHEMA_URL = f"https://schemas.langflow.org/extension/v{SCHEMA_VERSION}.json" 给出的规范 URL(manifest.py)。

ExtensionManifest 必填与可选字段

字段 约束 说明
id 正则 ^[a-z][a-z0-9-]{1,63}$,必填 全局唯一扩展 ID:小写、连字符、字母开头、2–64 字符,镜像 PyPI/npm 归一化规则
version SemVer 2.0.0 正则,必填 该扩展发布的版本号
name 1–200 字符,必填 Langflow 中展示的人类可读名称
description 至多 2000 字符,可选 摘要
lfx LfxCompat,必填 与 BUNDLE_API.md 契约的兼容性声明
bundles list[BundleRef],可为空 组件 Bundle 组;provider-only 扩展可为空
providers list[ProviderManifestEntry],可为空 模型 provider 贡献
capabilities 默认 Capabilities() v0 仅一个槽位 requiresCredentials: bool(默认 false
$schema 可选 编辑器工具用的 JSON-Schema URL 指针

关键模型级校验(manifest.py):

  1. 必须贡献至少一样东西bundlesproviders 全空直接报错——"An extension must declare at least one bundle or one provider";
  2. Bundle 名扩展内唯一Provider 名扩展内唯一
  3. 整个模型 extra="forbid",未知字段一律拒绝而非静默忽略。

BundleRef:指向 Bundle 目录的指针

BundleRef 字段:

  • name:匹配 BUNDLE_NAME_RE^[a-z][a-z0-9_]{1,63}$),可被编址为 ext:<name>:<Class>@<slot>
  • path:相对 Manifest 父目录、必须留在其内部;模型层只做语法检查(拒绝绝对路径与 ..),更彻底的 symlink-aware 检查由 validate_extension 在有文件系统访问时执行;
  • display_name(可选,≤120 字符):侧栏标题,省略时 UI 从 name 人话化(my_bundleMy Bundle);
  • icon(可选,≤64 字符):Lucide 图标名,省略时回退到通用 Package / folder 图标。

ProviderManifestEntry:Bundle 注册模型 provider

providers[] 条目把 provider 元数据合并进 lfx 的统一模型系统。字段包括:

  • name(规范名)、provider_id(稳定的机器身份,正则 ^[a-z0-9][a-z0-9._-]*$;省略时由 name 确定性派生,旧 Manifest 行为不变)、display_namealiases(解析稳定身份时接受的旧名,须非空且不重复);
  • metadata:镜像一条 MODEL_PROVIDER_METADATA 值(iconvariables、含非空 model_classmappingapi_docs_url 等),缺 mapping.model_class 会在模型校验阶段报错;
  • model_class / embedding(module, attr, install_hint) 形式的惰性类导入指针——provider 的 LangChain 类只在实例化时导入,绝不在发现阶段导入;
  • api_key_required(默认 true)、live(加入 LIVE_MODEL_PROVIDERS)、conditional_live(仅在配置了自定义端点时 live);liveconditional_live 互斥,同时为 true 直接校验失败;
  • live_discovery / validator / catalog_loader:点号路径的可调用对象(分别为 live 模型发现、凭据校验、静态模型目录行)。

加载时内置 provider 在命名冲突上永远获胜;畸形 spec 报 provider-invalid、与已加载 provider 撞名报 provider-skipped,两者均为 warning-only——扩展其余部分照常加载。

范围外(Out of scope)的保留字段

v0 中以下字段在 Manifest schema 里保留:一旦设置即产生类型化 field-deferred-in-this-milestone 错误,不属于 v0 契约:

  • services — Bundle 声明的服务工厂
  • routes — Bundle 挂载的 HTTP 路由
  • hooks — Bundle 声明的生命周期钩子
  • starter_projects — Bundle 附带的起始流程
  • userConfig — Bundle 声明的用户配置 schema
  • 多 Bundle Manifest(bundles 长度 > 1 的语义限制)

manifest.py 中它们被建模为 None-only 字段(servicesrouteshooksstarterProjectsuserConfig),以便区分"不存在"与"显式设置了一个当前里程碑不支持的值"。注意 bundles[] 本身现在支持多条目(见 Changelog),与保留的"多 Bundle 语义限制"是两回事;真正的 v0 限制体现在发现/加载路径与 CLI 能力上。

试点 Bundle:lfx-duckduckgo 的完整落地

文档指定的 LE-1023 试点是 duckduckgo,被抽成独立分发 lfx-duckduckgo(位于 src/bundles/duckduckgo/),拥有自己的 pyproject.tomllangflow 主包的 pyproject.toml 声明 lfx-duckduckgo>=0.1.0 为常规依赖,使扁平的 pip install langflow 依然像以前一样携带该 Bundle。

选择它的理由(BUNDLE_API.md):

  • 单组件(DuckDuckGoSearchComponent)单文件(duck_duck_go_search_run.py);
  • 过去六个月零 git 变更;
  • 现代 Component 基类(无 LCToolComponent 遗留);
  • 无需认证——失败模式是单个请求失败,而非付费 API 中断;
  • 类名在 src/lfx/src/lfx/components/** 中全局唯一,因此裸名(bare-name)迁移条目被 check_bare_names.py 放行。

实际的分发声明印证了契约要求,src/bundles/duckduckgo/pyproject.toml

[project]
name = "lfx-duckduckgo"
dependencies = [
    "lfx>=1.12.0.dev0,<2.0.0",
    "langchain-community>=0.4.1,<1.0.0",
    "ddgs>=9.0.0",
]

# Manifest-shipping distributions are discovered via the
# ``langflow.extensions`` entry-point.
[project.entry-points."langflow.extensions"]
lfx-duckduckgo = "lfx_duckduckgo"

[tool.hatch.build.targets.wheel]
packages = ["src/lfx_duckduckgo"]
include = ["src/lfx_duckduckgo/extension.json", "src/lfx_duckduckgo/components/**/*.py"]

两个要点:其一,lfx 依赖用 >=1.10 线、<2.0.0 的大版本区间做粗粒度兼容,细粒度的 BUNDLE_API 兼容性由 Manifest 的 lfx.compat 契约单独强制;其二,带 Manifest 的分发通过 langflow.extensions entry-point 组被发现,其值是包含 extension.json 的包点号路径——这正是 Changelog 中"editable 安装通过 entry-point 回退被发现"所针对的路径(wheel 安装走 dist.files 主扫描,editable 安装才需要回退到 entry-point + importlib.util.find_spec 定位包目录)。

配套的 extension.jsonsrc/bundles/duckduckgo/src/lfx_duckduckgo/extension.json)是最小完整契约实例:

{
  "$schema": "https://schemas.langflow.org/extension/v1.json",
  "id": "lfx-duckduckgo",
  "version": "0.1.3",
  "name": "DuckDuckGo Search",
  "description": "DuckDuckGo Search component as a standalone Langflow Extension Bundle.",
  "lfx": { "compat": ["1"] },
  "bundles": [
    { "name": "duckduckgo", "path": "components/duckduckgo" }
  ]
}

M1 交付门禁的运行时半边("在迁移前的 Langflow 保存流程 → 升级 → 确认它既加载又行为一致地运行")的 dogfood 清单位于 src/bundles/duckduckgo/M1_DOGFOOD_CHECKLIST.md;反序列化半边由 src/lfx/tests/integration/extension/test_pilot_duckduckgo_upgrade.py 覆盖。

无 Manifest 的 lfx.bundles 元包发现(metapackage 模型)

这是 Changelog 中最重量级的增量特性之一,理解它需要把发现优先级连起来看:

  • 一个分发包可以声明 [project.entry-points."lfx.bundles"],其值是可导入的包;该包的每个直接子目录都被当作一个 @official 槽位的 Bundle 加载,且没有 extension.json(langchain-community 模型)。这些目录不是 lfx extension validate 的输入——校验器要求存在 Manifest,缺了会报 manifest-not-found
  • load_lfx_bundles_extensionslfx.extension 导出(增量符号)。
  • 跨源 Bundle 名冲突的启动期优先级为:installed > seed > lfx_bundles > dev > inline——Manifest 永远获胜,因此带 Manifest 的 lfx-<provider> 会以既有的 bundle-shadowed warning 遮蔽元包中同名的 provider(毕业流程无需 lockstep 发版)。名字已被 installed/seed 源占据的元包 provider 绝不会被导入——所有 @official 源共享 _lfx_ext.official.<bundle>.*sys.modules 命名空间,导入失败方会覆盖获胜方的活模块;被跳过的副本携带同一类型化诊断(在 errors 上,与 resolver 一致)。
  • 命名空间包会跨所有 portions 遍历,解析出的根按路径去重,重复声明不会自我遮蔽。
  • 新增四个 warning-only code(均不中断启动):bundle-discovery-malformed(声明解析不到可导入包目录,包括父包 __init__find_spec 时抛错)、bundles-provider-name-invalidbundles-root-unreadableduplicate-lfx-bundles-provider(同名 provider 出现在多个根,first wins)。
  • 无 Manifest 的 Bundle 按构造绕过 version-constraint-unsatisfied 的 API 版本门禁(没有 Manifest 可承载 lfx.compat),安装期兼容改由元包的 PEP 508 lfx>=X,<Y 区间承载。
  • 无 Manifest 记录以 manifestless=TrueLoadResult/BundleRecord 的增量字段)注册,且不可热重载:reload 管线以新类型化 code reload-manifestless-unsupported 拒绝它们,元包变更通过升级分发并重启进程生效。
  • 列表时不可见:该优先级是启动期属性——load_lfx_bundles_extensions 运行在组件加载路径而非 discover_all_extensions,所以 lfx extension list 不枚举无 Manifest 的元包 provider;它与带 Manifest 包的遮蔽只在服务器启动组装命名空间时解决。

行为收紧要点:v0 契约面下的"代码审查加固"

BUNDLE_API.md 的 v0 Changelog 中有一条跨扩展子系统的代码审查加固记录——没有任何公共符号的名称或签名变化,但若干行为收紧对 Bundle 作者与运维都重要。以下是最具实操价值的部分:

  1. 路径安全契约在每条发现路径上生效。 discover_installed_extensions / discover_seed_extensions 产出的 DiscoveredExtension 记录现在执行与 validate_extension 相同的 resolve + relative_to 包含检查;symbolic link 的 bundles[0].path 或种子子目录若逃逸扩展根,会在到达 exec_module() 之前被 path-escape 拒绝。共享原语位于 lfx.extension._paths.is_within,所有 walker(loader、validator、种子发现、inline Bundle 发现)使用同一函数与同一 SKIP_DIR_NAMES
  2. --execute-imports 环境变量白名单。 校验器的 --execute-imports 子进程现在继承显式白名单(PATHLANGLC_*SYSTEMROOTTMPDIRTZ、Python locale + 编码变量),而非只黑名单 LANGFLOW_*/LFX_*——云/CI 凭据(AWS_*OPENAI_API_KEY 等)不再泄漏进不受信任的 Bundle 导入。该 pass 被明确定位为 best-effort 卫生 lint,不是沙箱。
  3. AST 卫生 lint 扩面。 _find_top_level_io 现在把 execeval__import__compile 标记为顶层原语,把 importlib.import_module / importlib.__import__ 标记为点号名原语;仍是字面量名匹配,混淆可轻易绕过(文档如实说明)。
  4. Reload 交换非破坏性。 _swap_sys_modules 在任何 sys.modules 变更之前构建 staging→prod 重命名映射,把弹出的旧模块快照进恢复 map,中途异常时恢复;zip(strict=True) 长度失配触发器不再把 prod 命名空间撕碎。cls.__module__ 无法重新打标时,reload-class-retag-failed 追加到 ReloadResult.warnings,"重载后调色板为空"的回归会留下痕迹。
  5. 跨源 Bundle 名碰撞。 load_installed_extensions 检测"规范名不同但 bundle.name 相同"的两个分发(会在 _lfx_ext.official.<name>.* 下互相静默覆盖),对失败方报类型化 duplicate-bundle-name 并丢弃其组件;BundleRegistry.install_bundle 在被不同 source_path 的记录替换现有记录时额外记 WARNING。
  6. Reload 端点移出事件循环。 POST /api/v1/extensions/{id}/bundles/{name}/reload 现通过 asyncio.to_thread 调用 reload_bundle,慢的大 Bundle 导入不再冻结 worker 上的其他在途请求;线协议(状态码、body 形态)不变。
  7. 用户级作用域的事件。 Bundle 生命周期事件(bundle_reloadedbundle_reload_failedflow_migratedextension_error)发布到每用户 keyspace(user:<user_id>)而非共享 "global" 桶。reload_bundle 增加关键字参数 user_id: str | None = NoneNone 保持旧的 global 发布,CLI/无认证开发不受影响);HTTP 触发的 reload 会把认证用户 id 传入。GET /api/v1/extensions/events 不再接受客户端提供的 keyspace 查询参数,显式传递者收到 422 + 类型化 extension-events-keyspace-forbidden 错误(该 code 增量加入 ERROR_CODES);从不下发该参数的树内轮询客户端不受影响。
  8. 事件 payload 与 ReloadResult 对齐。 两种 reload 事件现在携带完整 ReloadResult.to_dict() 信封(okbundlereload_idcomponents_addedcomponents_removedcomponents_changedwarningserrors),轮询客户端可以据 components_changed 区分"仅 body 变更",并以 errors[0].message 替代"检查服务器日志"的泛化兜底。
  9. reload 端点的结构性失败返回 422(损坏的 Bundle、缺失源路径、名不匹配),而非 ok=false200 OK;body 为 {...primaryError, result: ReloadResult},完整类型化结果保留在 FastAPI detail 信封内;reload-in-progress409 Conflict 不变。
  10. duplicate-distribution 先解析符号链接再判重。 RHEL 系(ubi)venv 将 lib64 -> lib 做符号链接并使两种拼写同时出现在 sys.pathimportlib.metadata.distributions() 会把每个分发吐两遍——现在通过 Path.resolve() 折叠同一物理文件的不同拼写,消除 Docker 启动时的假阳性;字典序最前的未解析拼写仍为获胜方,消息与选择行为不变。
  11. Dev 注册表状态文件以 0600 写入,且区分"文件缺失(静默空注册表)/存在但不可读(WARNING)/存在但 JSON 损坏(带详情的 WARNING)"。
  12. 可编辑安装的发现回退lfx extension reload--bundle 可选 + --all 实现:省略 --bundle 时从本地 discover_all_extensions 解析 Bundle 名(显式 --bundle 仍然优先);--all 遍历每个本地发现的 Bundle 逐个 POST reload,任一失败即非零退出,且与位置 id/--bundle 互斥(exit 2)。

实操路径:按契约开发一个 Extension Bundle

把上述契约落到实操,一条可复现的链路是(命令面见 lfx/cli/_extension_commands.py):

  1. 脚手架lfx extension init <dir> 生成 basic 模板(extension.json$schema 指针、components/ 下的最小组件、pytest 测试)。生成物必须零错误通过校验器——这本身被"init 后立即 validate"的测试所验证。
  2. 编写组件:继承 lfx.custom.custom_component.component.Component,用 lfx.io 的 Input/Output 声明端口,实现 build();Bundle 包 __init__.pylfx.utils.lazy_import.import_mod 做懒导出。
  3. 声明 Manifest:写 extension.json(或 [tool.langflow.extension]),id 小写连字符、version SemVer、lfx.compat: ["1"]bundles[] 指向 snake_case 命名且路径在扩展根内的组件目录;如需 provider 则填 providers[] 并保证 metadata.mapping.model_class 非空。
  4. 本地校验lfx extension validate <dir>(可加 --execute-imports 做导入级检查,注意其 env 白名单语义);lfx extension schema 打印 schema 供编辑器使用。
  5. 本地开发联调lfx extension dev <path> 注册本地路径并 exec langflow run;程序化等价物是 register_dev_extension / unregister_dev_extensionextension/dev_registry.py)。
  6. 发布与热重载:打 wheel 发布后,lfx extension list 查看已装扩展;运行时改动源码后 lfx extension reload <ext_id>(或 --all)触发 POST /api/v1/extensions/{id}/bundles/{name}/reload,按 200/409/422 状态码与 ReloadResult 判断结果。
  7. 变更契约时:修改 in-scope 表面必须同步更新 BUNDLE_API_VERSION(破坏性)与 BUNDLE_API.md## Changelog,否则被 check_bundle_api_changelog.py 拦截。

小结

BUNDLE_API.md 定义的不是"API 列表",而是一份可被 CI 强制执行的软件契约:符号级表面 + 整数版本 + Changelog 门禁 + 只追加的迁移表 + 类型化错误码集合,四者互相咬合。对 Bundle 作者而言,契约面就是 Componentlfx.io、Schema 类型、import_mod、Manifest 模型与加载/重载/校验/迁移这十组符号;对平台侧而言,official/extra 双槽位注册表、_lfx_ext.official.<bundle>.* 命名空间与优先级链(installed > seed > lfx_bundles > dev > inline)共同保证了多源 Bundle 在启动期与热重载期的确定性。试点分发 lfx-duckduckgo 则给出了从 pyproject.toml entry-point、extension.json 到 M1 dogfood 与反序列化集成测试的完整参照实现。

登录后查看全文
热门项目推荐
相关项目推荐