前面七篇我们沿着「一条消息的旅程」纵向穿透了平台、流水线、模型、插件、沙箱。这一篇横过来看:LangBot 怎么把自己的能力暴露给外界——给人用的是 Web 管理面板背后的 HTTP API,给 AI Agent 用的是 MCP Server 和 Skills。关键洞见是:它们不是三套并行实现,而是复用同一个 Service 层的三个「表面」。读懂这一篇,你会理解仓库 AGENTS.md 里那条反复强调的「三同步铁律」为什么是架构正确性问题,而不是文档洁癖。

一、这一层解决什么问题

LangBot 号称「几乎所有操作都能在 Web 前端完成」,并且「对外暴露丰富的 API」。这意味着同一份业务能力(增删机器人、配模型、建知识库……)至少要被三类调用方使用:

  1. Web 前端 SPA——人在浏览器里点。
  2. 第三方集成 / 脚本——用 API Key 直接打 HTTP。
  3. AI Agent——通过 MCP 协议调用工具来「自己管理 LangBot」。

最糟糕的设计是为这三者各写一套逻辑,然后慢慢漂移。LangBot 的解法是单一 Service 层 + 多个薄表面:所有业务逻辑下沉到 pkg/api/http/service/,HTTP Controller 和 MCP 工具都只是它的薄封装。这就是整篇的主线。

flowchart TB
    WEB["Web 前端 SPA<br>(Vite+React)"]
    CLI["第三方/脚本<br>(API Key)"]
    AGENT["AI Agent<br>(MCP 协议)"]
    SKILL["skills/<br>(技能说明书)"]

    subgraph backend["LangBot 后端 (Quart, :5300)"]
        CTRL["HTTP Controllers<br>~25 个 RouterGroup"]
        MCP["MCP Server<br>精选工具子集"]
        SVC["Service 层<br>pkg/api/http/service/*"]
        PER["persistence_mgr"]
    end

    WEB --> CTRL
    CLI --> CTRL
    AGENT --> MCP
    SKILL -.指导.-> AGENT
    CTRL --> SVC
    MCP --> SVC
    SVC --> PER

注意:MCP 和 CTRL 两条线最终都汇到同一个 SVC。这是本篇要反复强调的事实。

二、Quart 应用与路由的自动注册

后端是 Quart(Flask 的异步版)。整个 HTTP 层的入口是 HTTPController(src/langbot/pkg/api/http/controller/main.py:30),它在构造时建好 Quart app、开全局 CORS、设置上传上限:

# src/langbot/pkg/api/http/controller/main.py:35
def __init__(self, ap: app.Application) -> None:
    self.ap = ap
    self.quart_app = quart.Quart(__name__)
    quart_cors.cors(self.quart_app, allow_origin='*')

    # Set maximum content length to prevent large file uploads
    self.quart_app.config['MAX_CONTENT_LENGTH'] = group.MAX_FILE_SIZE  # 10MB

MAX_FILE_SIZE = 10 * 1024 * 1024(group.py:13),超限会被一个自定义 RequestEntityTooLarge 处理器拦下返回 400(main.py:49)。

路由不是手动逐条 @app.route 注册的,而是自动发现。每个路由组用 @group_class(name, path) 装饰器把自己登记进一个全局列表,register_routes() 再统一实例化:

# src/langbot/pkg/api/http/controller/group.py:16
preregistered_groups: list[type[RouterGroup]] = []

def group_class(name: str, path: str):
    def decorator(cls):
        cls.name = name
        cls.path = path
        preregistered_groups.append(cls)
        return cls
    return decorator
# src/langbot/pkg/api/http/controller/main.py:113
async def register_routes(self) -> None:
    @self.quart_app.route('/healthz')
    async def healthz():
        return {'code': 0, 'msg': 'ok'}

    for g in group.preregistered_groups:
        ginst = g(self.ap, self.quart_app)
        await ginst.initialize()
    # ... 之后是前端 SPA 的静态文件兜底路由

这套机制能跑起来,靠的是和第二篇 BuildAppStage、第四篇 Stage 注册一脉相承的「import 即注册」模式——main.py 顶部用 importutil.import_modules_in_pkg(...) 把 groups/ 下所有模块都 import 一遍,装饰器随之执行,列表就填满了。

当前仓库里 groups/ 下有 29 个 RouterGroup 文件(按子域分布在 platform/、pipelines/、provider/、knowledge/、resources/ 等子目录),覆盖机器人、流水线、模型/供应商、知识库、MCP/工具、API Key、监控、系统信息等。它们统一挂在 /api/v1/<group> 下。

flowchart LR
    subgraph reg["import 即注册"]
        I["importutil<br>import_modules_in_pkg(groups)"]
        D["@group_class 装饰器执行"]
        L["preregistered_groups 列表填充"]
    end
    R["register_routes()<br>遍历实例化 + initialize()"]
    Q["Quart 路由表<br>/api/v1/&lt;group&gt;/..."]
    I --> D --> L --> R --> Q

前端同源伺服