modify: sophon 视频文件上传、告警接口

This commit is contained in:
2026-07-28 14:13:43 +08:00
parent 8db26c08a0
commit 292605e446
6 changed files with 159 additions and 19 deletions
+85 -1
View File
@@ -206,6 +206,89 @@ summary = "Add via MQTT"
params = ["a: float", "b: float"] params = ["a: float", "b: float"]
``` ```
### 两种路由注册方式
本项目支持两种路由注册方式,根据场景选用:
#### 方式一:TOML 动态路由(标准 JSON 端点)
通过 `config/workers/<插件>/*.toml` 定义 `[GET]` / `[POST]` 节,网关自动生成 FastAPI 路由。
适用于参数和响应可被 Pydantic 模型描述的标准 JSON 接口。
- 路由生成流程:`TOML 配置 → 自动生成 Pydantic 模型 → 注册 FastAPI 路由`
- 底层 RPC:`FastAPI 路由 → WorkerManager.call() → Zenoh Query → Worker @rpc_method → 响应`
```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` 实例,网关在 `load_workers()` 阶段自动发现并注册(无需修改网关代码)。
**约定**:插件模块(`__init__.py`)需导入并暴露一个名为 `router` 的 `APIRouter` 实例:
```python
# src/plugins/<name>/__init__.py
from .routes import router # 暴露给网关自动发现
```
```python
# src/plugins/<name>/routes.py
from fastapi import APIRouter, UploadFile, File, Form
router = APIRouter(prefix="/<prefix>", tags=["<worker_type>"])
@router.post("/upload")
async def upload(
file: UploadFile = File(...),
param1: str = Form(...),
):
...
return {"result": "ok"}
```
- 全局前缀(如 `/api/v1`)由网关自动注入,插件无需关心
- tags 应与 TOML 自动生成的端点保持一致(使用 worker_type)
- TOML 配置中对应端点应设 `internal = true`,避免重复注册
### HTTP / MQTT 调用路径
同一端点通过**不同通道**到达 Worker 的路径对比:
| 通道 | 调用路径 | 适用场景 |
| -------- | --------------------------------------------------------------------------------------- | ------------------------------ |
| **HTTP** | `客户端 → FastAPI 路由 → WorkerManager.call() → Zenoh Query → Worker` | REST API、Web 前端、文件上传 |
| **MQTT** | `MQTT 客户端 → 内嵌 Broker → MQTT Bridge → WorkerManager.call() → Zenoh Query → Worker` | IoT 设备上报、心跳、低带宽环境 |
HTTP 和 MQTT 最终都通过 Zenoh RPC 调用 Worker 的 `@rpc_method` 处理函数,业务逻辑完全共享。
```
┌──────────────────────────────┐
│ WorkerManager │
│ (负载均衡 / 超时重试) │
└──────────┬───────────────────┘
│ Zenoh Query
┌──────────▼───────────────────┐
│ Worker 副本池 │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ W1 │ │ W2 │ │ W3 │ │
│ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │
│ @rpc_method 处理函数 │
└────────────────────────────────┘
HTTP 请求到达 ▲ ▲ MQTT 消息到达
┌───────┘ └───────┐
FastAPI 路由(TOML 自动生成) MQTT Bridge
插件自定义路由(router 约定) (chongming/+/+)
```
### 添加新 Worker 插件 ### 添加新 Worker 插件
1. 创建 `src/plugins/<name>/` — `__init__.py`, `global_var.py`, `router/`, `models/` 1. 创建 `src/plugins/<name>/` — `__init__.py`, `global_var.py`, `router/`, `models/`
@@ -246,10 +329,11 @@ uv pip install -e utils/<name> # 安装/更新工具包子包
## 内置插件 ## 内置插件
| 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 | | 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 |
| ----------- | --------- | ------------- | ------------------------------------ | | ------------------------ | --------- | ------------------------ | ------------------------------------ |
| **auth** | `/auth` | `auth.*` | 注册、登录、JWT 鉴权、个人信息、改密 | | **auth** | `/auth` | `auth.*` | 注册、登录、JWT 鉴权、个人信息、改密 |
| **doubao** | `/doubao` | `doubao.*` | 火山引擎豆包视觉理解模型 | | **doubao** | `/doubao` | `doubao.*` | 火山引擎豆包视觉理解模型 |
| **example** | `/calc` | `example.*` | 四则运算 + 访问计数 + MQTT 心跳示例 | | **example** | `/calc` | `example.*` | 四则运算 + 访问计数 + MQTT 心跳示例 |
| **aibox_sophon_adapter** | `/aibox` | `aibox_sophon_adapter.*` | AI Box 设备心跳适配 + 视频上传 |
--- ---
+1
View File
@@ -25,6 +25,7 @@ dependencies = [
"chongming-password", "chongming-password",
"chongming-worker", "chongming-worker",
"amqtt>=0.11.3", "amqtt>=0.11.3",
"aiofile>=3.11.1",
] ]
description = "基于 modules-bank 的 fastapi 打包脚手架" description = "基于 modules-bank 的 fastapi 打包脚手架"
name = "chongming-zenoh" name = "chongming-zenoh"
+4 -4
View File
@@ -42,11 +42,11 @@ async def lifespan(app: FastAPI):
# 4. WorkerManager # 4. WorkerManager
manager = lifespan_setup.setup_worker_manager(app, session) manager = lifespan_setup.setup_worker_manager(app, session)
# 5. 加载 workers # 5. 加载 workers(同时自动发现插件自定义路由)
worker_tasks = await lifespan_setup.load_workers(app, session, manager) worker_tasks, custom_routers = await lifespan_setup.load_workers(app, session, manager)
# 6. 注册路由 # 6. 注册路由(含 TOML 动态路由 + 插件自定义路由)
await lifespan_setup.register_routes(app, manager) await lifespan_setup.register_routes(app, manager, custom_routers)
# 7. 启动 MQTT Bridge(内嵌 Broker + Worker 桥接) # 7. 启动 MQTT Bridge(内嵌 Broker + Worker 桥接)
await lifespan_setup.setup_mqtt(app, manager) await lifespan_setup.setup_mqtt(app, manager)
+24 -5
View File
@@ -218,13 +218,18 @@ def setup_worker_manager(app: FastAPI, session: zenoh.Session) -> WorkerManager:
async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerManager): async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerManager):
""" """
加载配置中的 worker 插件,启动对应的 worker 任务。 加载配置中的 worker 插件,启动对应的 worker 任务。
返回任务列表。
同时自动发现插件模块中暴露的 APIRouter(约定:模块顶层名为 `router` 的 APIRouter 实例),
收集后供 register_custom_routes() 统一注册,实现网关与插件的解耦。
返回: (worker_tasks, custom_routers)
""" """
workers_config_path = config.get("plugins_worker", {}).get("config_path", "config/workers/config.toml") workers_config_path = config.get("plugins_worker", {}).get("config_path", "config/workers/config.toml")
workers_conf = get_workers_config(workers_config_path).get("workers", []) workers_conf = get_workers_config(workers_config_path).get("workers", [])
logger.info(f"读取到 {len(workers_conf)} 个 worker 配置") logger.info(f"读取到 {len(workers_conf)} 个 worker 配置")
worker_tasks = [] worker_tasks = []
custom_routers = []
for wc in workers_conf: for wc in workers_conf:
module_name = wc.get("module", "") module_name = wc.get("module", "")
factory_name = wc.get("factory", "") factory_name = wc.get("factory", "")
@@ -237,6 +242,15 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
try: try:
module = __import__(module_name, fromlist=[factory_name]) module = __import__(module_name, fromlist=[factory_name])
# 自动发现插件的自定义 FastAPI 路由(约定:模块暴露 `router` 属性)
plugin_router = getattr(module, "router", None)
if plugin_router is not None:
from fastapi import APIRouter
if isinstance(plugin_router, APIRouter):
custom_routers.append(plugin_router)
logger.info(f"发现插件自定义路由: {module_name}.router")
factory = getattr(module, factory_name) factory = getattr(module, factory_name)
if inspect.iscoroutinefunction(factory): if inspect.iscoroutinefunction(factory):
tasks = await factory(session, worker_config, count, worker_manager=manager) tasks = await factory(session, worker_config, count, worker_manager=manager)
@@ -248,13 +262,18 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
logger.error(f"加载 Worker 插件失败 {module_name}: {e}") logger.error(f"加载 Worker 插件失败 {module_name}: {e}")
app.state.worker_tasks = worker_tasks app.state.worker_tasks = worker_tasks
return worker_tasks return worker_tasks, custom_routers
async def register_routes(app: FastAPI, manager: WorkerManager): async def register_routes(app: FastAPI, manager: WorkerManager, custom_routers: list | None = None):
"""注册动态路由。""" """注册动态路由和插件自定义路由。"""
prefix = config["default"]["prefix"] prefix = config["default"]["prefix"]
register_dynamic_routes(app, manager, prefix=prefix) register_dynamic_routes(app, manager, prefix=prefix)
# 可选:等待路由注册生效(已在 register_dynamic_routes 内部完成日志) # 注册插件自定义路由(如 FORM-DATA 文件上传等 TOML 无法描述的端点)
# 统一加上全局 prefix(如 /api/v1),与 TOML 动态路由保持一致
if custom_routers:
for r in custom_routers:
app.include_router(r, prefix=prefix)
logger.info(f"已注册 {len(custom_routers)} 个插件自定义路由(prefix={prefix})")
def setup_mqtt_broker(app: FastAPI, host: str, port: int): def setup_mqtt_broker(app: FastAPI, host: str, port: int):
"""启动 MQTT Broker 子进程(原子锁确保多 worker 仅启动一个)""" """启动 MQTT Broker 子进程(原子锁确保多 worker 仅启动一个)"""
+5 -4
View File
@@ -1,10 +1,11 @@
# src/chongming/app/routes/storage.py # src/chongming/app/routes/storage.py
import os import os
import shutil
import uuid import uuid
import urllib.parse import urllib.parse
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Request from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Request
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from aiofile import AIOFile
router = APIRouter(prefix="/api/v1/storage", tags=["Storage"]) router = APIRouter(prefix="/api/v1/storage", tags=["Storage"])
@@ -21,8 +22,9 @@ async def upload_file(
dest = os.path.join(upload_root, object_key) dest = os.path.join(upload_root, object_key)
os.makedirs(os.path.dirname(dest), exist_ok=True) os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "wb") as f: async with AIOFile(dest, "wb") as afp:
shutil.copyfileobj(file.file, f) while chunk := await file.read(1024 * 1024):
await afp.write(chunk)
file_size = os.path.getsize(dest) file_size = os.path.getsize(dest)
return { return {
@@ -33,7 +35,6 @@ async def upload_file(
"size": file_size, "size": file_size,
} }
@router.get("/download/{file_key:path}") @router.get("/download/{file_key:path}")
async def download_file(request: Request, file_key: str): async def download_file(request: Request, file_key: str):
"""从本地存储下载文件""" """从本地存储下载文件"""
Generated
+35
View File
@@ -2,6 +2,18 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
[[package]]
name = "aiofile"
version = "3.11.1"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
dependencies = [
{ name = "caio" },
]
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/48/41/2fea7e193e061ce54eacc3b7bc0e6a99e4fcff43c78cf0a76dd781ed8334/aiofile-3.11.1.tar.gz", hash = "sha256:1f91912c6643d2a4e49ca4ae3514f0bf3867ce948a36d99a6411b8f4755f4cf9", size = 19342, upload-time = "2026-05-16T08:18:33.538Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" },
]
[[package]] [[package]]
name = "aiosqlite" name = "aiosqlite"
version = "0.22.1" version = "0.22.1"
@@ -81,6 +93,27 @@ wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024, upload-time = "2026-06-28T19:39:20.982Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024, upload-time = "2026-06-28T19:39:20.982Z" },
] ]
[[package]]
name = "caio"
version = "0.9.25"
source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple" }
sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
wheels = [
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
{ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.6.17" version = "2026.6.17"
@@ -225,6 +258,7 @@ name = "chongming-zenoh"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiofile" },
{ name = "amqtt" }, { name = "amqtt" },
{ name = "apscheduler" }, { name = "apscheduler" },
{ name = "chongming-config" }, { name = "chongming-config" },
@@ -258,6 +292,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiofile", specifier = ">=3.11.1" },
{ name = "amqtt", specifier = ">=0.11.3" }, { name = "amqtt", specifier = ">=0.11.3" },
{ name = "apscheduler" }, { name = "apscheduler" },
{ name = "chongming-config", git = "http://124.71.68.6:3000/chongming/chongming-utils-config.git" }, { name = "chongming-config", git = "http://124.71.68.6:3000/chongming/chongming-utils-config.git" },