Files
chongming-core/README.md
T
chakcy b4a5985fe6 modify: inline 插件配置按模式对齐(网关侧兜底,兼容未升级的 worker 包)
现象:cache 模式下应用重启后插件仍按 toml 跑,而前端(读缓存)显示的是缓存值。
根因:跑起来的产物里 chongming-worker 是索引里的发布版(uv run build / uv sync 会把
uv pip install -e utils/worker 的可编辑安装替换回锁文件版本),旧版 Plugin.create_workers
在 on_init 时只拿到 config/workers/<type>/config.toml 展平出来的配置。

改动(两层对齐,规则与 SDK 一致且幂等):
- WorkerManager.align_inline_config:创建 inline 副本之前按 [cache].launch_worker_count
  对齐 [workers.config]——cache 模式取缓存值(缺失才用 toml 引导并写回),toml 模式以
  文件为准并把该段覆盖写入缓存;未配置缓存后端时原样返回。网关侧这层让网关与 worker
  包版本解耦:worker 包仍是旧版时 cache 模式同样生效。
- _discover_plugins 透出 [workers.config] 展平前的原始段;load_workers 在注册 starter/
  创建副本之前完成对齐(插件实例 on_init 用的就是这份配置)。
- test/test_inline_config_align.py:两模式 + 无缓存/无段的边界用例,并校验缓存键与 SDK 一致。
- README:补打包陷阱(可编辑安装会被 uv sync 替换回发布版)与对齐时机说明。
2026-09-18 15:23:13 +08:00

879 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/改配置指令(超管),前端可视化列表 + 分页管理 |
---
## 项目结构
```text
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 前端 |
### 首次克隆(开发环境引导)
```bash
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`
> ⚠️ **打包陷阱("改了 utils/* 但产物行为没变"几乎都是它)**:`uv run build` 会先按
> 锁文件同步环境,**`uv pip install -e utils/<name>` 的可编辑安装会被替换回锁文件里的
> 发布版**。因此本地改了工具包后,必须走上面 1-4 步(push + 发版 → 刷新 pin → sync),
> 否则 `build/` 产物里仍是旧版代码。仅本地联调时可临时用可编辑安装,但打包前要确认
> venv 指向的是准备发布的版本:
>
> ```bash
> uv pip install -e utils/worker --no-deps # 仅联调:让 venv 用本地源码
> python -c "import chongming_worker, inspect; print(inspect.getsourcefile(chongming_worker))"
> ```
**修改 `src/plugins/` / `config/` / `src/chongming-web/`**:
1. 在对应子仓库内提交并 push
2. 回主仓库启动验证(`uv run serve` + 相关测试)
**发布前总检查**:
- [ ] 所有子仓库 `git status` 干净且已 push
- [ ] `uv.lock` 与各 git 源最新提交一致
- [ ] 打包前确认 venv 里 `chongming-worker` 等工具包为目标版本(改了 `utils/*` 却仍是旧版时,
产物行为不会变,见上一节打包陷阱)
- [ ] 配置中无硬编码凭据——密钥一律走环境变量:
`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](https://docs.astral.sh/uv/) 包管理器
- ffmpeg(可选,stream_manager 需要)
### 安装与启动
> 首次克隆:先按「多仓库结构与开发工作流」拉取 `src/plugins` / `config` / `src/chongming-web` 子仓库,再执行以下步骤。
```bash
# 同步依赖(自动编译 Rust 扩展 + 安装 Python 包)
uv sync
# 启动服务(开发模式,热重载)
uv run serve
```
服务启动后:
| 通道 | 地址 | 说明 |
| ------- | ---------------------------- | ---------------- |
| HTTP | `http://localhost:8000` | REST API |
| Swagger | `http://localhost:8000/docs` | OpenAPI 交互文档 |
| MQTT | `mqtt://localhost:1883` | 内嵌 Broker |
### 前端开发
```bash
cd src/chongming-web
npm install
npm run dev # http://localhost:5173
```
---
## 架构与数据流
```mermaid
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:
```text
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()` 加入在线列表,并每 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。
- **共享方式与后端**:单实例本机 `sqlite` 即可;**多实例必须共享同一 cache 后端**(同一共享卷的 sqlite 或 redis),否则各写各的 cache 无法收敛成全局量。
- **配置优先来源模式**:根 `config.toml` 的 `[cache].launch_worker_count` 决定启动时 **`[workers.config]` 与 `count` 从哪一侧取值**(两种模式下"前端显示 / 插件实际使用 / 重启后行为"三处都自洽):
```toml
[cache]
backend = "sqlite" # "sqlite"(默认,本机共享) | "redis"(跨主机)
path = "./cache_data/cache.db"
launch_worker_count = "toml" # "toml"(默认)=以文件为准;"cache"=以缓存为准
```
| 环节 | `"toml"`(默认) | `"cache"` |
| --- | --- | --- |
| 启动时 | 用 toml 的 `[workers.config]` 与 `count` **覆盖写入缓存**(前端读缓存,因此随之显示 toml 值) | 以缓存为准;插件初始化即读缓存值,缓存缺失才用 toml 引导并写回 |
| 插件实际使用 | toml 值 | 缓存值 |
| 管理接口改动的持久性 | 仅本次运行有效,重启网关被文件覆盖 | 跨重启保持 |
两种模式都不回写 `config.toml`:管理接口的改动只进缓存。`cache` 模式下启动日志会打印 toml 与缓存的差异键(以缓存为准);如需让 toml 的改动生效,先删除 `worker:cm:configs:<type>`(运行时配置)或 `worker:cm:counts:<type>`(副本数)缓存键再重启,或改用 `"toml"` 模式。`GET /admin/workers/<type>/config` 的响应带 `source`(cache/toml)与 `mode`,前端据此标注展示值的来路。
- **对齐时机(两层,规则一致且幂等)**:inline 插件由**网关**在创建副本之前对齐——`WorkerManager.align_inline_config()` 在收到展平配置与 `[workers.config]` 原始段后按模式取缓存/写缓存,再交给插件的 `create_workers`;worker SDK 的 `Plugin.create_workers` 在 `_bootstrap()`/`on_init()` 之前用 `resolve_startup_config()` 再解析一次。网关侧这层是为了让网关与 worker 包版本解耦:worker 包仍是旧版时 cache 模式同样生效(否则旧版 `on_init` 只拿到 toml 展平配置,表现为"前端显示缓存值、实际跑 toml")。**external 插件**没有网关这层兜底,由插件进程内的 SDK 解析,需要 worker 包为含该改动的版本。
- **进程插件(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` 时退化为全额(仅单实例安全)。
### 外部进程实例粒度扩缩(逐实例精细扩缩)
当同一个类型跑多个外部进程(多机/多容器)时,除了 `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 一致。**前提**:
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 模型描述的标准接口。
```toml
# 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` 实例,网关自动发现并注册。
```python
# 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`):
```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,无法用单一模型描述)
```bash
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 会被整段缓冲、失去逐块即时性:
```nginx
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:
```bash
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]` 节声明:
```toml
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>/` 下):
```bash
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>/` 内):
```bash
uv run build_plugin demo # → dist/demo.zip
uv run build_plugin demo --output /tmp/pkg
```
产物布局(= 项目根,zip 根即容器工作目录):
```text
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 来源:
```toml
[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 认证(可选,默认匿名):
```toml
[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 即该插件本身):
```toml
# 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。分布式锁保证任意时刻只有一方胜出,无分裂。
```toml
[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 请求负载为信封结构,控制字段与业务数据分离:
```json
{
"meta": {
"method": "GET", // 可选,默认 GET;MQTT 专用端点需显式 "MQTT"
"timeout": 30, // 可选,RPC 超时(秒),默认 30
"cacheable": false // 可选,是否允许结果缓存,默认 false
},
"data": { "a": 3, "b": 5 } // 业务参数,传给 Worker
}
```
- 非信封格式的负载会被网关丢弃并告警(严格协议,设备侧请按此格式发布)。
- HTTP 通道由网关自动组装信封(注入 method / user_info),无需调用方关心。
### 快速测试
```bash
# 终端 1:启动服务
uv run serve
# 终端 2:端到端测试
python test_mqtt_e2e.py
# 手动测试
python subscriber.py # 监听所有响应
python publisher.py example add '{"a":3,"b":5}' # 调用 add
```
---
## 开发指南
### 添加新 Worker 插件
```text
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` 将导致启动失败,
> 模型缺失/孤儿等不一致以警告日志提示。
### 为现有插件添加新端点
```text
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)」。
### 常用命令
```bash
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-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`](config/README.md)。
---
## 构建部署
```bash
# 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 前端构建);可逗号分隔或重复传多个。
详见 [插件开发指南](src/plugins/README.md)。
### 常用命令
```bash
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 的 `[workers.config]`/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 设备心跳适配 + 视频上传 |
---
## 更多文档
- [配置说明](config/README.md)
- [插件开发指南](src/plugins/README.md)
- [进程插件开发指南(external 模式,从开发到部署)](docs/process-plugin.md)
- [前端开发指南](src/chongming-web/README.md)
- [Worker 框架](utils/worker/README.md)