第五篇我们看到
ToolManager把工具聚合成四类源,其中两类——Plugin 与 MCP——都来自一个跑在 LangBot 进程之外的「插件运行时」。这一篇就来拆解pkg/plugin:它本身不实现任何插件逻辑,只是一座桥——把 LangBot 主进程和独立的 Plugin Runtime 用 RPC 连起来。读懂这一篇,你会明白为什么 LangBot 要把插件「赶出」自己的进程,以及它如何在「个人轻量」和「生产容器」两种形态间自适应。
把插件直接 import 进主进程是最省事的做法,但 LangBot 偏偏没这么干。原因有三:
于是 LangBot 把插件 SDK、CLI(lbp)、Plugin Runtime整套都拆进了一个独立仓库 langbot-plugin-sdk,主仓库只通过 pyproject.toml 精确钉版本依赖:
# pyproject.toml
"langbot-plugin==0.4.6",
本仓库里只剩一座「桥」——pkg/plugin/,两个文件扛起全部职责:
connector.py(915 行)——PluginRuntimeConnector:负责连上 Runtime(选传输、心跳、重连),以及把「安装/升级/删除/列举/调用工具」等高层操作转发过去。handler.py(1241 行)——RuntimeConnectionHandler:处理双向 RPC 的具体 action。flowchart LR
subgraph LB["LangBot 主进程"]
AP["Application (ap)"]
CONN["PluginRuntimeConnector<br>connector.py"]
HDL["RuntimeConnectionHandler<br>handler.py"]
end
subgraph RT["Plugin Runtime (独立进程 / 独立仓库)"]
PR["Runtime 调度"]
P1["插件 A"]
P2["插件 B"]
MCP["MCP / Skill"]
end
AP --> CONN --> HDL
HDL <-->|"stdio 或 WebSocket<br>双向 RPC"| PR
PR --> P1 & P2 & MCP
这是本篇最能体现 LangBot「双形态」哲学的地方。PluginRuntimeConnector.initialize() 根据运行环境自动在三条路径里选一条(connector.py:121):
flowchart TD
START["initialize()"] --> Q1{"docker 环境 或<br>--standalone-runtime ?"}
Q1 -->|是| WS["WebSocket 连接外部 Runtime<br>plugin.runtime_ws_url"]
Q1 -->|否| Q2{"平台 == win32 ?"}
Q2 -->|是| WIN["子进程拉起 Runtime<br>+ WebSocket(localhost:5400) 通信"]
Q2 -->|否| STDIO["子进程拉起 lbp rt -s<br>+ stdio 管道通信"]
对应代码(已精简):
# src/langbot/pkg/plugin/connector.py:121
if platform.get_platform() == 'docker' or platform.use_websocket_to_connect_plugin_runtime(): # use websocket
ws_url = self.ap.instance_config.data.get('plugin', {}).get(
'runtime_ws_url', 'ws://langbot_plugin_runtime:5400/control/ws'
)
self.ctrl = ws_client_controller.WebSocketClientController(ws_url=ws_url, ...)
task = self.ctrl.run(new_connection_callback)
elif platform.get_platform() == 'win32':
# Windows 对 stdio + subprocess 支持不全,只能用 cmd 拉起、用 ws 通信
await self._start_runtime_subprocess('-m', 'langbot_plugin.cli.__init__', 'rt')
self.ctrl = ws_client_controller.WebSocketClientController(ws_url='ws://localhost:5400/control/ws', ...)
task = self.ctrl.run(new_connection_callback)
else: # stdio
python_path = sys.executable
self.ctrl = stdio_client_controller.StdioClientController(
command=python_path, args=['-m', 'langbot_plugin.cli.__init__', 'rt', '-s'], env=env,
)
task = self.ctrl.run(new_connection_callback)
三条路径的取舍:
| 形态 | 触发条件 | 传输 | Runtime 由谁启动 | 典型场景 |
|---|---|---|---|---|
| stdio | Unix/macOS 直接启动 | stdio 管道 | LangBot 子进程拉起 lbp rt -s |
个人本地、零配置 |
| WebSocket(外部) | Docker 或 --standalone-runtime |
WebSocket | 外部独立启动 | 生产容器化部署 |
| WebSocket(Windows) | win32 | WebSocket(localhost) | LangBot 子进程拉起 | Windows 本地(stdio 受限) |
这里的 --standalone-runtime 是个关键开关。它在入口处把 platform.standalone_runtime 置为 True(src/langbot/__main__.py:41),从而让 use_websocket_to_connect_plugin_runtime() 返回真(pkg/utils/platform.py:22)——LangBot 就会去连一个别人启动好的 Runtime,而不是自己拉起。这正是生产部署中「LangBot 容器 + Plugin Runtime 容器」分离的基础。