2026-08-27 17:06:03 +08:00
2026-08-19 11:44:29 +08:00
2026-08-27 10:13:03 +08:00
2026-08-27 17:06:03 +08:00
2026-08-27 17:06:03 +08:00
2026-07-21 15:39:47 +08:00
2026-08-27 17:06:03 +08:00
2026-08-27 17:06:03 +08:00
2026-07-27 14:25:29 +08:00
2026-08-27 17:06:03 +08:00

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>(工具包):

  1. 在 utils/<name> 内提交并 push 到对应远程
  2. 回主仓库执行 uv lock --upgrade-package <包名> 刷新版本 pin
  3. uv sync 后运行相关测试
  4. 主仓库提交 uv.lock

修改 src/plugins/ / config/ / src/chongming-web/:

  1. 在对应子仓库内提交并 push
  2. 回主仓库启动验证(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}),并把 [workers].count 写回本进程侧配置
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:N}),扩缩容 + 持久化

旧版运行时未声明 __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,由其所在进程内的池扩缩并写回自身配置(无需重启整个进程/容器)。

全局副本数协调(多网关)

多个网关共享同一份 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() 加入在线列表,并每 15s heartbeat_gateway() 刷新自身时间戳;超过租约(60s)未心跳的死网关会被自动清理(crash/kill -9 也不残留,避免配额失真)。当前由 CHONGMING_GATEWAY_ID 环境变量指定(多实例需唯一,尤其同宿主多网关必须显式指定;缺省回退 hostname,稳定且重启不变,不残留);关闭时 unregister_gateway() 移除,其余网关实时重算分摊。

  • 分摊算法:own_quota() 将全局总量按在线网关 sorted(id) 均分,余量按序补前若干。/admin/workers/<type>/scale(body {count:<全局总量>})写全局 count 后,各网关把自己的 session 副本调到自己的 quota。

  • 共享方式与后端:单实例本机 diskcache 即可;多实例必须共享同一 cache 后端(同一共享卷的 diskcache 或 redis),否则各写各的 cache 无法收敛成全局量。

  • 启动优先级开关:根 config.toml 的 [cache].launch_worker_count 决定网关启动时副本数的来源:

    [cache]
    backend = "diskcache"          # "diskcache"(默认) | "redis"
    cache_store_path = "./cache_data"
    launch_worker_count = "toml"   # "toml"(默认)=启动用 toml 的 count 覆盖并写回 cache;"cache"=cache 为单一事实来源,toml 仅在缺失时引导
    
  • 进程插件(external)不受该开关影响:网关只对 inline 插件用 cache 解析启动副本数(见「启动流程」第 5 步 load_workers);external 插件由外部编排按其自身配置 config/workers/<type>/config.toml 的 count 启动,不经网关、也不读 cache。运行时 scale 对 external 仍走「向存活副本下发 rescale」路径,同样与该开关无关。

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](见「全局副本数协调」);单实例本机 diskcache 即可,多实例需共享后端。

多 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,深色主题):

  • 登录:经 auth 插件 POST /auth/login 领取管理 Token(无需手填 JWT),从 JWT 解析当前用户与超管标识。
  • 鉴权:仅 is_superuser=true 可管理;权限不足时前端/后端弹出对应提示(403)。
  • 列表分页:展示副本(插件类型 / 副本 ID / 模式 inline|external / 状态),内置分页。
  • 操作:单副本重启、批量重启(选中)、插件级重启全部副本、更新插件配置(弹窗展示现有配置值,点行带入编辑)、调整副本数量(当前数 + el-input-number 设目标 + 应用,可扩缩容)。
  • auth 核心插件:配置不可更改,不在列表中展示(Worker 列表自动过滤 worker_type == "auth")。
  • 调小副本数会触发优雅排空并在前端确认提示中说明。

external(进程)插件同样可管理

external 副本走同一 __admin__ Queryable,路由发现、指令下发与扩缩容(rescale)与 inline 一致。前提:

  1. external 容器运行的是含管理能力(__admin__ + reload + rescale)的新版 chongming-worker 框架;
  2. config_set / rescale 由副本在其进程 cwd 下写 config/workers/<type>/config.toml,external 容器需挂载与网关同款配置目录才能写回网关侧配置(含 [workers].count 持久化);
  3. 纯自定义语言(如 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-worker SDK + zenoh,仅经 Zenoh 通讯

    • 路由注册:worker 启动时 put 描述符到 chongming/routes/<type>/...,退出时 delete;每 10s 周期重发布自愈
    • 服务发现:liveliness worker/<type>/<id>(与 inline 副本同一机制,网关无需改动)
  • 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(key mqtt_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(或共享卷 diskcache)
  • 关键条件:跨机器收敛依赖共享缓存后端(与「全局副本数协调」同一 [cache],单一事实来源)。仅 redis(或共享卷的 diskcache)后端时,不同物理机上的网关才会看到同一把锁,真正全局单 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 本地 diskcache ✅ 完全够用
多实例分布式 外部 Broker(必须,内嵌各机不聚合、接管会断设备) redis(或共享卷 diskcache) ✅ 推荐

关键:多实例是否收敛,MQTT 只是其中一环,真正的收敛前提是所有网关共享同一 [cache] 后端。Bridge 全局锁、inline 领导选举锁、在线网关清单/quota 全部工作在同一共享 cache 上;若各实例用各自本机 cache,mqtt_bridge 锁跨不了主机,每台都能抢到 Bridge,又会退化成 N 个 Bridge 各连各的 Broker → 单请求多响应,问题回归。

即:单机 = 内嵌 Broker + 本地 diskcache;多实例 = 外部 Broker(必须)+ redis/共享卷 diskcache(选一)。

请求负载格式(信封协议)

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/ 用户注册/登录/资料管理,JWT 鉴权,管理员初始化
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-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-worker SDK + 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.* 注册、登录、JWT 鉴权、个人信息、改密
doubao /doubao doubao.* 火山引擎豆包视觉理解模型
example /calc example.* 四则运算 + 访问计数 + MQTT 心跳示例
aibox_sophon_adapter /aibox aibox_sophon_adapter.* AI Box 设备心跳适配 + 视频上传

更多文档

S
Description
No description provided
Readme
6.8 MiB
bangze
Latest
2026-09-18 17:38:36 +08:00
Languages
Python 99.5%
Shell 0.5%