modify: sophon 视频文件上传、告警接口
This commit is contained in:
@@ -206,6 +206,89 @@ summary = "Add via MQTT"
|
||||
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 插件
|
||||
|
||||
1. 创建 `src/plugins/<name>/` — `__init__.py`, `global_var.py`, `router/`, `models/`
|
||||
@@ -246,10 +329,11 @@ uv pip install -e utils/<name> # 安装/更新工具包子包
|
||||
## 内置插件
|
||||
|
||||
| 插件 | HTTP 前缀 | MQTT 主题前缀 | 说明 |
|
||||
| ----------- | --------- | ------------- | ------------------------------------ |
|
||||
| ------------------------ | --------- | ------------------------ | ------------------------------------ |
|
||||
| **auth** | `/auth` | `auth.*` | 注册、登录、JWT 鉴权、个人信息、改密 |
|
||||
| **doubao** | `/doubao` | `doubao.*` | 火山引擎豆包视觉理解模型 |
|
||||
| **example** | `/calc` | `example.*` | 四则运算 + 访问计数 + MQTT 心跳示例 |
|
||||
| **aibox_sophon_adapter** | `/aibox` | `aibox_sophon_adapter.*` | AI Box 设备心跳适配 + 视频上传 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies = [
|
||||
"chongming-password",
|
||||
"chongming-worker",
|
||||
"amqtt>=0.11.3",
|
||||
"aiofile>=3.11.1",
|
||||
]
|
||||
description = "基于 modules-bank 的 fastapi 打包脚手架"
|
||||
name = "chongming-zenoh"
|
||||
|
||||
@@ -42,11 +42,11 @@ async def lifespan(app: FastAPI):
|
||||
# 4. WorkerManager
|
||||
manager = lifespan_setup.setup_worker_manager(app, session)
|
||||
|
||||
# 5. 加载 workers
|
||||
worker_tasks = await lifespan_setup.load_workers(app, session, manager)
|
||||
# 5. 加载 workers(同时自动发现插件自定义路由)
|
||||
worker_tasks, custom_routers = await lifespan_setup.load_workers(app, session, manager)
|
||||
|
||||
# 6. 注册路由
|
||||
await lifespan_setup.register_routes(app, manager)
|
||||
# 6. 注册路由(含 TOML 动态路由 + 插件自定义路由)
|
||||
await lifespan_setup.register_routes(app, manager, custom_routers)
|
||||
|
||||
# 7. 启动 MQTT Bridge(内嵌 Broker + Worker 桥接)
|
||||
await lifespan_setup.setup_mqtt(app, manager)
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
加载配置中的 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_conf = get_workers_config(workers_config_path).get("workers", [])
|
||||
logger.info(f"读取到 {len(workers_conf)} 个 worker 配置")
|
||||
|
||||
worker_tasks = []
|
||||
custom_routers = []
|
||||
for wc in workers_conf:
|
||||
module_name = wc.get("module", "")
|
||||
factory_name = wc.get("factory", "")
|
||||
@@ -237,6 +242,15 @@ async def load_workers(app: FastAPI, session: zenoh.Session, manager: WorkerMana
|
||||
|
||||
try:
|
||||
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)
|
||||
if inspect.iscoroutinefunction(factory):
|
||||
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}")
|
||||
|
||||
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"]
|
||||
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):
|
||||
"""启动 MQTT Broker 子进程(原子锁确保多 worker 仅启动一个)"""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# src/chongming/app/routes/storage.py
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import urllib.parse
|
||||
from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Request
|
||||
from fastapi.responses import FileResponse
|
||||
from aiofile import AIOFile
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/v1/storage", tags=["Storage"])
|
||||
|
||||
@@ -21,8 +22,9 @@ async def upload_file(
|
||||
dest = os.path.join(upload_root, object_key)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
|
||||
with open(dest, "wb") as f:
|
||||
shutil.copyfileobj(file.file, f)
|
||||
async with AIOFile(dest, "wb") as afp:
|
||||
while chunk := await file.read(1024 * 1024):
|
||||
await afp.write(chunk)
|
||||
|
||||
file_size = os.path.getsize(dest)
|
||||
return {
|
||||
@@ -33,7 +35,6 @@ async def upload_file(
|
||||
"size": file_size,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/download/{file_key:path}")
|
||||
async def download_file(request: Request, file_key: str):
|
||||
"""从本地存储下载文件"""
|
||||
|
||||
@@ -2,6 +2,18 @@ version = 1
|
||||
revision = 3
|
||||
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]]
|
||||
name = "aiosqlite"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "certifi"
|
||||
version = "2026.6.17"
|
||||
@@ -225,6 +258,7 @@ name = "chongming-zenoh"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiofile" },
|
||||
{ name = "amqtt" },
|
||||
{ name = "apscheduler" },
|
||||
{ name = "chongming-config" },
|
||||
@@ -258,6 +292,7 @@ dev = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofile", specifier = ">=3.11.1" },
|
||||
{ name = "amqtt", specifier = ">=0.11.3" },
|
||||
{ name = "apscheduler" },
|
||||
{ name = "chongming-config", git = "http://124.71.68.6:3000/chongming/chongming-utils-config.git" },
|
||||
|
||||
Reference in New Issue
Block a user