前面七篇我们沿着「一条消息的旅程」纵向穿透了平台、流水线、模型、插件、沙箱。这一篇横过来看:LangBot 怎么把自己的能力暴露给外界——给人用的是 Web 管理面板背后的 HTTP API,给 AI Agent 用的是 MCP Server 和 Skills。关键洞见是:它们不是三套并行实现,而是复用同一个 Service 层的三个「表面」。读懂这一篇,你会理解仓库
AGENTS.md里那条反复强调的「三同步铁律」为什么是架构正确性问题,而不是文档洁癖。
LangBot 号称「几乎所有操作都能在 Web 前端完成」,并且「对外暴露丰富的 API」。这意味着同一份业务能力(增删机器人、配模型、建知识库……)至少要被三类调用方使用:
最糟糕的设计是为这三者各写一套逻辑,然后慢慢漂移。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(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/<group>/..."]
I --> D --> L --> R --> Q