第五篇我们看到 ToolManager 把工具聚合成四类源,其中两类——Plugin 与 MCP——都来自一个跑在 LangBot 进程之外的「插件运行时」。这一篇就来拆解 pkg/plugin:它本身不实现任何插件逻辑,只是一座桥——把 LangBot 主进程和独立的 Plugin Runtime 用 RPC 连起来。读懂这一篇,你会明白为什么 LangBot 要把插件「赶出」自己的进程,以及它如何在「个人轻量」和「生产容器」两种形态间自适应。

一、这一层解决什么问题

把插件直接 import 进主进程是最省事的做法,但 LangBot 偏偏没这么干。原因有三:

  1. 故障隔离——第三方插件崩溃、死循环、内存泄漏不应该拖垮整个机器人。独立进程意味着插件挂了主进程还活着。
  2. 依赖隔离——插件可能依赖与 LangBot 冲突的库版本。独立进程 + 独立环境天然解耦。
  3. 生命周期解耦——插件要能热装、热卸、热升级,而不重启主进程。

于是 LangBot 把插件 SDK、CLI(lbp)、Plugin Runtime整套都拆进了一个独立仓库 langbot-plugin-sdk,主仓库只通过 pyproject.toml 精确钉版本依赖:

# pyproject.toml
    "langbot-plugin==0.4.6",

本仓库里只剩一座「桥」——pkg/plugin/,两个文件扛起全部职责:

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 容器」分离的基础。