跳到主要内容

数据截至 (上游 commit 7a975c596eca)

组件模型 — 一个 Python 类如何长成画布上的节点

30 秒导读: Langflow 画布上的每个节点,背后都是一个继承 Component 的 Python 类。类里只做三件事:用 inputs 列表声明"我要什么"、用 outputs 列表声明"我给什么"、写几个普通方法真正干活。节点的表单长什么样、有几个连接桩、桩上允许接什么类型——全部由这个类单向生成,人不用碰 JSON。

本章只讲单个组件:它在代码里是什么、输入输出怎么声明、值什么时候算、以及代码怎么变成 UI。 多个组件怎么连成图、谁先跑、环怎么处理,见 02 / 03 / 04


1. 一个组件长什么样

一句话定义: 组件 = 一个声明式的 Python 类——类属性描述它在 UI 上的样子,实例方法计算它的输出。

下面是仓库里一个真实的、完整的组件,只有 40 行(src/lfx/src/lfx/components/processing/combine_text.py:6-40,CombineTextComponent):

class CombineTextComponent(Component):
display_name = "Combine Text"
description = "Concatenate two text sources into a single text chunk using a specified delimiter."
icon = "merge"
name = "CombineText"

inputs = [
MessageTextInput(name="text1", display_name="First Text", info="..."),
MessageTextInput(name="text2", display_name="Second Text", info="..."),
MessageTextInput(name="delimiter", display_name="Delimiter", value=" "),
]
outputs = [
Output(display_name="Combined Text", name="combined_text", method="combine_texts"),
]

def combine_texts(self) -> Message:
combined = self.delimiter.join([self.text1, self.text2])
self.status = combined
return Message(text=combined)

代码里的每样东西,在画布上都有一个对应物:

代码里写的画布上变成谁负责生成
display_name / icon / description节点标题、图标、悬浮说明get_template_config(base_component.py:88)
inputs = [...]节点里的表单字段 + 左侧输入桩ComponentFrontendNode.from_inputs
outputs = [...]右侧输出桩(每个 Output 一个)to_frontend_node(component.py:1132)
Output(method="combine_texts")这个桩被拉线时要跑哪个函数运行时反射 getattr(self, output.method)
def combine_texts(self) -> Message桩上允许连什么类型(Message)从返回类型注解反推
self.text1上游节点送来的值 / 表单里填的值__getattr__ 魔法(§3.2)

一句话直觉: 把组件类当成一张报名表——它先填好"我叫什么、需要哪些材料、能产出哪些东西",框架照着这张表画出 UI;等真要出结果时,框架再回头按表上写的方法名去叫对应的函数。类本身从不主动执行


2. 顶层全景:一个类的三段人生

怎么读这张图:从左到右是时间顺序。同一个类,先被声明,再被实例化成"活的"组件,然后分叉成两条互不相干的用途——生成 UI,或者算值。

① 声明期(import 时) ② 实例化(__init__) ③ 两条出路
┌────────────────────┐ ┌──────────────────────┐
│ class XxxComponent │ │ 类属性 → 实例副本 │ ┌──► (A) 画 UI
│ inputs = [...] │ ──────►│ map_inputs() │──────┤ to_frontend_node()
│ outputs = [...] │ │ → _inputs{name:obj} │ │ → 一坨节点 JSON
│ def foo(self) │ │ map_outputs() │ │
└────────────────────┘ │ → _outputs_map{} │ └──► (B) 算值
仅仅是模板,人人共享 └──────────────────────┘ build_results()
每个实例一份深拷贝 → 反射调 foo()

三段各自的关键符号:

阶段干了什么入口符号(src/lfx/src/lfx/custom/custom_component/component.py)
声明期什么都不做,inputs/outputs 只是类属性列表Component.inputs / Component.outputs:151-152
实例化深拷贝模板 → 建 _inputs / _outputs_map 两张字典__init__:156、map_inputs:644、map_outputs:611
出路 A(生成 UI)把实例翻译成前端节点 JSONto_frontend_node:1132
出路 B(算值)挑要算的 output,反射调方法_build_results:1294

为什么第一步要深拷贝? inputs = [...] 写在类体里,是所有实例共享的同一批对象。若不拷贝,A 流程里给某个输入赋的值会漏进 B 流程的同名组件。__init__ 一上来就做了(component.py:157-158_copy_component_template,component.py:107),map_inputs / map_outputs 再各自 deepcopy 一次(component.py:661component.py:642)——两层拷贝,一层防类污染,一层防实例间串味


3. 输入:声明就是一个 pydantic 模型

3.1 输入类家族:字段决定 UI 控件

每种输入都是一个 pydantic 模型,继承 BaseInputMixin(src/lfx/src/lfx/inputs/input_mixin.py:60)。所有输入的公共字段就那么十几个:namedisplay_namevaluerequiredshowadvancedinfoinput_typesreal_time_refresh

前端画什么控件,由 field_type 决定;桩上能接什么,由 input_types 决定。 这是两件事:

  • field_type(如 FieldTypes.PASSWORD)→ 渲染成密码框还是滑块还是下拉。
  • input_types(如 ["Message"])→ 上游哪种输出允许连到这个桩上。input_types 为空列表就意味着没有连接桩,只能手填

常用的几种(src/lfx/src/lfx/inputs/inputs.py):

输入类前端长什么样特点
StrInput单行文本最朴素,支持 load_from_db 取全局变量285
MessageTextInput单行文本 + Message 输入桩收到 Message/Data 会自动拆成 str367
MultilineInput多行文本框继承 MessageTextInput450
SecretStrInput密码框password=Trueload_from_db=True、不上报遥测488
DropdownInput下拉/可搜索下拉optionscombobox792
BoolInput / IntInput / FloatInput / SliderInput开关 / 数字 / 滑块数字类带 range_spec664 / 552 / 611 / 976
HandleInput只有桩,没有表单field_type=OTHER,给 LLM、Retriever 这类对象用97
JSONInput(别名 DataInput) / DataFrameInput只有桩input_types=["Data","JSON"] / ["DataFrame","Table"]126 / 143
TableInput可编辑表格is_list=True57
FileInput上传框file_types 限定后缀943

这些类不是各写各的,而是拼 mixin 拼出来的(input_mixin.py):

Mixin加了什么能力
ListableInputMixinis_list → 前端出现 "Add More"224
DatabaseLoadMixinload_from_db → 值从全局变量表取,而不是字面量230
RangeMixin / DropDownMixinrange_spec / options284 / 300
ToolModeMixintool_mode → 该字段允许由 agent 在运行时填211
InputTraceMixin / MetadataTraceMixin追踪时这个字段算"输入"还是"元数据"215 / 219
FileMixinfile_path / file_types244

所有输入类被并成一个 InputTypes 联合类型(inputs.py:995),并由它自动生成 InputTypesMap 名字→类的反查表(inputs.py:1032)——反序列化保存的流时靠它把 "MessageTextInput" 这个字符串还原成类。

3.2 值一进来就被规整

pydantic 的 validate_assignment=True(input_mixin.py:60)意味着每次给 input.value 赋值都会跑校验器。校验器不只是拦错,它还做类型归一

最典型的是 MessageTextInput._validate_value(inputs.py:381-410):你连过来一个 Message,它取 v.text;连过来一个 Data,它按 text_key 取那一项、取不到就报一句"请把 text_key 改成以下之一"的人话错误。所以组件方法里 self.text1 拿到的永远是 str,不用自己判断类型。

is_list 的分发也在这一层:StrInput.validate_value(inputs.py:326-342)先看 info.data["is_list"],是列表就逐项校验。

安全上有一条硬线:Credential 类型的全局变量到达时是 SecretStr,_reject_secret_in_non_password_field(inputs.py:38)会拒绝它落进非密码字段——否则密钥会顺着 Message.text 流到日志和追踪里。

3.3 self.some_input 的魔法

组件方法里写 self.delimiter,类里根本没有这个属性。它靠 Component.__getattr__(component.py:1033-1061)兜底——Python 只在常规属性查找失败后才调 __getattr__,所以这段是纯兜底逻辑,不影响正常属性。

查找顺序是固定的四级,命中即返回:

self.delimiter


① _attributes 里有吗? ──是──► 返回它(运行期的"已定稿值")
│否

② _inputs 里有吗? ──是──► 返回 _inputs["delimiter"].value ← 输入的魔法在这
│否

③ _outputs_map 里有吗?──是──► 返回 Output 对象本身(注意:不是它的值)
│否

④ 名字是 user_id/vertex/tracing_service/graph? ──是──► 返回内部字段 / 占位 Graph
│否

AttributeError("Attribute X not found in ...")

第 ③ 级容易踩:self.some_output 拿到的是 Output 模型对象,不是计算结果。想要结果得走 await self.resolve_output("some_output")(§5.4)。

第 ④ 级里 graph 这条比较妙:组件在没有图的场合(单测、纯代码调用)访问 self.graph 不会崩,而是拿到一个 PlaceholderGraph(component.py:111-147)——一个假的具名元组,get_vertex_neighbors 永远返回空字典。同一份组件代码,既能在图里跑,也能脱离图跑。

那么值是什么时候写进去的?运行时由 set_attributes(params)(component.py:1202-1228)统一灌:

  1. _validate_inputsparams 里的值写进 _inputs[key].value(过一遍 pydantic 校验)。
  2. 再把每个输入的最终值抄一份进 _attributes,密码字段顺手 unwrap_secret_value 解包(_wrap_if_secret,component.py:81-91)、把明文登记进 _secret_values 供事后脱敏。

因为 _attributes 排在查找的第 ①,set_attributes 跑过之后,self.x 读的是 _attributes 的那份快照


4. 输出:名字 + 方法名 + 类型 + 缓存

Output 是个比输入简单得多的 pydantic 模型(src/lfx/src/lfx/template/field/base.py:179)。核心就四个字段:

字段作用
name这个桩的标识,边靠它寻址186
method方法名字符串,运行时反射调用195
types桩上标的输出类型(如 ["Message"]),前端据此判断能不能连180
value算出来的结果,默认是哨兵 UNDEFINED198
cache默认 True,同一次运行内复用 value201
allows_loop允许被回边指向(留给 04)206

为什么 value 的默认值是 UNDEFINED 而不是 None? 因为 None合法的计算结果。用一个 Enum 哨兵(UndefinedType.undefined,field/base.py:25-29)才能区分"还没算"和"算出来是空"。序列化时它变成字符串 "__UNDEFINED__",反序列化时再还原成哨兵(Output.serialize_model:239、validate_model:246)。

types 不用手写。 组件 __init__ 的最后一步 _set_output_types(component.py:747)对每个 output 调 _set_output_return_type(:751),后者走 _get_method_return_type(component.py:1118-1127)——用 get_type_hints(method) 读方法的返回类型注解,拆开 Union/list[...] 后格式化成字符串塞进 output.types

也就是说,def combine_texts(self) -> Message: 里那个 -> Message,同时是 Python 类型注解和 UI 上的连线契约。漏写注解,桩就没类型,前端不知道该不该允许连线。


5. 值什么时候算

5.1 主循环

运行时的入口不是组件自己,而是外面的 vertex:build_component(src/lfx/src/lfx/interface/initialize/loading.py:350-358)先 set_attributes(params) 灌值,再 await build_results()

_build_results(component.py:1294-1308)本身短得出奇:

_build_results()

├─ _pre_run_setup_if_needed() 钩子:组件可自定义跑前准备
├─ _handle_tool_mode() 若有字段开了 tool_mode,追加一个 Toolset 输出

└─ for output in _get_outputs_to_process(): ← 只挑"需要算的"
result = await _get_output_result(output) ← 缓存 + 反射 + 线程池
results[output.name] = result
artifacts[output.name] = _build_artifact(result) ← 给前端看的展示形态
_log_output(output) ← 该 output 期间的日志归档

注意最后两行:每个 output 除了原始结果,还产出一份 artifact 和一份日志。artifact 是"给人看的样子"(repr 字符串 + raw 数据 + 类型标签,_build_artifact:1413),日志则按 output 名分桶(_log_output:1486),所以画布上能分别展开每个桩的输出预览。

5.2 只算下游真的连了线的 output

一个组件可能声明 5 个输出,但流里只用了 1 个。全算就是纯浪费(尤其是每个输出都可能调一次 LLM)。

判断逻辑只有三行(_should_process_output,component.py:1320-1328):

if not self._vertex or not self._vertex.outgoing_edges:
return True # 没图 / 没下游 → 全算(单测、末端节点)
return output.name in self._vertex.edges_source_names

edges_source_names 是这个 vertex 所有边的 source_handle.name 集合(src/lfx/src/lfx/graph/vertex/base.py:212-214)。换句话说:一个输出桩上没拉线,它的方法就根本不会被调用。

_get_outputs_to_process(component.py:1330-1358)在此之上保证顺序——先按类里 self.outputs 的声明顺序,再补上 _outputs_map 里多出来的(比如运行时追加的 Toolset 输出)。

5.3 缓存与反射:_get_output_result

这是全章最该读的一段(component.py:1360-1393),四件事按顺序发生:

步骤代码为什么
命中缓存就返回if output.cache and output.value != UNDEFINED :1368一个输出被多个下游连,只算一次
按名字取方法method = getattr(self, output.method) :1375method 是字符串,所以 output 定义可以从 JSON 来
异步/同步统一await method() if iscoroutinefunction else await asyncio.to_thread(method) :1377同步方法丢线程池,不阻塞事件循环
回填与净化apply_options_sanitize_secret_valuesoutput.value = result :1389-1391应用 output 上的 filter;把 _secret_values 里登记过的明文替换成 **********

"method 是字符串 + getattr 反射"这一条是整个模型的枢纽:它让 map_outputs 可以拿前端存的 output 定义(Output(**output),component.py:626-633)覆盖类里写死的那份,而覆盖后仍然能找回类里的真方法。UI 因此可以增删/改名输出桩,而不必改 Python 类。

中间还夹了一条小修补:结果若是 Message 且没有 flow_id,顺手补上当前流的 id(:1382-1388),这样消息落库时才知道自己属于哪条流。

5.4 按需拉单个输出:resolve_output

resolve_output(output_name)(component.py:1395-1411)是"我现在就要这一个输出的值"的入口:查 _outputs_map,命中缓存直接返,否则走 _get_output_result。找不到时抛的是一句给用户看的人话 KeyError,不是裸的键错误。


6. 纯代码里连线:set() 如何把图建起来

Langflow 的组件不只服务画布,在纯 Python 里也能拼流。这段是 lfx CLI 和几乎所有集成测试的写法:

# 示意,非源码
combine = CombineTextComponent()
out = ChatOutputComponent()
out.set(input_value=combine.combine_texts) # 注意:传的是"方法对象",不是调用结果

关键在于 set() 收到的值是什么类型,决定了它做参数赋值还是建边(set:519 → _process_connection_or_parameters:947 → _process_connection_or_parameter:861):

传进去的值set() 的判断结果
另一个组件的方法对象(如 c.combine_texts)callable__self__Component建一条边(_connect_to_component:976)
另一个组件实例(如 c)isinstance(value, Component)按类型自动挑一个匹配的输出,再建边
普通字面量(str/int/dict/Message…)都不是set_input_value,当参数赋值(_set_parameter_or_attribute:1004)
上面这些组成的 list_process_connection_or_parameters:950逐项处理,可以多条边接同一个输入
本组件某个 allows_loop=True输出名_is_loop_connection:884建一条指向"输出"的回边(见 04)

传实例而不是方法时,框架得自己猜连哪个桩:_find_matching_output_method(component.py:805-859)拿对方每个 output.types 去和本输入的 input_types 求交集。恰好一个匹配才放行;多个就抛错并把所有候选打印成 输出名[类型]->输入名[类型] 的清单让人自己选,零个也抛错。这是"宁可报错也不猜"的取舍。

边不是对象,就是一个普通字典(_add_edge,component.py:982-1002):

{"source": 上游组件 id, "target": 本组件 id,
"data": {"sourceHandle": {"dataType", "id", "name": 输出名, "output_types"},
"targetHandle": {"fieldName": 输入名, "id", "inputTypes", "type"}}}

这个字典的形状和前端画布保存下来的边 JSON 是同一个——所以纯代码拼出来的图,和画布拖出来的图,后面走的是同一条构图路径(02)。

顺带一提 _get_or_create_input(component.py:959-974):set() 到一个类里没声明的名字时,会临时造一个通用 Input 兜底。这里有一段带血的注释——直接 self.inputs.append(...) 会改到类属性列表,把一个活的 LLM 客户端泄漏给后续所有实例,所以要先把 inputs 提升成实例私有副本。


7. 代码 → UI:单向生成

没有"UI 编辑器"这种东西。 节点长什么样,是从 Python 类算出来的;人改的是代码,不是节点定义。

Python 类

│ get_template_config() —— 按 ATTR_FUNC_MAPPING 白名单抄类属性

{display_name, icon, description, inputs, _outputs_map, ...}

│ ComponentFrontendNode.from_inputs() —— inputs 列表 → Template(fields=...)

FrontendNode 对象

├── _map_parameters_on_frontend_node() 把构造时传的参数写回字段 value
├── add_code_field() 把整份源码塞进名为 "code" 的字段
├── 补 output.types(读方法返回注解)
└── validate_component() / set_base_classes_from_outputs()

{"data": {"node": {...}, "type": 类名, "id": ...}, "id": ...} ← 前端直接吃的 JSON

第一步是白名单抄写。 get_template_config(base_component.py:88-104)遍历 ATTR_FUNC_MAPPING(src/lfx/src/lfx/custom/attributes.py:77-98)——一张"属性名 → 取值函数"的表,只认 display_namedescriptioniconbetalegacypriorityinputsoutputs_inputs_outputs_mapmetadata 等固定几项。类里写别的属性,UI 一律看不见。

第二步把 inputs 变成 template。 FrontendNode.from_inputs(src/lfx/src/lfx/template/frontend_node/base.py:196-206)干的事简单到有点朴素:把 inputs 列表整个塞进 Template(type_name="Component", fields=inputs)。前端拿到的"表单定义",就是这批 pydantic 模型序列化后的样子。

第三步是两次写回参数值。 to_frontend_node(component.py:1132-1184)先对 FrontendNode 对象调 _map_parameters_on_frontend_node(:1096),转成 dict 后又对 template 调 _map_parameters_on_template(:1103)。两者都把 self._parameters(构造时传进来的 kwargs)写成字段的 value,并同步 load_from_db 标志。后者在 key 找不到时会用 find_closest_match 给一句 Did you mean 'xxx'?——拼错参数名的报错体验就是这里来的

第四步:代码本身也是一个字段。 add_code_field(src/lfx/src/lfx/custom/utils.py:418-433)造一个 name="code"field_type="code"advanced=True 的输入,value 就是整份源码字符串(to_frontend_node 里有一份等价的内联实现,component.py:1149-1161)。所以每个节点都随身带着自己的源码——这既是画布上"打开代码"能编辑组件的原因,也是保存的流能脱离仓库运行的原因(执行这份代码的机制留给 06)。

第五步补类型、验重名。 对每个还没有 types 的 output 读方法返回注解补上;validate_component() 再检查一遍输入输出没有重名(同样的检查在 __init__ 里也做过一遍,_there_is_overlap_in_inputs_and_outputs,component.py:318-329)。

两个入口,一条岔路

外部调用方走的是 build_custom_component_template(utils.py:540-610)。它开头有个岔路(utils.py:574):

  • template_configinputs → 走新路 build_custom_component_template_from_inputs(utils.py:464-504),即上面那条管线。
  • 没有 inputs → 走老路:CustomComponentFrontendNode + run_build_config + 从 build() 函数签名反推字段(add_extra_fields:238)。这是 Component 出现之前的旧式自定义组件。

新路比 to_frontend_node 多做两件事:reorder_fields_get_field_order()(即 inputs 的声明顺序,component.py:1523-1528)排字段;build_component_metadata(utils.py:507-537)算源码哈希(缓存失效用)并分析依赖。

"单向"的边界在哪

诚实地说,单向是就这条生成路径而言的:

  • 生成方向永远是 代码 → JSON,前端不会反过来改 Python 文件。
  • 但存下来的流里带着 code 字段,加载时会 eval 回一个类,并且 map_outputs(component.py:626-635)会用 vertex 里存的 output 定义覆盖类里的。所以老流打开时看到的是它当年存下的形状,不是仓库当前版本的形状。
  • 上游改了组件、老流要不要跟着变,靠的是"类名即身份"这条约定——改类名等于换了个组件,老流会找不到它

8. 组件之间流通的三种数据

组件之间不传裸 str 和裸 dict,只传三种带语义的类型。它们全在 src/lfx/src/lfx/schema/ 下,且都有历史别名:

类型(别名)真正的类是什么关键字段/性质
Data(别名)JSON(schema/data.py:51,别名在 :321)一个带 text_key 的字典data: dicttext_key="text"default_value
MessageMessage(Data)(schema/message.py:59)一条会话消息textsendersession_idfilestimestampcontent_blocksflow_id
DataFrame(别名)Table(pandas.DataFrame)(schema/dataframe.py:19,别名在 :269)一张表,每行一个 Data继承 pandas 全部能力,附带 text_key/default_value

三者的关系是层层包裹的: Message 继承 Data(所以消息本质也是个带 text_key 的字典);Table 是"一堆 Data 排成表"。

互转是显式方法,不是隐式转换:

从 → 到方法位置
DataMessageto_message()(有 text_key 用它,没有就 str(data))data.py:290
DataDataFrameto_dataframe()data.py:297
MessageData / DataFrameto_data() / to_dataframe()(单行表)message.py:468 / :471
DataFrameDatato_data()(整表塞进 {"results": [...]})dataframe.py:241
DataFrameMessageto_message()(转成 markdown 表格文本)dataframe.py:250
DataFramelist[Data]to_data_list()dataframe.py:100

DataFrame.to_message 那段值得看一眼:它会去空行、压多余换行、转义 |、把单元格里的换行换成 <br/>,然后 to_markdown()——为了让表格塞进聊天气泡里不炸掉 markdown

这三个类型名同时也是 input_types 里的字符串(["Message"]["Data","JSON"]["DataFrame","Table"])。类型系统在这里是"名字字符串匹配",不是 Python 的 isinstance——因为前端要在浏览器里判断两个桩能不能连,它手上只有字符串。


9. 巧妙之处

  1. 模板/实例两层深拷贝。 类属性当模板、__init__ 拷一份、map_* 再拷一份(component.py:157、:642、:661)。__deepcopy__(:428-492)还专门重写了:配置和输入浅拷(里面可能有带 threading.RLock 的服务,深拷会炸),但 _outputs_map 必须深拷——注释直白写了原因:Output.cache=True 时,浅拷会让并发请求共享同一个 Output,第一个请求的结果被返回给所有后续请求。

  2. UNDEFINED 哨兵而不是 None 让"没算过"和"算出来是 None"可区分(field/base.py:25-29),且能安全地过 JSON 往返。

  3. 方法名存字符串 + 反射调用。 这一条解耦了"UI 上的输出桩"和"Python 里的方法",使输出定义可以来自 JSON、可以被前端覆盖(component.py:1375、:626-635)。

  4. 类型注解直接当 UI 契约。 -> Message 既是给人/mypy 看的,也是画布连线的合法性依据(_get_method_return_type:1118)。组件作者不写第二份类型声明。

  5. 同步方法自动丢线程池。 asyncio.to_thread(method)(:1377)让组件作者可以随手写阻塞的 requests.get,不会拖垮整个事件循环。

  6. 密钥登记 + 输出脱敏。 密码字段的明文在 set_attributes 时登记进 _secret_values(:1219),每个输出结果出门前都被 _sanitize_secret_values(:1436-1458)递归扫一遍——str/Message/Data/dict/list/tuple 都进去替换成 **********

  7. 脱离图也能跑。 PlaceholderGraph(:111-147)让 self.graph 在没有真图时也有个安全的假对象,组件因此可以在单测里直接 component(**kwargs) 调用(__call__:1016)。


10. 边界与局限

  • __getattr__ 会把拼写错误变成运行期错误。 类里没声明的名字要到方法真跑起来才抛 AttributeError(:1060),IDE 和类型检查器帮不上忙。
  • 输入名和输出名不能重叠。 构造时就检查并抛错(_there_is_overlap_in_inputs_and_outputs:318),因为 __getattr__ 的查找顺序里两者共用一个命名空间。
  • self.some_output 拿到的是 Output 对象,不是值。 想要值必须 await resolve_output(name)
  • CONFIG_ATTRIBUTES 那条分支是死代码。 CONFIG_ATTRIBUTES 里每一项都以 _ 开头(:78),而 __init__ 先判断 key.startswith("_")(:188),elif key in CONFIG_ATTRIBUTES 永远走不到。功能上无害(两条分支都往 config 里放),但 config[key[1:]] 那个去下划线的意图从未生效。
  • 改类名 = 破坏兼容。 类名是保存的流里用来找回组件的身份标识。
  • build_custom_component_templateHTTPException 一个本该纯粹的模板构建函数直接抛 HTTP 异常(utils.py:559),把 Web 层的概念泄漏进了组件层。
  • 本章不涉及:边与图的构建(02)、执行顺序与并行(03)、环与条件分支(04)、事件流(05)、组件发现与用户代码执行(06)。

11. 代码地图

主题文件(相对克隆根)符号
组件基类、全部核心逻辑src/lfx/src/lfx/custom/custom_component/component.pyComponent
实例化:拷模板、建两张表同上__init___copy_component_templatemap_inputsmap_outputs
self.x 解析魔法同上__getattr__PlaceholderGraph
运行期灌值同上set_attributes_validate_inputs_wrap_if_secret
计算主循环同上_build_resultsbuild_results
按需计算 / 缓存 / 反射同上_should_process_output_get_outputs_to_process_get_output_resultresolve_output
纯代码连线建图同上set_process_connection_or_parameter_find_matching_output_method_add_edge
生成前端节点同上to_frontend_node_map_parameters_on_frontend_node_map_parameters_on_template_get_method_return_type
输出脱敏与 artifact同上_sanitize_secret_values_build_artifactextract_data
深拷贝语义同上__deepcopy__
输入类型全家族src/lfx/src/lfx/inputs/inputs.pyInputTypesMessageTextInputSecretStrInputHandleInputInputTypesMap
输入字段的 mixinsrc/lfx/src/lfx/inputs/input_mixin.pyBaseInputMixinListableInputMixinDatabaseLoadMixinToolModeMixin
输出定义与哨兵src/lfx/src/lfx/template/field/base.pyOutputInputUNDEFINEDUndefinedType
前端节点容器src/lfx/src/lfx/template/frontend_node/base.pyFrontendNodefrom_inputsset_field_value_in_template
类属性 → 模板配置src/lfx/src/lfx/custom/custom_component/base_component.pyget_template_config
允许进 UI 的属性白名单src/lfx/src/lfx/custom/attributes.pyATTR_FUNC_MAPPING
模板构建入口src/lfx/src/lfx/custom/utils.pybuild_custom_component_templatebuild_custom_component_template_from_inputsadd_code_fieldbuild_component_metadata
运行时入口(灌值 → 算值)src/lfx/src/lfx/interface/initialize/loading.pybuild_componentget_instance_results
三种流通数据src/lfx/src/lfx/schema/message.pyschema/data.pyschema/dataframe.pyMessageJSON(别名 Data)、Table(别名 DataFrame)
最短的真实组件范例src/lfx/src/lfx/components/processing/combine_text.pyCombineTextComponent