Langflow BUNDLE API 契约详解:Extension Bundle 的稳定 API 表面、Manifest 规范与热重载管线
本文基于仓库根目录的 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"的子类要求内联声明 build 或 outputs(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 安装的分发与种子目录)与 extra(LANGFLOW_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.py 中 reload_bundle 是替换单个 Bundle 的唯一入口;其结果类型 ReloadResult 携带 ok、bundle、reload_id、components_added/removed/changed、warnings、errors 等字段(这些字段名在下文"事件 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_VERSION。errors.py 中的 ERROR_CODES 实际收录了按子系统分组的完整集合,例如:
- Schema/manifest 发现类:
manifest-not-found、manifest-invalid、manifest-unreadable、field-deferred-in-this-milestone、multi-bundle-unsupported(含旧码multi-bundle-deferred-in-this-milestone的弃用别名); - Validate 类:
path-escape、bundle-empty、syntax-error、no-component-subclass、build-method-missing、version-constraint-unsatisfied等; - Loader 类:
module-import-failed、optional-dependency-missing、duplicate-component-name、duplicate-distribution、bundle-shadowed、seed-bundle-shadowed、duplicate-bundle-name等; - Reload 类:
reload-in-progress、reload-source-missing、reload-bundle-not-installed、reload-class-retag-failed、reload-manifestless-unsupported、extension-reload-disabled等; - Migration 类:
component-not-found-with-hint、component-name-ambiguous等; - Provider 注册类(warning-only):
provider-invalid、provider-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.py。lfx extension init 背后是 init_template.py 脚手架:写入单 Bundle 的 basic 模板(extension.json + README.md + components/ 下一个最小 Component 子类 + 一个 pytest 测试 + .gitignore)。生成结果保证"零错误通过 manifest 校验器",且输出对给定 InitOptions 完全确定(有快照测试)。full、service、route、multi-bundle、starter-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。种子数据即经典的回归用例:MergeDataComponent、SplitTextComponent、SubFlowComponent。
配套的 CI 守卫(都在 scripts/migrate/ 下):
- check_bare_names.py:验证 2+ 个 Bundle 文件夹中出现的每个 Component 类都有对应 marker,未来任何 Bundle 搬移引入的新歧义会在 PR 时被抓到;
- check_migration_append_only.py:迁移表是只追加的——
ambiguous_bare_namesmarker 一经发布不可删除,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 N与import M [as alias]、以及__init__.py感知的相对导入)扫描所有挂载/extensions前缀路由的文件,禁止其中出现安装/卸载/注册表变更类处理器。非字面量前缀的文件可用# router-trust: in-scopemarker 显式纳入。
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):
- 必须贡献至少一样东西:
bundles与providers全空直接报错——"An extension must declare at least one bundle or one provider"; - Bundle 名扩展内唯一、Provider 名扩展内唯一;
- 整个模型
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_bundle→My Bundle);icon(可选,≤64 字符):Lucide 图标名,省略时回退到通用Package/folder图标。
ProviderManifestEntry:Bundle 注册模型 provider
providers[] 条目把 provider 元数据合并进 lfx 的统一模型系统。字段包括:
name(规范名)、provider_id(稳定的机器身份,正则^[a-z0-9][a-z0-9._-]*$;省略时由name确定性派生,旧 Manifest 行为不变)、display_name、aliases(解析稳定身份时接受的旧名,须非空且不重复);metadata:镜像一条MODEL_PROVIDER_METADATA值(icon、variables、含非空model_class的mapping、api_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);live与conditional_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 字段(services、routes、hooks、starterProjects、userConfig),以便区分"不存在"与"显式设置了一个当前里程碑不支持的值"。注意 bundles[] 本身现在支持多条目(见 Changelog),与保留的"多 Bundle 语义限制"是两回事;真正的 v0 限制体现在发现/加载路径与 CLI 能力上。
试点 Bundle:lfx-duckduckgo 的完整落地
文档指定的 LE-1023 试点是 duckduckgo,被抽成独立分发 lfx-duckduckgo(位于 src/bundles/duckduckgo/),拥有自己的 pyproject.toml。langflow 主包的 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.json(src/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_extensions从lfx.extension导出(增量符号)。- 跨源 Bundle 名冲突的启动期优先级为:
installed > seed > lfx_bundles > dev > inline——Manifest 永远获胜,因此带 Manifest 的lfx-<provider>会以既有的bundle-shadowedwarning 遮蔽元包中同名的 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-invalid、bundles-root-unreadable、duplicate-lfx-bundles-provider(同名 provider 出现在多个根,first wins)。 - 无 Manifest 的 Bundle 按构造绕过
version-constraint-unsatisfied的 API 版本门禁(没有 Manifest 可承载lfx.compat),安装期兼容改由元包的 PEP 508lfx>=X,<Y区间承载。 - 无 Manifest 记录以
manifestless=True(LoadResult/BundleRecord的增量字段)注册,且不可热重载:reload 管线以新类型化 codereload-manifestless-unsupported拒绝它们,元包变更通过升级分发并重启进程生效。 - 列表时不可见:该优先级是启动期属性——
load_lfx_bundles_extensions运行在组件加载路径而非discover_all_extensions,所以lfx extension list不枚举无 Manifest 的元包 provider;它与带 Manifest 包的遮蔽只在服务器启动组装命名空间时解决。
行为收紧要点:v0 契约面下的"代码审查加固"
BUNDLE_API.md 的 v0 Changelog 中有一条跨扩展子系统的代码审查加固记录——没有任何公共符号的名称或签名变化,但若干行为收紧对 Bundle 作者与运维都重要。以下是最具实操价值的部分:
- 路径安全契约在每条发现路径上生效。
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。 --execute-imports环境变量白名单。 校验器的--execute-imports子进程现在继承显式白名单(PATH、LANG、LC_*、SYSTEMROOT、TMPDIR、TZ、Python locale + 编码变量),而非只黑名单LANGFLOW_*/LFX_*——云/CI 凭据(AWS_*、OPENAI_API_KEY等)不再泄漏进不受信任的 Bundle 导入。该 pass 被明确定位为 best-effort 卫生 lint,不是沙箱。- AST 卫生 lint 扩面。
_find_top_level_io现在把exec、eval、__import__、compile标记为顶层原语,把importlib.import_module/importlib.__import__标记为点号名原语;仍是字面量名匹配,混淆可轻易绕过(文档如实说明)。 - Reload 交换非破坏性。
_swap_sys_modules在任何sys.modules变更之前构建 staging→prod 重命名映射,把弹出的旧模块快照进恢复 map,中途异常时恢复;zip(strict=True)长度失配触发器不再把 prod 命名空间撕碎。cls.__module__无法重新打标时,reload-class-retag-failed追加到ReloadResult.warnings,"重载后调色板为空"的回归会留下痕迹。 - 跨源 Bundle 名碰撞。
load_installed_extensions检测"规范名不同但bundle.name相同"的两个分发(会在_lfx_ext.official.<name>.*下互相静默覆盖),对失败方报类型化duplicate-bundle-name并丢弃其组件;BundleRegistry.install_bundle在被不同source_path的记录替换现有记录时额外记 WARNING。 - Reload 端点移出事件循环。
POST /api/v1/extensions/{id}/bundles/{name}/reload现通过asyncio.to_thread调用reload_bundle,慢的大 Bundle 导入不再冻结 worker 上的其他在途请求;线协议(状态码、body 形态)不变。 - 用户级作用域的事件。 Bundle 生命周期事件(
bundle_reloaded、bundle_reload_failed、flow_migrated、extension_error)发布到每用户 keyspace(user:<user_id>)而非共享"global"桶。reload_bundle增加关键字参数user_id: str | None = None(None保持旧的global发布,CLI/无认证开发不受影响);HTTP 触发的 reload 会把认证用户 id 传入。GET /api/v1/extensions/events不再接受客户端提供的keyspace查询参数,显式传递者收到422+ 类型化extension-events-keyspace-forbidden错误(该 code 增量加入ERROR_CODES);从不下发该参数的树内轮询客户端不受影响。 - 事件 payload 与
ReloadResult对齐。 两种 reload 事件现在携带完整ReloadResult.to_dict()信封(ok、bundle、reload_id、components_added、components_removed、components_changed、warnings、errors),轮询客户端可以据components_changed区分"仅 body 变更",并以errors[0].message替代"检查服务器日志"的泛化兜底。 - reload 端点的结构性失败返回
422(损坏的 Bundle、缺失源路径、名不匹配),而非ok=false的200 OK;body 为{...primaryError, result: ReloadResult},完整类型化结果保留在 FastAPIdetail信封内;reload-in-progress的409 Conflict不变。 duplicate-distribution先解析符号链接再判重。 RHEL 系(ubi)venv 将lib64 -> lib做符号链接并使两种拼写同时出现在sys.path,importlib.metadata.distributions()会把每个分发吐两遍——现在通过Path.resolve()折叠同一物理文件的不同拼写,消除 Docker 启动时的假阳性;字典序最前的未解析拼写仍为获胜方,消息与选择行为不变。- Dev 注册表状态文件以 0600 写入,且区分"文件缺失(静默空注册表)/存在但不可读(WARNING)/存在但 JSON 损坏(带详情的 WARNING)"。
- 可编辑安装的发现回退与
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):
- 脚手架:
lfx extension init <dir>生成 basic 模板(extension.json带$schema指针、components/下的最小组件、pytest 测试)。生成物必须零错误通过校验器——这本身被"init 后立即 validate"的测试所验证。 - 编写组件:继承
lfx.custom.custom_component.component.Component,用lfx.io的 Input/Output 声明端口,实现build();Bundle 包__init__.py用lfx.utils.lazy_import.import_mod做懒导出。 - 声明 Manifest:写
extension.json(或[tool.langflow.extension]),id小写连字符、versionSemVer、lfx.compat: ["1"]、bundles[]指向 snake_case 命名且路径在扩展根内的组件目录;如需 provider 则填providers[]并保证metadata.mapping.model_class非空。 - 本地校验:
lfx extension validate <dir>(可加--execute-imports做导入级检查,注意其 env 白名单语义);lfx extension schema打印 schema 供编辑器使用。 - 本地开发联调:
lfx extension dev <path>注册本地路径并 execlangflow run;程序化等价物是register_dev_extension/unregister_dev_extension(extension/dev_registry.py)。 - 发布与热重载:打 wheel 发布后,
lfx extension list查看已装扩展;运行时改动源码后lfx extension reload <ext_id>(或--all)触发POST /api/v1/extensions/{id}/bundles/{name}/reload,按 200/409/422 状态码与ReloadResult判断结果。 - 变更契约时:修改 in-scope 表面必须同步更新
BUNDLE_API_VERSION(破坏性)与 BUNDLE_API.md 的## Changelog,否则被 check_bundle_api_changelog.py 拦截。
小结
BUNDLE_API.md 定义的不是"API 列表",而是一份可被 CI 强制执行的软件契约:符号级表面 + 整数版本 + Changelog 门禁 + 只追加的迁移表 + 类型化错误码集合,四者互相咬合。对 Bundle 作者而言,契约面就是 Component、lfx.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 与反序列化集成测试的完整参照实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00