53 KiB
Chongming-Zenoh
基于 FastAPI + Zenoh + MQTT + Vue 3 的配置驱动微服务网关平台。
通过 TOML 配置文件定义 RPC 端点,自动生成 Pydantic 模型与 FastAPI 路由,同时暴露 HTTP 和 MQTT 双通道。底层基于 Zenoh 分布式消息协议实现 RPC 通信,Worker 插件可独立开发、按副本数水平扩展。
核心特性
| 特性 | 说明 |
|---|---|
| FastAPI + Uvicorn | 高性能异步 Web 框架,自动生成 OpenAPI 文档 |
| Zenoh 服务发现 | 基于 liveliness 的 Worker 注册与 RPC 调用,负载均衡 + 超时重试 |
| MQTT 集成 | amqtt Broker(内嵌或外部),MQTT ↔ Worker 双向桥接 |
| 动态路由生成 | 扫描 config/workers/*/*.toml 自动注册 HTTP 路由 |
| Worker 框架 | @rpc_method 装饰器 + Plugin 基类统一生命周期 + Pydantic 模型绑定 |
| 启动契约校验 | TOML 路由 ↔ @rpc_method ↔ 生成模型三方一致性校验,fail fast |
| 混合部署 | inline(网关进程内)与 external(独立进程)两种插件模式按插件粒度共存 |
| 动态路由注册 | external 插件经 Zenoh 注册路由,网关动态挂载/摘除 HTTP 端点 |
| 进程插件隔离 | 进程插件与网关零导入关系,仅经 Zenoh 通讯,可独立打包到不同容器 |
| 自动化模型生成 | gen_models 根据 TOML 配置生成请求/响应模型 |
| 插件脚手架 | gen_plugin 一键生成风格统一的 Worker 插件骨架 |
| JWT 鉴权 | Rust 实现,路由级 auth_required 控制 |
| Argon2id 密码哈希 | Rust 实现的高性能密码安全方案 |
| 异步 SQLite | aiosqlite + SQLModel,多进程并发安全 |
| 本地文件存储 | 内置文件上传/下载端点 |
| 流媒体代理 | ffmpeg + MediaMTX RTSP 流转发 |
| 构建打包 | PyInstaller / Nuitka 一键打包为独立可执行文件 |
| Vue 3 前端 | Element Plus UI、Pinia 状态管理、类型安全的 API 客户端 |
| Worker 运维管理 | 前端经 Zenoh 向单个副本下发 restart/stop/改配置指令(超管),前端可视化列表 + 分页管理 |
项目结构
chongming-zenoh/
├── config.toml # 应用主配置
├── pyproject.toml # 项目依赖与脚本入口
├── config/ # 配置文件目录
│ ├── zenoh_config.json5 # Zenoh 会话配置
│ └── workers/ # Worker 路由定义(TOML)
│ ├── config.toml # 旧式全局插件注册(仅作兜底兼容)
│ ├── auth/ # 鉴权插件(config.toml = 插件注册 + 运行时配置)
│ ├── doubao/ # 豆包视觉理解插件
│ ├── aibox_sophon_adapter/ # AI Box 设备心跳监控插件
│ └── example/ # 示例插件(四则运算 / MQTT 端点)
├── src/
│ ├── chongming/ # 核心应用
│ │ ├── app/
│ │ │ ├── core/ # WorkerManager, RouteGenerator, MQTTBridge, contracts(契约校验), auth
│ │ │ └── routes/ # 内置路由(文件上传等)
│ ├── chongming-web/ # Vue 3 前端
│ │ └── src/
│ │ ├── api/generated/ # 自动生成 API 客户端
│ │ ├── views/ # 页面组件
│ │ └── router/ # Vue Router 配置
│ └── plugins/ # Worker 插件源码(inline 模式)
│ ├── auth/ # JWT 鉴权
│ ├── doubao/ # 豆包 AI(支持 AICC 安全通道)
│ ├── aibox_sophon_adapter/ # 设备心跳 + 告警
│ └── example/ # 演示示例
├── workers/ # 进程插件(external 模式,本地开发目录,不入库;
│ └── demo/ # 生产在独立容器部署,示例见本地目录)
├── utils/ # 独立工具包
│ ├── bytedance/ # 字节跳动安全通信 SDK
│ ├── cli/ # 命令脚本集合(build / gen_models / gen_plugin,chongming-cli)
│ ├── config/ # TOML 配置加载
│ ├── database/ # 异步 SQLite 封装
│ ├── jwt/ # Rust JWT 编解码
│ ├── password/ # Rust Argon2id 哈希
│ ├── request/ # Zenoh RPC 客户端
│ ├── stream_manager/ # ffmpeg RTSP 流管理
│ └── worker/ # Worker 框架核心
├── data/
│ ├── storage/chongming/ # SQLite 数据库文件
│ └── uploads/ # 文件上传目录
├── logs/ # 日志目录
├── resources/ # 模块银行打包资源
├── test_mqtt_e2e.py # MQTT 端到端测试
├── publisher.py / subscriber.py # MQTT 收发示例
└── README.md
多仓库结构与开发工作流
本仓库(chongming-core)只跟踪网关核心代码,其余资产分布在独立仓库中:
| 目录 / 包 | 独立仓库 | 说明 |
|---|---|---|
src/chongming/ |
本仓库(chongming-core) | 网关核心(FastAPI + Zenoh) |
utils/<name>/ |
chongming-utils-*(9 个,见「工具包列表」) |
工具包,由 uv sync 按 git 源安装 |
utils/cli/ |
chongming-utils-cli(规划中,暂以 path 源安装) |
命令脚本集合(build / build_plugin / gen_models / gen_plugin) |
src/plugins/ |
chongming-plugins |
Worker 插件源码 |
config/ |
chongming-config |
应用与 Worker 路由配置 |
src/chongming-web/ |
chongming-web |
Vue 3 前端 |
首次克隆(开发环境引导)
git clone http://124.71.68.6:3000/chongming/chongming-core.git chongming-zenoh
cd chongming-zenoh
git clone http://124.71.68.6:3000/chongming/chongming-plugins.git src/plugins
git clone http://124.71.68.6:3000/chongming/chongming-config.git config
git clone http://124.71.68.6:3000/chongming/chongming-web.git src/chongming-web
uv sync # utils/* 由 uv 按 pyproject 的 git 源自动安装,无需手工 clone
开发与发布检查清单
修改 utils/<name>(工具包):
- 在
utils/<name>内提交并 push 到对应远程 - 回主仓库执行
uv lock --upgrade-package <包名>刷新版本 pin uv sync后运行相关测试- 主仓库提交
uv.lock
修改 src/plugins/ / config/ / src/chongming-web/:
- 在对应子仓库内提交并 push
- 回主仓库启动验证(
uv run serve+ 相关测试)
发布前总检查:
- 所有子仓库
git status干净且已 push uv.lock与各 git 源最新提交一致- 配置中无硬编码凭据——密钥一律走环境变量:
VOLC_ACCESSKEY(豆包 Ark Key)、AIBOX_MQTT_URI(AI Box 外部 Broker URI) uv run python test_mqtt_e2e.py等测试通过
快速开始
环境要求
- Python >= 3.12
- Rust 工具链(用于编译
jwt/password扩展) - Node.js >= 22(前端开发)
- uv 包管理器
- ffmpeg(可选,stream_manager 需要)
安装与启动
首次克隆:先按「多仓库结构与开发工作流」拉取
src/plugins/config/src/chongming-web子仓库,再执行以下步骤。
# 同步依赖(自动编译 Rust 扩展 + 安装 Python 包)
uv sync
# 启动服务(开发模式,热重载)
uv run serve
服务启动后:
| 通道 | 地址 | 说明 |
|---|---|---|
| HTTP | http://localhost:8000 |
REST API |
| Swagger | http://localhost:8000/docs |
OpenAPI 交互文档 |
| MQTT | mqtt://localhost:1883 |
内嵌 Broker |
前端开发
cd src/chongming-web
npm install
npm run dev # http://localhost:5173
架构与数据流
graph TB
HTTP[HTTP 客户端] -->|REST API| API[FastAPI 动态路由]
MQTT[MQTT 客户端] -->|chongming/+/+| BRIDGE[MQTT Bridge<br/>纯客户端模式]
REG[RouteRegistry<br/>订阅 chongming/routes/**] -->|动态挂载/摘除 HTTP 路由| API
API --> WM[WorkerManager<br/>随机负载均衡 + 超时重试]
BRIDGE --> WM
subgraph GW[网关进程 chongming]
API
REG
BRIDGE
WM
end
WM -->|Zenoh Query| IW[inline Worker 副本<br/>网关进程内加载]
WM -->|Zenoh Query| EW[external Worker 进程<br/>独立容器,零网关依赖]
EW -.->|发布路由描述符<br/>chongming/routes/**| REG
IW --> DB[(SQLite)]
EW --> DB
EW -->|AI 调用| AI[豆包 API]
启动流程:
uv run serve → FastAPI lifespan:
1. setup_storage() — 本地文件系统存储
2. setup_zenoh() — Zenoh peer 会话
3. setup_mqtt_broker() — 内嵌 amqtt Broker(子进程 + 文件锁单例;
`use_embedded_broker=false` 时跳过,改用外部 Broker)
4. setup_worker_manager() — WorkerManager + 定期 liveliness 刷新 + 网关登记(register_gateway,参与全局分摊)+ 该插件副本数经 resolved_count 按 launch_worker_count 从 cache/toml 解析
5. load_workers() — 按插件 [workers].load_mode 分派:
inline → 进程内 import 插件 → 启动 Worker 协程
external → 跳过(独立进程运行,盘扫条目仅观测)
5.1 setup_route_registry() — 常驻订阅 chongming/routes/**,external 插件路由动态挂载/摘除
6. validate_contracts() — inline 插件三方契约校验,失败终止启动
7. register_routes() — inline 插件盘扫生成 HTTP 路由
8. setup_mqtt() — MQTT Bridge(inline 主题静态收集 + external 主题动态推送;基于共享缓存的分布式锁保证全局单实例)
Worker 管理系统
通过 Zenoh 向 worker/<type>/<worker_id>/__admin__ 管理端点下发指令,精细化控制单个 Worker 副本(inline 与 external 进程插件共用同一管理协议)。提供 REST 管理端点 + Vue 前端页面,操作需超级管理员鉴权。
管理协议
每个用 Worker 框架(chongming_worker.runtime.create_workers)创建的副本都会注册一个管理 Queryable:
worker/<type>/<worker_id>/__admin__
| 指令 | 说明 |
|---|---|
status |
查询副本状态 |
restart |
重启该副本,副本槽位(index)由管理循环驱动,重新生成新 worker_id(worker-<index>-<epoch>-<pid>)并经 liveliness 更新 |
stop |
停止该副本 |
drain |
优雅排空:摘除 liveliness/业务 Queryable 隔绝新请求,等待正在处理的在途请求完成后再退出,管理循环不重建(数量-1) |
rescale |
按 data.scale 把当前进程内该类型副本数调整为指定值(data:{"scale":N});两种口径:不带 instance_id 时为「全局对称分摊」(写回全局 count + 各实例均分),带 instance_id + absolute=true 且命中本实例时为「逐实例精细扩缩」(只改该实例绝对副本数),target 非本实例则安全忽略 |
config_set |
更新 [workers.config] 运行时配置(路由配置除外);成功则写回配置并重启该类型全部对应副本,任一失败返回失败信息 |
REST 管理端点(仅超管)
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/admin/workers |
盘点全部副本(inline 本地注册 + external liveliness 发现) |
POST |
/api/v1/admin/workers/restart |
重启单个副本(worker_type / worker_id 查询参数) |
POST |
/api/v1/admin/workers/<type>/stop |
停止 single 副本 / 某插件全部副本 |
GET |
/api/v1/admin/workers/<type>/config |
读取该插件的现有 [workers.config](展示当前值) |
POST |
/api/v1/admin/workers/<type>/config |
更新配置(body {updates:{k:v}}),下发并重启 |
POST |
/api/v1/admin/workers/<type>/scale |
调整该类型副本数(body {count:<全局总量>}):写全局 count → 清除逐实例定制回均分 → 广播全部存活实例,全局对称分摊 |
GET |
/api/v1/admin/workers/<type>/instances |
按外部进程实例(instance_id)分组读取该类型副本(供前端逐实例精细扩缩展示) |
POST |
/api/v1/admin/workers/<type>/scale_instance |
逐实例精细扩缩(body {instance_id, count}):只把指定实例的副本数调到 N,其余实例不动,持久化到 instcounts 覆盖表 |
旧版运行时未声明
__admin__管理能力时,管理指令会超时(HTTP 502,见日志提示)。
配置热更新(路由配置除外)
- worker 回写插件
config.toml的[workers]段(保留其他段),拒绝写保留键(路由/加载控制键如type/module/count等)。 Plugin.reload(new_config)在互斥锁下先on_cleanup()释放旧资源、再on_init()用新配置重建客户端(如 AI/DB client),使配置立即生效、无需重启网关。- 仅 index0 副本落盘避免并发写冲突,其余副本同样应用新配置;任一失败则返回失败明细且不触发重启方式的成功确认。
副本数量管理(扩缩容)
同类 worker 的副本数可随时调整,并持久化到配置(重启网关后保持):
- 扩容:新增副本槽位(
WorkerPool新增_managed_worker管理循环,新副本注册 liveliness 后可被负载均衡)。 - 缩容:随机挑选多余副本触发
drain(优雅排空)——先摘除 liveliness/业务 Queryable 隔绝新请求,等待正在处理的在途请求完成后再关闭该副本(in-flight计数实现),避免中断业务;管理循环不重建。 - 目标数量持久化:
rescale把[workers].count写回config.toml,网关/进程重启后按新数量加载。 - 统一协议:inline 与 external 共用。网关对 inline 直接调整本进程
WorkerPool;对 external 经 Zenoh 向该类型全部存活实例广播rescale(不只是挑一个副本),由各实例按其 quota 扩缩并写回自身配置(无需重启整个进程/容器)。 - 逐实例 vs 全局:
/scale是对某个类型的全局对称扩缩;若外部进程有多个(多机/多容器),/scale会把全局总量均分到每个存活实例。要单独调整某一个外部实例的副本数,见下文「外部进程实例粒度扩缩」。
全局副本数协调(多网关)
多个网关共享同一份 chongming-cache(根 config.toml 的 [cache] 段)时,副本数「count」语义为该类 worker 的全局目标总量,由 chongming-cache 作为单一事实来源,各网关按在线数量自动分摊。这样只在任意一台网关执行扩缩容,即可全局生效。
-
单一事实来源:
count存于 cache 键worker:cm:counts:<type>,在线网关 id 列表存worker:cm:gateways,各网关分摊配额存worker:cm:quota:<type>:<gw>(可观测)。 -
网关登记/注销 + 心跳租约:网关启动时
register_gateway()加入在线列表,并每 15sheartbeat_gateway()刷新自身时间戳;超过租约(60s)未心跳的死网关会被自动清理(crash/kill -9 也不残留,避免配额失真)。当前由CHONGMING_GATEWAY_ID环境变量指定(多实例需唯一,尤其同宿主多网关必须显式指定;缺省回退hostname,稳定且重启不变,不残留);关闭时unregister_gateway()移除,其余网关实时重算分摊。 -
分摊算法:
own_quota()将全局总量按在线网关 sorted(id) 均分,余量按序补前若干。/admin/workers/<type>/scale(body{count:<全局总量>})写全局 count 后,各网关把自己的 session 副本调到自己的 quota。 -
共享方式与后端:单实例本机
sqlite即可;多实例必须共享同一 cache 后端(同一共享卷的 sqlite 或 redis),否则各写各的 cache 无法收敛成全局量。 -
启动优先级开关:根
config.toml的[cache].launch_worker_count决定网关启动时副本数的来源:[cache] backend = "sqlite" # "sqlite"(默认,本机共享) | "redis"(跨主机) path = "./cache_data/cache.db" launch_worker_count = "toml" # "toml"(默认)=启动用 toml 的 count 覆盖并写回 cache;"cache"=cache 为单一事实来源,toml 仅在缺失时引导 -
进程插件(external)实例粒度参与分摊:网关只对 inline 插件用 cache 解析启动副本数(见「启动流程」第 5 步
load_workers),external 插件的启动副本数由外部编排按自身配置决定;但运行时的缩放上,external 并不退化为“每进程全额”,而是同样在共享 cache 上登记实例参与方来分摊,避免多实例把副本翻倍:- 实例登记 + 心跳租约:每个 external 进程启动时向
worker:cm:instances:<type>登记自身参与方([instance_id, ts]),并每 15s 心跳续约;超过租约(60s)未心跳的死实例会被清理。divmod按存活参与方 N 均分全局 count(余量补靠前实例),N==1时退化为全额(单部署零影响)。 - 实例标识默认唯一:
--instance/ 环境变量CHONGMING_WORKER_INSTANCE显式指定(跨重启稳定、逐实例精细扩缩可持续命中);缺省回退{hostname}:{pid}——同一机器并行多个未指定--instance的进程也能靠 pid 区分,不会因为 id 相同被并成一个参与方导致副本翻倍或前端只显示一个实例。注意缺省 id 随进程重启变化。 - 广播 + 幂等收敛:
/scale写全局 count 后向该类型全部存活实例广播rescale,各实例读同一全局 count + 同一份参与方列表算出的 quota 一致,重复 rescale 也幂等,因此任意地收敛到同一预期状态。 - 前提:external 需共用共享
[cache](多实例 redis 或共享卷 sqlite);cache is None时退化为全额(仅单实例安全)。
- 实例登记 + 心跳租约:每个 external 进程启动时向
外部进程实例粒度扩缩(逐实例精细扩缩)
当同一个类型跑多个外部进程(多机/多容器)时,除了 scale 的全局对称扩缩,还可以只对某一个实例精确扩缩:
- 语义:把目标实例的副本数绝对值设为 N,其它实例不动;与全局
scale共存——全局对称是默认/一键复位,逐实例是定制覆盖。 - 覆盖表:每个实例的绝对目标存 cache 键
worker:cm:instcounts:<type>={instance_id: 绝对副本数}。有记录的实例按绝对数执行;没记录的实例回退到全局均分。一旦用scale_instance定制了某实例,之后scale(全局)会清除整张覆盖表回到均分(显式“复位”),因此两者不会互相干扰。 - 命中判断:rescale 的下发必须命中本实例才执行(
instance_id与register_instance一致),目标是其它实例则安全忽略,避免误缩。 - REST / UI:
POST /admin/workers/<type>/scale_instance(body{instance_id, count})下发;GET /admin/workers/<type>/instances供前端按实例分组展示当前副本数。前端用「调整数量」(全局对称)和「逐实例精细扩缩」表格(实例 id / 模式 / 当前副本数 / 目标 + 应用)两个入口。 - 协调前提:与外部广播缩放一致,需共享
[cache],且外部进程用稳定的--instance/CHONGMING_WORKER_INSTANCE(缺省{hostname}:{pid}虽唯一但重启会变,跨重启持续命中请显式指定)。
inline worker 宿主级领导选举(gunicorn 多 worker 兼容)
inline worker 的生命周期交给基于分布式锁的宿主级领导选举(取代“每 gunicorn worker 各自拉起”),key inline_owner:<gateway_id> 按网关隔离:
- 多 worker(同网关):同一时刻只有一个网关进程托管 inline worker(副本数精确等于
count,不再是worker 数 × count);其余进程只做 HTTP,经 Zenoh liveliness 发现并调用 inline worker。HTTP 层仍由所有 gunicorn worker 分担吞吐。 - 多实例(不同网关):各网关按自己的
gateway_id各自选举,互不抢占。 - 持有者崩溃自愈:非持有者进程经
refresh_workers周期性巡检(_maybe_heal_inline)——若发现某 inline 类型既无线活副本又无本地副本,就经分布式锁抢得领导权并拉起这些 worker(防 gunicorn 复活死 slot 却因租约未过期而跳过的问题)。接管同样set_nx唯一胜出,无双持有。 - 前提:同一共享
[cache](见「全局副本数协调」);单实例本机sqlite即可,多实例需共享后端。
多 worker 的边界(转发 vs 处理)
在 gunicorn 多 worker 模式下,结论是:多 worker 提升的是网关"HTTP 接入/转发"能力,不提升"worker 处理"能力——网关角色是代理,真正的 RPC 处理在 worker 端。
| 瓶颈所在 | 多 gunicorn worker 是否有效 | 正确扩法 |
|---|---|---|
| HTTP 接入/转发(高 QPS 转发、并发慢客户端、鉴权/路由) | ✅ 有效(上限≈核数) | 加 gunicorn -w |
| inline 插件实际计算 | ⚠️ 有限(多 worker 只是更快把请求塞给同一个 inline worker,inline 仍单点) | 加 worker 副本数 count / 改跑 external 进程 |
| external 进程插件处理 | ✅ 有效(多 worker 可并行转发给独立进程) | 加 gunicorn -w + 对等扩 external |
前端管理页
Worker 管理(/workers,深色主题):
- 登录:内置网关端点
POST /auth/login(auth 域拆分后登录/签发不依赖 auth 插件,插件缩容或故障时登录与 admin API 仍可用),领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。 - 鉴权:仅
is_superuser=true可管理;权限不足时前端/后端弹出对应提示(403)。 - 列表分页:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
- 查询与筛选:列表顶部统一筛选栏——关键字搜索(插件类型 / 副本 ID / 实例 ID)、模式筛选(全部 / inline / external)、副本数 ≥ N 过滤;同时作用于下方「逐实例精细扩缩」分组表(副本数过滤仅对分组表生效),筛选条件变化自动回到第一页。
- 操作:单副本重启、批量重启(选中)、插件级重启全部副本、更新插件配置(弹窗展示现有配置值,点行带入编辑;支持对象/数组等嵌套值的结构化 JSON 编辑与校验/格式化)、调整副本数量(当前数 +
el-input-number设目标 + 应用,可扩缩容)。 - 逐实例精细扩缩:按外部进程实例(instance_id)分组展示(实例 id / 模式 / 当前副本数),每行可设目标副本数并「应用」——仅调整该实例,其余实例不动;全局「调整数量」后分组表也会自动刷新。
- 自动刷新:页面每 10s 轮询总副本列表与逐实例分组表,扩缩容/重启/停止等异步结果无需手动刷新即可反映。
- auth 核心插件:配置不可更改,不在列表中展示(
Worker列表自动过滤worker_type == "auth")。 - 调小副本数会触发优雅排空并在前端确认提示中说明。
external(进程)插件同样可管理
external 副本走同一 __admin__ Queryable,路由发现、指令下发与扩缩容(rescale)与 inline 一致。前提:
- external 容器运行的是含管理能力(
__admin__+reload+rescale)的新版chongming-worker框架; config_set/rescale由副本在其进程 cwd 下写config/workers/<type>/config.toml,external 容器需挂载与网关同款配置目录才能写回网关侧配置(含[workers].count持久化);- 纯自定义语言(如 Rust
rsdemo)进程插件需自行实现__admin__Queryable + liveliness + 数量管理,否则只能发现、无法改写配置或扩缩容。
端点类型与路由注册
端点类型
TOML 文件中的节类型决定端点暴露方式:
| 节类型 | HTTP 路由 | OpenAPI 文档 | MQTT | 请求模型命名 |
|---|---|---|---|---|
[GET] |
✅ | ✅ | ✅ | XxxGetRequest / XxxGetResponse |
[POST] |
✅ | ✅ | ✅ | XxxPostRequest / XxxPostResponse |
[MQTT] |
❌ | ❌ | ✅ | XxxMqttRequest(无 response_model) |
两种路由注册方式
方式一:TOML 动态路由(标准 JSON 端点)
通过 config/workers/<插件>/*.toml 定义端点,自动生成 FastAPI 路由。适用于参数和响应可被 Pydantic 模型描述的标准接口。
# config/workers/example/add.toml
[GET]
summary = "加法运算"
params = ["a: float", "b: float"]
[GET.response_model]
result = { type = "float", required = true }
方式二:插件自定义路由(特殊端点)
当端点无法用 TOML 描述时(如 multipart/form-data 上传),插件暴露 APIRouter 实例,网关自动发现并注册。
# src/plugins/<name>/routes.py
from fastapi import APIRouter, UploadFile, File
router = APIRouter(prefix="/<prefix>", tags=["<worker_type>"])
@router.post("/upload")
async def upload(file: UploadFile = File(...)):
...
约定:插件
__init__.py中导入router实例(from .routes import router),网关通过getattr(module, "router", None)自动发现。
流式端点(SSE)
基于 Zenoh Query/Reply 原生"多回复"实现流式输出:Worker 的 @rpc_method 返回生成器时,
框架逐块 query.reply() 推送(见 utils/worker README「流式端点」),网关以 SSE
(text/event-stream)逐块转发给 HTTP 客户端。无额外端口、无额外进程、无新依赖;
未声明 stream 的端点行为与之前完全一致。
TOML 配置
在方法节中声明 stream = true(示例 config/workers/example/stream_chat.toml):
[POST]
summary = "模拟 AI 流式对话(SSE,逐 token 输出)"
auth_required = false
stream = true
timeout = 300.0 # 流总寿命上限(秒,zenoh query deadline 硬上限)
stream_idle_timeout = 10.0 # 网关侧连续无数据块的最大间隔(秒)
params = ["prompt: str", "tokens: int"]
| 字段 | 默认值 | 说明 |
|---|---|---|
stream |
false |
流式端点:以 SSE(text/event-stream)输出 |
timeout |
3.0 |
流总寿命上限(秒)。流式端点必须配大(如 300s),到期后 zenoh 丢弃后续回复 |
stream_idle_timeout |
10.0 |
空闲超时(秒):超过该间隔无新数据块则网关终止流 |
timeout(总寿命硬上限)与stream_idle_timeout(空闲超时)是两回事,流式场景两个都要配。
请求与响应
- 请求与普通端点一致(GET 走 query/path 参数,POST/PUT/PATCH 走 body 模型),鉴权依赖同样生效
- 响应为
text/event-stream,每块一个data: {json}\n\n事件(SSE 标准分帧) - 响应头:
Cache-Control: no-cache、X-Accel-Buffering: no(配合反代关闭缓冲) - OpenAPI 不声明流式端点的响应 schema(逐块 JSON dict,无法用单一模型描述)
curl -N -X POST localhost:8000/api/v1/calc/stream_chat \
-H 'Content-Type: application/json' -d '{"prompt":"你好","tokens":3}'
# data: {"token":"你好·1","index":0}
# data: {"token":"你好·2","index":1}
# data: {"token":"你好·3","index":2}
# data: {"done":true}
错误语义
| 场景 | 表现 |
|---|---|
| 无可用 worker(首个数据块前) | HTTP 503(与普通端点一致) |
| Worker 中途异常 / 流式端点初始化失败 | SSE error 事件 data: {"error":"..."} 收尾(HTTP 200 已提交,无法再改状态码) |
| 空闲超时 | SSE error 事件 data: {"error":"流式空闲超时 ..."} 收尾 |
| 客户端断开 | 网关终止迭代,Worker 侧 reply 失败自动停流,不空转 |
部署注意
生产环境经 nginx 反代时需关闭缓冲,否则 SSE 会被整段缓冲、失去逐块即时性:
location /api/v1/ {
proxy_buffering off;
}
本地联调前注意
流式能力在 chongming-worker 框架(utils/worker/,独立仓库)。本地未 push 的框架改动
不会自动生效——venv 里安装的是 uv sync 从 git 源拉取的旧版副本,旧版 @rpc_method
不认 stream 参数,会报 TypeError: rpc_method() got an unexpected keyword argument 'stream'
并连锁导致契约校验失败(启动终止)。本地联调时先以可编辑方式装入 venv:
uv pip install -e utils/worker
正式发布仍走「开发与发布检查清单」流程(工具包 push → uv lock --upgrade-package chongming-worker → uv sync,可编辑安装会被锁文件版本替换)。
测试
uv run python test_stream_worker.py # Worker 框架流式回复单测
uv run python test_stream_gateway.py # 网关 call_stream 单测
uv run python test_stream_e2e_zenoh.py # 真实 Zenoh 会话端到端(peer 模式,无需 router)
uv run python test_stream_route.py # 路由层 SSE 输出与兼容性回归(非流式端点不变)
进程插件与混合部署
两种插件运行模式按插件粒度共存,在 config/workers/<name>/config.toml 的 [workers] 节声明:
load_mode = "inline" # 默认:插件在网关进程内加载
load_mode = "external" # 独立进程运行,经 Zenoh 注册路由
inline 插件(src/plugins/,chongming-plugins 仓库):
- 网关进程内 import;路由由网关盘扫 TOML 生成;契约校验在网关启动时执行
external 进程插件(本地开发目录 workers/,自包含项目,不入库):
-
与网关零导入关系:只依赖
chongming-workerSDK + zenoh,仅经 Zenoh 通讯- 路由注册:worker 启动时 put 描述符到
chongming/routes/<type>/...,退出时 delete;每 10s 周期重发布自愈 - 服务发现:liveliness
worker/<type>/<id>(与 inline 副本同一机制,网关无需改动)
- 路由注册:worker 启动时 put 描述符到
-
worker 进程自读 TOML 并在自己进程内做契约校验(
chongming_worker.contracts); 网关 RouteRegistry 常驻订阅描述符后动态挂载/摘除 HTTP 路由(type 级 2s 防抖)。 路由注册与网关侧插件盘扫(config/workers/*/config.toml)解耦:网关容器无需 放置 external 插件的配置文件,盘扫发现的 external 条目仅用于启动日志观测; inline 插件仍按盘扫在网关进程内加载,不受影响 -
每个插件是自包含项目(
workers/<name>/= 打包 zip 根 = 容器工作目录, 开发即产物),代码结构与 inline 插件完全一致(Plugin 基类 + router/ + models/), 示例见本地workers/demo/;新项目用uv run gen_plugin <name> --process生成骨架 -
运行(在插件项目根
workers/<name>/下):cd workers/demo uv run chongming-worker --type demo # 本地调试(首次先 uv sync 装依赖) ./start.sh # 与容器一致的启动方式(uv sync + 启动) -
生产部署:进程插件与网关分别打包到不同容器
- 网关容器:
chongming+ 依赖 - 进程插件容器:插件代码 +
chongming-worker+ 依赖
- 网关容器:
-
隔离性由
test_worker_isolation.py保证(阻断 chongming 导入 + AST 检查 import 语句)
进程插件打包
build_plugin 把插件项目根直接压缩为可部署 zip(开发目录即产物,
插件代码 + 配置 + 依赖清单 + 容器入口都在 workers/<name>/ 内):
uv run build_plugin demo # → dist/demo.zip
uv run build_plugin demo --output /tmp/pkg
产物布局(= 项目根,zip 根即容器工作目录):
workers/<name>/ 插件代码(模块 workers.<name>)
config/workers/<name>/ 插件注册 + 端点配置
config/zenoh_config.json5 Zenoh 会话配置
config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需)
pyproject.toml uv 依赖清单(chongming-worker 内网 git 源)
start.sh 容器入口:uv sync + chongming-worker --type <name>
- 打包自动排除
__pycache__/*.pyc/.venv等,start.sh权限强制 0o755 - 插件扩展依赖(可选):直接在项目根
pyproject.toml的 dependencies 中追加 - 容器内部署:
unzip <name>.zip -d /app && cd /app && ./start.sh(镜像需预装 uv) - 打包前校验:
workers/<name>/项目根、项目内config/workers/<name>/config.toml(含[workers]节)、config/zenoh_config.json5与根config.toml(含[zenoh])必须存在,load_mode非external时告警 - 插件配置随 zip 部署在插件容器(worker 自读);网关侧无需放置(RouteRegistry 常驻订阅 Zenoh 路由描述符,与盘扫解耦)
MQTT 协议
Topic 约定
| 方向 | Topic | 说明 |
|---|---|---|
| 请求 | chongming/<type>/<op> |
MQTT → Worker |
| 响应 | chongming/<type>/<op>/response |
Worker → MQTT(None 时跳过) |
Broker 来源(内嵌 vs 外部)
网关默认启动内嵌 amqtt Broker(子进程 + 宿主级分布式排他锁单例),[<env>.mqtt].use_embedded_broker 控制 Broker 来源:
[development.mqtt]
enabled = true
host = "0.0.0.0" # true 时为内嵌 Broker 监听地址;false 时为外部 Broker 地址
port = 1883
use_embedded_broker = true # true(默认)网关启动 amqtt Broker;false 连接外部 Broker
use_embedded_broker = true(默认):网关启动内嵌 amqtt Broker 子进程,MQTT Bridge 作为客户端连回自身。use_embedded_broker = false:网关不启动 Broker,MQTT Bridge 直接连接host:port指向的外部 MQTT Broker(如独立部署的 EMQX/Mosquitto)。host需改为外部 Broker 地址,enabled = false时整个 MQTT 通道关闭。
单例与生命周期控制(取代旧文件锁):
- 内嵌 Broker 用
chongming-cache的DistributedLock(keymqtt_broker:<hostname>,按宿主隔离——内嵌 Broker 绑定本机端口),多 worker/多实例下同一宿主机只启动一个 Broker;cache未配置时跳过并告警。 - 持锁的网关在缓存登记其 Broker 子进程 PID(
mqtt_broker_owner:<hostname>)。前任持有者崩溃/被强杀而留下孤儿 Broker 子进程时,接管的网关据此 PID 识别并终止之,再启动由自己托管的新子进程(可停、可接管);否则端口被不可托管进程占用时仅复用、不托管。 - 关停时仅真正托管子进程的网关终止进程、清理登记并释放锁,绝不误伤他人。
Broker 认证(用户名/密码)
[<env>.mqtt].username / password 配置 Broker 认证(可选,默认匿名):
[development.mqtt]
enabled = true
host = "0.0.0.0"
port = 1883
username = "chongming" # 可选;内嵌/外部 Broker 均生效
password = "change-me"
- 内嵌 Broker(
use_embedded_broker=true):配置后启用认证(amqtt FileAuthPlugin + argon2 哈希),所有客户端(含网关 Bridge)必须携带该账号密码连接;未配置则保持匿名。 - 外部 Broker:网关 Bridge 以该账号密码连接,broker 侧需自行创建对应账号。
- 客户端连接示例:
mqtt://chongming:change-me@localhost:1883/(密码含特殊字符时需百分号编码,如@→%40;网关 Bridge 内部自动处理)。 - 注意:密码明文存放于配置,生产环境建议使用外部 Broker + 环境变量注入。
自定义订阅主题
除内置的 chongming/<type>/<op> 接口外,可将外部 MQTT 主题桥接到任意 Worker。推荐在插件自己的配置 config/workers/<type>/config.toml 中通过 [[workers.mqtt_topics]] 声明(目标 worker 即该插件本身):
# config/workers/aibox_sophon_adapter/config.toml
[[workers.mqtt_topics]]
topic = "box/aibox_sophon/board_ping" # 支持 +(单级)/ #(多级,须在末尾)通配符
subject = "aibox_sophon_adapter.board_ping" # 可选,{1} {2}... 对应通配符捕获值
response_topic = "/board_ping/response" # 可选,响应主题;默认 "<实际主题>/response"
subject缺省为<插件类型>.<topic 最后一段>;response_topic缺省为<实际匹配主题>/response。- 网关发现插件时自动收集所有
[[workers.mqtt_topics]],MQTT Bridge 一并订阅并路由。 - 多条自定义主题按配置顺序匹配,先匹配者优先;内置
chongming/...接口最后兜底。 - 全局
[<env>.mqtt].custom_topics仍可作为通用兜底(不依赖具体插件时使用,可选)。
MQTT Bridge 单例(多实例免重复响应)
MQTT Bridge 是网关进程内的 MQTT 客户端组件,负责订阅主题并把请求经 WorkerManager 转发给 Worker、回发 /response。为了让「同一条请求只被消费一次、只回一份响应」,Bridge 的启动采用基于共享缓存的分布式锁(chongming-cache 提供,DistributedLock),而不再使用本机文件锁。
- 抢锁机制:启动时
set_nx(SET NX + TTL 租约)原子抢锁,key 为mqtt_bridge;抢到锁的网关才启动 Bridge,其余网关跳过。 - 租约续约:持锁网关后台定期
touch续约;若持锁网关宕机/失联,租约到期后锁自动释放,其他网关可重新抢到(重启后生效)。 - 受保护释放:关闭时仅当 token 仍匹配(仍是自己持有)才删除,绝不误删其他网关抢到的锁。
- 持有者崩溃自愈接管:仅靠 gunicorn 复活死掉 slot 无法 failover(新进程可能在旧锁租约未过期时抢锁失败而照样跳过)。各网关进程内有周期性巡检(
mqtt_bridge_supervisor,周期经[<env>.mqtt].bridge_supervise_interval配置,默认 15s):非持有者进程若发现锁已空闲(原持有者租约过期=已死/失联),即经set_nx抢得唯一持有权并自动重建 Bridge;接管时同步按需拉起/接管内嵌 Broker。分布式锁保证任意时刻只有一方胜出,无分裂。
[cache]
backend = "redis" # 跨主机收敛必须用 redis(或共享卷 sqlite)
- 关键条件:跨机器收敛依赖共享缓存后端(与「全局副本数协调」同一
[cache],单一事实来源)。仅redis(或共享卷的sqlite)后端时,不同物理机上的网关才会看到同一把锁,真正全局单 Bridge。 use_embedded_broker = true(各机内嵌独立 Broker):各机 Broker 互不相连、本身不产生重复响应;此时全局单 Bridge 只会连到持锁网关那台机器的 Broker。use_embedded_broker = false(共享外部 Broker):原先 N 台网关的 N 个 Bridge 会共同订阅同一批主题,导致一条请求被各消费一次、各回一份响应;改用分布式锁后全局只有一个 Bridge 消费,彻底规避重复响应。
部署结论(内嵌 Broker 单例同样成立,mqtt_broker:<hostname> 按宿主隔离):
| 部署形态 | MQTT Broker | [cache] 后端 |
状态 |
|---|---|---|---|
| 单机(含多 worker) | 内嵌 amqtt | 本地 sqlite |
✅ 完全够用 |
| 多实例分布式 | 外部 Broker(必须,内嵌各机不聚合、接管会断设备) | redis(或共享卷 sqlite) |
✅ 推荐 |
关键:多实例是否收敛,MQTT 只是其中一环,真正的收敛前提是所有网关共享同一
[cache]后端。Bridge 全局锁、inline 领导选举锁、在线网关清单/quota 全部工作在同一共享 cache 上;若各实例用各自本机 cache,mqtt_bridge锁跨不了主机,每台都能抢到 Bridge,又会退化成 N 个 Bridge 各连各的 Broker → 单请求多响应,问题回归。即:单机 = 内嵌 Broker + 本地 sqlite;多实例 = 外部 Broker(必须)+ redis/共享卷 sqlite(选一)。
请求负载格式(信封协议)
MQTT 请求负载为信封结构,控制字段与业务数据分离:
{
"meta": {
"method": "GET", // 可选,默认 GET;MQTT 专用端点需显式 "MQTT"
"timeout": 30, // 可选,RPC 超时(秒),默认 30
"cacheable": false // 可选,是否允许结果缓存,默认 false
},
"data": { "a": 3, "b": 5 } // 业务参数,传给 Worker
}
- 非信封格式的负载会被网关丢弃并告警(严格协议,设备侧请按此格式发布)。
- HTTP 通道由网关自动组装信封(注入 method / user_info),无需调用方关心。
快速测试
# 终端 1:启动服务
uv run serve
# 终端 2:端到端测试
python test_mqtt_e2e.py
# 手动测试
python subscriber.py # 监听所有响应
python publisher.py example add '{"a":3,"b":5}' # 调用 add
开发指南
添加新 Worker 插件
1. 创建 src/plugins/<name>/ — __init__.py(Plugin 子类), global_var.py, router/, models/
2. 创建 config/workers/<name>/config.toml,在 [workers] 节注册插件
(type / module / factory / cleanup / count + [workers.config] 运行时配置)
3. 创建 config/workers/<name>/*.toml 端点定义
4. 运行 uv run gen_models 生成模型
5. 实现 router/<op>.py,引用 models/pydantic.py 中的模型
6. 更新 router/__init__.py 导入
推荐用脚手架一步生成骨架:
uv run gen_plugin <name> [operation ...]。 启动时网关会做契约校验:TOML 路由缺少对应@rpc_method将导致启动失败, 模型缺失/孤儿等不一致以警告日志提示。
为现有插件添加新端点
1. 创建 config/workers/<name>/<op>.toml
2. 运行 uv run gen_models
3. 创建 router/<op>.py → 引用生成的模型
4. 更新 router/__init__.py
5. 重启服务
流式端点(SSE)在此基础上额外两步:TOML 方法节声明 stream = true 并配大 timeout;
@rpc_method(..., stream=True) 函数返回生成器(yield 逐块),详见上文「流式端点(SSE)」。
常用命令
uv run serve # 启动开发服务
uv run gen_plugin <name> # 生成统一风格插件骨架
uv run gen_models # 生成 Pydantic 模型
uv run build # 构建打包(PyInstaller/Nuitka)
uv run chongming-worker --type <name> # 独立 Worker 进程(项目根下本地调试,load_mode=external)
chongming-worker --type <name> # 独立 Worker 进程(容器入口)
uv pip install -e utils/<name> # 安装/更新工具包
插件列表
| 插件 | 路径 | 功能 |
|---|---|---|
| auth | src/plugins/auth/ |
用户管理插件:注册/资料/改密(登录与 token 签发已内置网关,Users 模型/种子/校验/签发共享于 chongming-auth 包) |
| auth_oauth | src/plugins/auth_oauth/ |
身份源插件示例:GitHub OAuth + 手机验证码登录,基于 chongming-auth 契约签发平台 token |
| doubao | src/plugins/doubao/ |
豆包视觉理解,支持 AICC 安全通信通道 |
| aibox_sophon_adapter | src/plugins/aibox_sophon_adapter/ |
AI Box 设备心跳监控、离线检测、告警上报与查询、视频上传 |
| example | src/plugins/example/ |
四则运算 + 心跳演示(含 MQTT 端点示例) |
| demo(进程插件示例) | workers/demo/(本地自包含项目,不入库) |
独立进程运行(load_mode=external),经 Zenoh 注册路由 |
工具包列表
| 包名 | 路径 | 语言 | 说明 |
|---|---|---|---|
| chongming-auth | utils/auth/ |
Python | auth 域共享契约:Users 模型、建库/种子、登录校验与 JWT 签发(网关内置登录与 auth 插件共用) |
| chongming-config | utils/config/ |
Python | TOML 配置加载,TypedDict 类型安全 |
| chongming-database | utils/database/ |
Python | aiosqlite + SQLModel 异步数据库封装 |
| chongming-jwt | utils/jwt/ |
Rust + PyO3 | JWT 编解码,支持 HS256/384/512 + RS256/384/512 |
| chongming-password | utils/password/ |
Rust + PyO3 | Argon2id 密码哈希与验证 |
| chongming-request | utils/request/ |
Python | Zenoh RPC 直连客户端 |
| chongming-worker | utils/worker/ |
Python | Worker 框架核心:装饰器、运行时、契约校验、独立进程入口(chongming-worker) |
| chongming-stream-manager | utils/stream_manager/ |
Python | ffmpeg + MediaMTX RTSP 流代理管理 |
| bytedance | utils/bytedance/ |
Python | 字节跳动 AICC 安全通信 SDK(Jeddak 等) |
配置说明
- 应用配置:根目录
config.toml,包含环境、服务器、安全、MQTT 等全局设置 - 插件加载模式:按插件粒度声明在
config/workers/<name>/config.toml的[workers]节load_mode = "inline"(默认):插件在网关进程内加载load_mode = "external":插件以独立进程运行(进程编排交给 Docker/K8s, 进程入口uv run chongming-worker --type <name>),经 Zenoh 在chongming/routes/**注册路由,网关动态挂载 HTTP 端点(RouteRegistry 常驻订阅,无需在网关侧保留插件配置)- 两种模式可同时存在(混合部署)
- 进程插件与网关完全隔离:进程插件只依赖
chongming-workerSDK + zenoh, 与网关(chongming包)零导入关系,仅通过 Zenoh(chongming/routes/**注册路由、worker/<type>/<id>服务发现)通讯。workers/目录仅为开发便利, 生产部署时进程插件与网关分别打包到不同容器:- 网关容器:
chongming(含src/chongming/)+ 依赖 - 进程插件容器:插件代码(
workers/<name>/)+chongming-worker+ 依赖, 入口chongming-worker --type <name>), 打包:uv run build_plugin <name>→dist/<name>.zip(见「进程插件打包」)
- 网关容器:
- Zenoh 配置:
config/zenoh_config.json5 - 插件注册:
config/workers/<name>/config.toml的[workers]节,定义加载方式、副本数与运行时配置(旧全局config/workers/config.toml仅作兜底兼容) - 路由定义:
config/workers/<name>/*.toml,每个端点一个文件
详细配置说明见 config/README.md。
构建部署
# PyInstaller 打包
uv run build pyinstaller
# Nuitka 打包
uv run build nuitka
# 仅重新打包 .mbank 模块文件(第三方依赖不变时,更新只需替换 mbank 文件)
uv run build --only mbank
# 只执行指定步骤,跳过其余流程
uv run build --only mbank,prepare,vue
构建过程包括:模块加密打包 → 资源复制 → PyInstaller/Nuitka 打包 → 数据库初始化 → 前端编译 → VFS 挂载。
--only <step> 可选步骤:mbank(模块打包)、prepare(准备构建目录)、
obfuscate(PyArmor 混淆,仅 pyinstaller 模式)、pack(PyInstaller/Nuitka 打包)、
clean(清理临时文件)、vue(Vue 前端构建);可逗号分隔或重复传多个。
详见 插件开发指南。
常用命令
uv run serve # 启动开发服务器
uv run gen_plugin <name> # 生成统一风格插件骨架(--process 生成进程插件项目)
uv run gen_models # 生成 Pydantic 模型
uv run build pyinstaller # PyInstaller 打包
uv run build nuitka # Nuitka 打包
uv run build_plugin <name> # 进程插件打包 → dist/<name>.zip
chongming-worker --type <name> # 独立 Worker 进程(进程插件容器入口)
uv pip install -e utils/<name> # 安装/更新工具包子包
TOML 配置字段速查
| 字段 | 位置 | 说明 |
|---|---|---|
count |
<插件>/config.toml |
Worker 副本数([workers] 节) |
launch_worker_count |
根 config.toml |
启动时副本数来源:"toml"=用 toml count 覆盖 cache(默认);"cache"=以 cache 为单一事实来源([cache] 节) |
router_prefix |
<插件>/config.toml |
API 路由前缀 |
defaults.timeout |
<插件>/config.toml |
默认超时(秒) |
defaults.cacheable |
<插件>/config.toml |
是否缓存 RPC 结果 |
params |
<操作>.toml |
参数列表("name: type" 格式) |
auth_required |
<操作>.toml |
是否需要 JWT 鉴权 |
internal |
<操作>.toml |
true 则仅限内部 RPC,不注册 HTTP 路由 |
cacheable |
<操作>.toml |
端点是否可缓存 |
summary |
<操作>.toml |
端点描述(用于 OpenAPI / 日志) |
stream |
<操作>.toml |
true 则流式输出(SSE),需同时配大 timeout |
stream_idle_timeout |
<操作>.toml |
流式空闲超时(秒,默认 10),超时无新数据块则终止流 |
内置插件
| 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 |
|---|---|---|---|
| auth | /auth |
auth.* |
注册、个人信息、改密(登录 POST /auth/login 内置网关) |
| doubao | /doubao |
doubao.* |
火山引擎豆包视觉理解模型 |
| example | /calc |
example.* |
四则运算 + 访问计数 + MQTT 心跳示例 |
| aibox_sophon_adapter | /aibox |
aibox_sophon_adapter.* |
AI Box 设备心跳适配 + 视频上传 |