modify: aibox_sophon_adapter plugins

This commit is contained in:
2026-07-27 14:28:07 +08:00
parent 2b7b8ee08c
commit 79eb7fe5cc
23 changed files with 1380 additions and 61 deletions
+210 -12
View File
@@ -1,19 +1,217 @@
# 插件目录
# Worker 插件开发指南
每个子目录代表一个 Worker 插件,包含以下约定结构:
每个子目录代表一个独立的 Worker 插件,封装特定领域的业务逻辑。插件通过 Zenoh RPC 对外提供服务,
HTTP 路由由框架根据 TOML 配置自动生成。
---
## 目录结构约定
```text
plugin_name/
├── init.py # 提供 create_workers 和 cleanup 工厂函数
├── router/ # RPC 方法实现(使用 @rpc_method)
├── models/ # 由 gen_models 生成的 Pydantic 模型(自动生成,勿手动修改)
├── global_var.py # 全局状态管理(如数据库连接)
└── README.md # 插件说明
├── __init__.py # 入口:create_workers() 工厂 + cleanup()
├── global_var.py # 模块级全局状态(数据库连接、API 客户端等)
├── router/ # RPC 端点实现
│ ├── __init__.py # 导入所有端点模块,__all__ 导出
│ └── <操作名>.py # 每个端点一个文件,使用 @rpc_method 装饰器
├── models/ # 数据模型
│ ├── __init__.py # 模型导出(自动生成,勿手动修改)
│ ├── pydantic.py # Pydantic 请求/响应模型(由 gen_models.py 自动生成)
│ └── <表名>.py # SQLModel ORM 模型(如需要数据库)
└── README.md # 插件说明文档
```
## 开发新插件
---
1. 在 `config/workers/` 下创建同名目录,编写路由 TOML
2. 运行 `uv run gen_models config/workers/<plugin> src/plugins/<plugin>`
3. 实现 `router/*.py` 中的业务逻辑
4. 在 `__init__.py` 中实现 `create_workers` 工厂函数
## 开发新插件:完整流程
### 第一步:创建目录结构
```bash
mkdir -p src/plugins/<plugin_name>/router
mkdir -p src/plugins/<plugin_name>/models
touch src/plugins/<plugin_name>/__init__.py
touch src/plugins/<plugin_name>/global_var.py
touch src/plugins/<plugin_name>/router/__init__.py
```
### 第二步:注册插件
在 `config/workers/config.toml` 中添加:
```toml
[[workers]]
type = "<plugin_name>"
module = "plugins.<plugin_name>"
factory = "create_workers"
cleanup = "cleanup"
count = 3
[workers.config]
type = "<plugin_name>"
# 自定义配置项...
```
### 第三步:编写端点 TOML 配置
创建 `config/workers/<plugin_name>/config.toml`(分组配置):
```toml
[worker]
defaults = { timeout = 3.0, auth_required = true, cacheable = false, internal = false }
[worker.registration]
router_prefix = "/<prefix>"
```
创建每个操作的 `.toml` 文件,如 `config/workers/<plugin_name>/my_action.toml`:
```toml
[POST]
summary = "我的操作"
auth_required = true
[POST.response_model]
result = { type = "str", required = true }
message = { type = "str", default = "" }
```
### 第四步:生成 Pydantic 模型
```bash
uv run python -m chongming.scripts.gen_models
```
这会自动在 `src/plugins/<plugin_name>/models/` 下生成 `pydantic.py` 和更新 `__init__.py`。
### 第五步:实现 RPC 端点
`src/plugins/<plugin_name>/router/my_action.py`:
```python
from chongming_worker.decorators import rpc_method
from ..models.pydantic import MyActionPostRequest, MyActionPostResponse
@rpc_method(
subject="<plugin_name>.my_action",
method="POST",
response_model=MyActionPostResponse,
)
async def my_action(request: MyActionPostRequest) -> MyActionPostResponse:
"""业务逻辑实现"""
result = await do_something(request)
return MyActionPostResponse(result="success", message="done")
```
### 第六步:更新 router 导出
`src/plugins/<plugin_name>/router/__init__.py`:
```python
from . import my_action
__all__ = ["my_action"]
```
### 第七步:实现工厂函数
`src/plugins/<plugin_name>/__init__.py`:
```python
from chongming_worker.runtime import create_workers as _create_workers
def create_workers(session, config, count, worker_manager):
"""插件入口:初始化全局资源,启动 Worker"""
from . import router # 触发 @rpc_method 注册
from .global_var import set_db
# 初始化数据库、客户端等
from chongming_database import Database
db = Database(config["data_dir"] / "mydb.db")
# await db.initialize() -- 在异步环境中
set_db(db)
_create_workers(session, config, count, worker_manager)
async def cleanup(worker_manager):
"""清理资源"""
from .global_var import db
if db:
await db.dispose()
```
---
## 为现有插件添加新端点
1. 在 `config/workers/<plugin_name>/` 下创建 `<新操作>.toml`
2. 运行 `uv run python -m chongming.scripts.gen_models`
3. 在 `router/` 下创建 `<新操作>.py`,引用生成的模型
4. 更新 `router/__init__.py`
5. 重启服务
---
## 全局变量模式
每个插件通过 `global_var.py` 管理模块级共享资源,避免跨请求的状态冲突:
```python
# global_var.py
db: "Database | None" = None
def set_db(database: "Database"):
global db
db = database
```
在端点中直接引用:
```python
from ..global_var import db
@rpc_method(subject="plugin.action", method="GET", ...)
async def handler(request: RequestModel):
async with db.get_session() as session:
# 执行数据库操作
...
```
---
## 装饰器说明
```python
@rpc_method(
subject="<plugin>.<operation>", # Zenoh 主题(必需)
method="GET", # HTTP 方法:GET 或 POST
response_model=ResponseCls, # Pydantic 响应模型类
)
async def handler(request: RequestCls) -> ResponseCls:
...
```
- **GET 端点**:参数通过 URL query string 传递,在 TOML 中用 `params` 定义
- **POST 端点**:参数通过 JSON body 传递,自动绑定到 Pydantic 请求模型
- **internal 端点**:设置 `internal = true` 后,不注册 HTTP 路由,仅可通过 Zenoh RPC 内部调用
---
## 命名约定
| 概念 | 约定 | 示例 |
| ----------------- | --------------- | ----------------------------------- |
| 插件名 | 小写下划线 | `doubao`, `auth`, `example` |
| 操作名 | 小写下划线 | `analyze`, `add`, `change_password` |
| Zenoh subject | `<插件>.<操作>` | `example.add`, `doubao.analyze` |
| TOML 文件名 | 与操作名一致 | `analyze.toml`, `add.toml` |
| Python 路由文件名 | 与操作名一致 | `analyze.py`, `add.py` |
---
## 现有插件
| 插件 | 说明 | 文档 |
| ---------------------------- | -------------------------------------- | ---------------- |
| [auth](auth/README.md) | 用户注册、登录、JWT 鉴权、用户信息管理 | 含 SQLite 数据库 |
| [doubao](doubao/README.md) | 火山引擎豆包视觉理解模型集成 | 第三方 API 调用 |
| [example](example/README.md) | 四则运算示例 + 访问计数器 | 入门参考 |
+149
View File
@@ -0,0 +1,149 @@
"""
AI Box Sophon Adapter 插件
基于 MQTT 的 AI Box 设备心跳适配器,实现:
1. 通过 MQTT 主题 /board_ping 接收设备心跳上报
2. 解析设备系统参数(温度、硬盘、内存、通道、任务、TPU 等)
3. 持久化心跳数据到 SQLite 数据库
4. 提供 HTTP API 查询设备在线状态和详情
5. 后台定时检查离线设备,触发异常告警
心跳超时机制:
- 设备按配置间隔(默认 5 秒)上报心跳
- 服务端每 check_interval 秒检查一次
- 超过 heartbeat_timeout 秒未收到心跳的设备标记为离线
"""
import asyncio
import logging
from datetime import datetime
from pathlib import Path
from chongming_worker import auto_bind_models
from chongming_worker.runtime import create_workers as _create_workers
from .router import *
logger = logging.getLogger(__name__)
# 自动绑定 Pydantic 模型(用于 gen_models 生成的模型)
auto_bind_models("aibox_sophon_adapter")
# 离线检查任务引用
_offline_check_task: asyncio.Task | None = None
async def _check_offline_devices(interval: float):
"""
后台任务:定期检查离线设备
每隔 interval 秒扫描数据库,将超过 heartbeat_timeout 秒
未收到心跳的设备标记为离线,并输出告警日志。
"""
from .global_var import db, heartbeat_timeout
while True:
await asyncio.sleep(interval)
try:
if db is None:
continue
async with db.get_session() as session:
from .models.orm import BoardHeartbeat
from sqlalchemy import select
stmt = select(BoardHeartbeat).where(BoardHeartbeat.is_online == True)
result = await session.execute(stmt)
records = result.scalars().all()
now = datetime.now()
offline_count = 0
for record in records:
elapsed = (now - record.last_ping_time).total_seconds()
if elapsed > heartbeat_timeout:
record.is_online = False
record.updated_at = now
offline_count += 1
logger.warning(
f"[Offline] 设备 {record.board_id} (IP: {record.board_ip}) "
f"已离线 {elapsed:.0f} 秒(超时阈值: {heartbeat_timeout}s)"
)
if offline_count > 0:
await session.commit()
logger.info(f"[Offline] 本轮标记 {offline_count} 个设备为离线")
except Exception as e:
logger.error(f"[OfflineChecker] 检查异常: {e}")
async def create_workers(session, config, count=1, worker_manager=None):
"""
插件入口 — 初始化数据库和全局资源,创建 Worker 副本
由 FastAPI lifespan 在启动时调用。
"""
from .global_var import set_global_var, heartbeat_timeout as _default_timeout
from chongming_database import Database
# 读取心跳配置
worker_config = config.get("worker", {}).get("heartbeat", {}) if isinstance(config, dict) else {}
hb_interval = worker_config.get("interval", 5)
hb_timeout = worker_config.get("timeout", _default_timeout)
check_interval = worker_config.get("check_interval", 10)
# 设定全局变量
global heartbeat_timeout
heartbeat_timeout = hb_timeout
# 初始化数据库
data_dir = Path(config.get("data_dir", "./data")) if isinstance(config, dict) else Path("./data")
db_path = data_dir / "storage" / "chongming" / "aibox_sophon_adapter.db"
db_path.parent.mkdir(parents=True, exist_ok=True)
db = Database(db_path)
await db.initialize()
# 创建表
from .models.orm import BoardHeartbeat
async with db.get_session() as sess:
from sqlmodel import SQLModel
await sess.run_sync(lambda s: SQLModel.metadata.create_all(s.bind))
set_global_var("db", db)
set_global_var("heartbeat_timeout", hb_timeout)
set_global_var("heartbeat_interval", hb_interval)
logger.info(
f"[aibox_sophon_adapter] 已初始化: "
f"心跳间隔={hb_interval}s, 超时={hb_timeout}s, 检查间隔={check_interval}s"
)
# 启动离线设备检查后台任务
global _offline_check_task
_offline_check_task = asyncio.create_task(_check_offline_devices(check_interval))
# 创建 Worker 副本
return _create_workers(session, config, count, worker_manager)
async def cleanup(worker_item: str):
"""
清理资源
在 Worker 关闭时调用,关闭数据库连接和后台任务。
"""
from .global_var import db
global _offline_check_task
if _offline_check_task is not None and not _offline_check_task.done():
_offline_check_task.cancel()
try:
await _offline_check_task
except asyncio.CancelledError:
pass
logger.info("[aibox_sophon_adapter] 离线检查任务已停止")
if db is not None:
await db.dispose()
logger.info(f"[aibox_sophon_adapter] {worker_item} 清理完毕")
+36
View File
@@ -0,0 +1,36 @@
"""
AI Box Sophon Adapter — 全局变量
模块级共享资源管理。
"""
from typing import Any
from chongming_database import Database
# 数据库实例
db: Database | None = None
# 心跳超时配置(秒)
heartbeat_timeout: float = 15.0
# 心跳上报间隔(秒)
heartbeat_interval: float = 5.0
def set_global_var(name: str, value: Any) -> None:
"""
设置当前模块的全局变量。
Args:
name: 变量名(字符串)
value: 任意值
"""
globals()[name] = value
__all__ = [
"db",
"heartbeat_timeout",
"heartbeat_interval",
"set_global_var",
]
+45
View File
@@ -0,0 +1,45 @@
"""
模型导出 — aibox_sophon_adapter
包含 gen_models 自动生成的模型和手动定义的 board_models。
"""
from .pydantic import (
AiboxSophonAdapterResponse,
GetDeviceGetRequest,
GetDeviceGetResponse,
GetOfflineDevicesGetResponse,
ListDevicesGetResponse,
)
from .board_models import (
BoardPingRequest,
HostDiskInfo,
MediaInfo,
MediaStatusInfo,
AlgItemInfo,
AlgTaskStatusInfo,
TaskInfo,
TpuInfo,
DeviceSummary,
)
from .orm import BoardHeartbeat
__all__ = [
# 自动生成
"AiboxSophonAdapterResponse",
"GetDeviceGetRequest",
"GetDeviceGetResponse",
"GetOfflineDevicesGetResponse",
"ListDevicesGetResponse",
# 手动定义
"BoardPingRequest",
"HostDiskInfo",
"MediaInfo",
"MediaStatusInfo",
"AlgItemInfo",
"AlgTaskStatusInfo",
"TaskInfo",
"TpuInfo",
"DeviceSummary",
# ORM
"BoardHeartbeat",
]
+133
View File
@@ -0,0 +1,133 @@
"""
AI Box Sophon Adapter — 设备心跳数据 Pydantic 模型(手动定义)
定义 BoardPingRequest 及其嵌套子结构。
这些模型无法通过 gen_models 自动生成(嵌套层次过深),需要手动维护。
对应 MQTT 主题 /board_ping 的上报数据结构。
命名约定:
- Pydantic 模型类名以 Info 结尾(如 HostDiskInfo),避免与 JSON 字段名冲突
- 字段名直接对应 JSON key,无需 alias
"""
from typing import Optional
from pydantic import BaseModel, Field
# ============================================================
# 心跳数据子结构
# ============================================================
class HostDiskInfo(BaseModel):
"""设备硬盘使用情况"""
Available: int = Field(..., description="可用空间 (kB)")
Total: int = Field(..., description="总空间 (kB)")
Used: int = Field(..., description="已用空间 (kB)")
class MediaStatusInfo(BaseModel):
"""通道状态描述"""
label: str = Field(..., description="状态标签,如 '正常'")
type: int = Field(..., description="状态类型码")
class MediaInfo(BaseModel):
"""设备通道信息"""
MediaName: str = Field(..., description="通道名称")
MediaStatus: MediaStatusInfo = Field(..., description="通道状态")
MediaUrl: str = Field(..., description="通道视频流地址")
SubId: str = Field(default="", description="关联 GB28181 通道号")
class AlgItemInfo(BaseModel):
"""算法配置信息"""
lineDesc: str = Field(default="", description="线描述")
lineRequired: bool = Field(default=False, description="是否需要线")
majorId: int = Field(..., description="主 ID")
majorOnly: bool = Field(default=False, description="仅主算法")
minorId: int = Field(..., description="副 ID")
name: str = Field(..., description="算法名称")
zoneDesc: str = Field(default="", description="区域描述")
zoneRequired: bool = Field(default=False, description="是否需要区域")
class AlgTaskStatusInfo(BaseModel):
"""任务状态描述"""
label: str = Field(..., description="状态标签,如 '运行中'")
type: int = Field(..., description="状态类型码")
class TaskInfo(BaseModel):
"""设备任务信息"""
AlgItem: list[AlgItemInfo] = Field(default_factory=list, description="算法配置列表")
AlgTaskSession: str = Field(..., description="任务号")
AlgTaskStatus: AlgTaskStatusInfo = Field(..., description="任务状态")
class TpuInfo(BaseModel):
"""TPU 算力资源使用情况"""
mem_total: int = Field(..., description="内存总量")
mem_usage: int = Field(..., description="内存使用量")
tpu_usage: int = Field(..., description="TPU 使用率")
# ============================================================
# 心跳请求体(MQTT 上报的完整数据结构)
# ============================================================
class BoardPingRequest(BaseModel):
"""
AI Box 设备心跳上报数据
设备通过 MQTT 主题 /board_ping 定期上报。
上报间隔默认 5 秒。
包含字段:设备标识、IP、温度、硬盘、内存、GPS、
通道列表、任务列表、TPU 算力等。
"""
BoardId: str = Field(..., description="当前盒子唯一标识")
BoardIp: str = Field(..., description="设备 IP 地址")
BoardPlatform: str = Field(..., description="当前设备硬件类型,如 'BM-BOX'")
BoardTemp: str = Field(..., description="当前设备芯片温度,如 '44 (C)'")
BoardType: str = Field(..., description="当前设备接入方式 WAN/4G")
GBClientId: str = Field(default="", description="GB28181 设备号,配置后透传")
GrantCode: int = Field(..., description="授权码")
GrantDesc: str = Field(..., description="授权描述,如 '已授权'")
HostDisk: HostDiskInfo = Field(..., description="当前设备硬盘情况")
HostMemory: list[float] = Field(..., description="当前设备内存使用 [已使用(kB), 总利用率]")
Key: str = Field(..., description="运行状态关键字,如 '运行中'")
Latitude: float = Field(default=0.0, description="GPS 纬度")
Longitude: float = Field(default=0.0, description="GPS 经度")
Malloc: int = Field(..., description="当前应用 VmRSS 信息 (kB)")
Medias: list[MediaInfo] = Field(default_factory=list, description="当前设备通道信息")
Pid: int = Field(..., description="本次应用 PID")
Se6ip: str = Field(default="", description="特定设备时透传主控端 IP")
Status: str = Field(..., description="设备状态,如 '在线'")
System: str = Field(..., description="当前设备系统版本")
Tasks: list[TaskInfo] = Field(default_factory=list, description="当前设备任务信息")
Time: int = Field(..., description="当前时间戳(毫秒)")
Tpu: list[TpuInfo] = Field(default_factory=list, description="当前设备算力资源使用情况")
Version: str = Field(..., description="软件版本")
angleCourse: float = Field(default=0.0, description="GPS 角度")
kSpeed: float = Field(default=0.0, description="GPS 速度 (km/h)")
nSpeed: float = Field(default=0.0, description="GPS 速度 (m/s)")
# ============================================================
# 设备状态摘要(API 响应用)
# ============================================================
class DeviceSummary(BaseModel):
"""设备状态摘要(用于列表 API 响应)"""
board_id: str = Field(..., description="设备唯一标识")
board_ip: str = Field(..., description="设备 IP")
board_platform: str = Field(..., description="硬件类型")
board_type: str = Field(..., description="接入方式")
board_temp: str = Field(..., description="芯片温度")
status: str = Field(..., description="设备状态")
version: str = Field(..., description="软件版本")
is_online: bool = Field(..., description="是否在线")
last_ping_time: Optional[str] = Field(default=None, description="最后心跳时间")
grant_code: int = Field(..., description="授权码")
grant_desc: str = Field(..., description="授权描述")
+31
View File
@@ -0,0 +1,31 @@
"""
AI Box Sophon Adapter — ORM 模型
SQLModel 定义设备心跳记录表。
"""
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class BoardHeartbeat(SQLModel, table=True):
"""设备心跳记录表"""
__tablename__ = "board_heartbeat"
id: Optional[int] = Field(default=None, primary_key=True)
board_id: str = Field(index=True, unique=True, description="设备唯一标识 (BoardId)")
board_ip: str = Field(default="", description="设备 IP")
board_platform: str = Field(default="", description="硬件类型")
board_temp: str = Field(default="", description="芯片温度")
board_type: str = Field(default="", description="接入方式 WAN/4G")
status: str = Field(default="未知", description="设备状态")
version: str = Field(default="", description="软件版本")
system_info: str = Field(default="", description="系统版本信息")
grant_code: int = Field(default=0, description="授权码")
grant_desc: str = Field(default="", description="授权描述")
last_ping_time: datetime = Field(default_factory=datetime.now, description="最后心跳时间")
ping_data: str = Field(default="{}", description="完整心跳数据 JSON")
is_online: bool = Field(default=True, description="是否在线")
created_at: datetime = Field(default_factory=datetime.now, description="首次上报时间")
updated_at: datetime = Field(default_factory=datetime.now, description="最后更新时间")
+34
View File
@@ -0,0 +1,34 @@
"""
自动生成的 Pydantic 模型 — aibox_sophon_adapter
来源: config/workers/aibox_sophon_adapter/
"""
from __future__ import annotations
from typing import Any, Optional
from pydantic import BaseModel, Field
class GetDeviceGetRequest(BaseModel):
"""查询单个设备详情 — 请求体"""
board_id: str = Field(...)
class GetDeviceGetResponse(BaseModel):
"""查询单个设备详情 — 响应体"""
board_id: str = Field(...)
is_online: bool = Field(...)
last_ping_time: Optional[str] = Field(default=None)
ping_data: Optional[dict[str, Any]] = Field(default=None)
class GetOfflineDevicesGetResponse(BaseModel):
"""查询离线/异常设备列表 — 响应体"""
offline_devices: list[dict] = Field(...)
total: int = Field(...)
class ListDevicesGetResponse(BaseModel):
"""查询所有设备状态 — 响应体"""
devices: list[dict] = Field(...)
total: int = Field(...)
# 所有响应模型的联合类型
AiboxSophonAdapterResponse = ListDevicesGetResponse | GetOfflineDevicesGetResponse | GetDeviceGetResponse
+14
View File
@@ -0,0 +1,14 @@
"""
路由导出 — aibox_sophon_adapter
"""
from .board_ping import board_ping
from .list_devices import list_devices
from .get_device import get_device
from .get_offline_devices import get_offline_devices
__all__ = [
"board_ping",
"list_devices",
"get_device",
"get_offline_devices",
]
+94
View File
@@ -0,0 +1,94 @@
"""
AI Box Sophon Adapter — MQTT 心跳上报处理器
监听 MQTT 主题 /board_ping(通过 MQTT Bridge 路由到 chongming/aibox_sophon_adapter/board_ping),
解析设备心跳数据并更新数据库记录。
"""
import json
import logging
from datetime import datetime
from chongming_worker import rpc_method
from ..models.board_models import BoardPingRequest
logger = logging.getLogger(__name__)
@rpc_method(
subject="aibox_sophon_adapter.board_ping",
method="MQTT",
summary="AI Box 设备心跳上报",
request_model=BoardPingRequest,
)
async def board_ping(request: BoardPingRequest, worker_id: str = "") -> dict:
"""
处理设备心跳上报
设备通过 MQTT 主题 /board_ping 定期(默认 5 秒)上报系统状态。
收到心跳后更新数据库中的设备记录,标记为在线。
"""
from ..global_var import db, heartbeat_timeout
board_id = request.BoardId
try:
async with db.get_session() as session:
from ..models.orm import BoardHeartbeat
from sqlalchemy import select
stmt = select(BoardHeartbeat).where(BoardHeartbeat.board_id == board_id)
result = await session.execute(stmt)
record: BoardHeartbeat | None = result.scalar_one_or_none()
now = datetime.now()
ping_json = request.model_dump_json()
if record is None:
# 新设备,创建记录
record = BoardHeartbeat(
board_id=board_id,
board_ip=request.BoardIp,
board_platform=request.BoardPlatform,
board_temp=request.BoardTemp,
board_type=request.BoardType,
status=request.Status,
version=request.Version,
system_info=request.System,
grant_code=request.GrantCode,
grant_desc=request.GrantDesc,
last_ping_time=now,
ping_data=ping_json,
is_online=True,
)
session.add(record)
logger.info(f"[Heartbeat] 新设备上线: {board_id} (IP: {request.BoardIp})")
else:
# 已有设备,更新心跳
record.board_ip = request.BoardIp
record.board_platform = request.BoardPlatform
record.board_temp = request.BoardTemp
record.board_type = request.BoardType
record.status = request.Status
record.version = request.Version
record.system_info = request.System
record.grant_code = request.GrantCode
record.grant_desc = request.GrantDesc
record.last_ping_time = now
record.ping_data = ping_json
record.is_online = True
record.updated_at = now
await session.commit()
logger.debug(
f"[Heartbeat] {board_id}: temp={request.BoardTemp}, "
f"disk={request.HostDisk.Used}/{request.HostDisk.Total} kB, "
f"mem={request.HostMemory[0]:.1f} kB ({request.HostMemory[1]*100:.1f}%), "
f"medias={len(request.Medias)}, tasks={len(request.Tasks)}"
)
except Exception as e:
logger.error(f"[Heartbeat] 处理心跳失败 (board_id={board_id}): {e}")
return {"error": str(e)}
return {"received": True, "board_id": board_id}
+63
View File
@@ -0,0 +1,63 @@
"""
查询单个设备详情 — GET /aibox/get_device?board_id=xxx
"""
import json
import logging
from datetime import datetime
from chongming_worker import rpc_method
logger = logging.getLogger(__name__)
@rpc_method(
subject="aibox_sophon_adapter.get_device",
method="GET",
summary="查询单个设备详情",
)
async def get_device(board_id: str, worker_id: str = "") -> dict:
"""根据 BoardId 查询设备的完整心跳数据和在线状态"""
from ..global_var import db, heartbeat_timeout
try:
async with db.get_session() as session:
from ..models.orm import BoardHeartbeat
from sqlalchemy import select
stmt = select(BoardHeartbeat).where(BoardHeartbeat.board_id == board_id)
result = await session.execute(stmt)
record: BoardHeartbeat | None = result.scalar_one_or_none()
if record is None:
return {"error": f"设备不存在: {board_id}"}
now = datetime.now()
elapsed = (now - record.last_ping_time).total_seconds()
is_online = elapsed <= heartbeat_timeout
ping_data = {}
try:
ping_data = json.loads(record.ping_data) if record.ping_data else {}
except json.JSONDecodeError:
pass
return {
"board_id": record.board_id,
"board_ip": record.board_ip,
"board_platform": record.board_platform,
"board_type": record.board_type,
"board_temp": record.board_temp,
"status": record.status,
"version": record.version,
"system_info": record.system_info,
"grant_code": record.grant_code,
"grant_desc": record.grant_desc,
"is_online": is_online,
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
"elapsed_seconds": round(elapsed, 1),
"ping_data": ping_data,
}
except Exception as e:
logger.error(f"[get_device] 查询失败 (board_id={board_id}): {e}")
return {"error": str(e)}
@@ -0,0 +1,54 @@
"""
查询离线/异常设备 — GET /aibox/get_offline_devices
"""
import json
import logging
from datetime import datetime
from chongming_worker import rpc_method
logger = logging.getLogger(__name__)
@rpc_method(
subject="aibox_sophon_adapter.get_offline_devices",
method="GET",
summary="查询离线/异常设备列表",
)
async def get_offline_devices(worker_id: str = "") -> dict:
"""返回所有超过心跳超时时间未上报的设备列表"""
from ..global_var import db, heartbeat_timeout
offline_devices = []
try:
async with db.get_session() as session:
from ..models.orm import BoardHeartbeat
from sqlalchemy import select
stmt = select(BoardHeartbeat).order_by(BoardHeartbeat.last_ping_time.desc())
result = await session.execute(stmt)
records = result.scalars().all()
now = datetime.now()
for record in records:
elapsed = (now - record.last_ping_time).total_seconds()
is_online = elapsed <= heartbeat_timeout
if not is_online:
offline_devices.append({
"board_id": record.board_id,
"board_ip": record.board_ip,
"board_platform": record.board_platform,
"board_type": record.board_type,
"status": record.status,
"version": record.version,
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
"elapsed_seconds": round(elapsed, 1),
"is_online": False,
})
except Exception as e:
logger.error(f"[get_offline_devices] 查询失败: {e}")
return {"offline_devices": [], "total": 0, "error": str(e)}
return {"offline_devices": offline_devices, "total": len(offline_devices)}
@@ -0,0 +1,69 @@
"""
查询所有设备状态 — GET /aibox/list_devices
"""
import json
import logging
from datetime import datetime
from chongming_worker import rpc_method
logger = logging.getLogger(__name__)
@rpc_method(
subject="aibox_sophon_adapter.list_devices",
method="GET",
summary="查询所有设备状态",
)
async def list_devices(worker_id: str = "") -> dict:
"""返回所有已上报过心跳的设备及其在线状态"""
from ..global_var import db, heartbeat_timeout
devices = []
try:
async with db.get_session() as session:
from ..models.orm import BoardHeartbeat
from sqlalchemy import select
stmt = select(BoardHeartbeat).order_by(BoardHeartbeat.updated_at.desc())
result = await session.execute(stmt)
records = result.scalars().all()
now = datetime.now()
for record in records:
# 检查是否超时
elapsed = (now - record.last_ping_time).total_seconds()
is_online = elapsed <= heartbeat_timeout
ping_data = {}
try:
ping_data = json.loads(record.ping_data) if record.ping_data else {}
except json.JSONDecodeError:
pass
devices.append({
"board_id": record.board_id,
"board_ip": record.board_ip,
"board_platform": record.board_platform,
"board_type": record.board_type,
"board_temp": record.board_temp,
"status": record.status,
"version": record.version,
"system_info": record.system_info,
"grant_code": record.grant_code,
"grant_desc": record.grant_desc,
"is_online": is_online,
"last_ping_time": record.last_ping_time.isoformat() if record.last_ping_time else None,
"elapsed_seconds": round(elapsed, 1),
"host_disk": ping_data.get("HostDisk", {}),
"host_memory": ping_data.get("HostMemory", []),
"medias_count": len(ping_data.get("Medias", [])),
"tasks_count": len(ping_data.get("Tasks", [])),
"tpu": ping_data.get("Tpu", []),
})
except Exception as e:
logger.error(f"[list_devices] 查询失败: {e}")
return {"devices": [], "total": 0, "error": str(e)}
return {"devices": devices, "total": len(devices)}
+193
View File
@@ -0,0 +1,193 @@
# Auth Worker 插件
用户认证与授权插件,提供完整的用户管理系统。
---
## 功能
- **用户注册**:用户名 + 邮箱 + 密码注册,自动校验唯一性
- **用户登录**:JWT Token 签发,有效期 3 小时
- **获取用户信息**:根据 JWT Token 获取完整用户档案
- **修改密码**:旧密码验证 + 新密码更新
- **更新资料**:更新全名、邮箱、头像、手机号等信息
- **自动初始化**:首次启动自动创建 `admin` 管理员账户
---
## 数据库
使用 SQLite(通过 `chongming-database` 异步封装),数据库文件位于 `data/storage/chongming/auth.db`。
### Users 表结构
| 字段 | 类型 | 说明 |
| ----------------- | ----------------- | --------------------------------- |
| `id` | `str` (PK) | UUID 格式主键 |
| `username` | `str(50)` UNIQUE | 登录用户名 |
| `email` | `str(255)` UNIQUE | 电子邮箱 |
| `hashed_password` | `str(255)` | Argon2id 加密密码 |
| `full_name` | `str(100)` | 用户全名(可选) |
| `avatar_url` | `str(500)` | 头像链接(可选) |
| `phone` | `str(20)` | 手机号码(可选) |
| `is_active` | `bool` | 账户启用状态(默认 `true`) |
| `is_verified` | `bool` | 邮箱/手机验证状态(默认 `false`) |
| `is_superuser` | `bool` | 超级管理员标识(默认 `false`) |
| `created_at` | `datetime` | 创建时间 |
| `updated_at` | `datetime` | 最后更新时间 |
| `last_login_at` | `datetime` | 最后登录时间(可选) |
密码使用 Rust 实现的 **Argon2id** 哈希,内存成本 19456 KiB,迭代 2 次,并行度 1。
---
## API 端点
所有端点路由前缀为 `/auth`。
### `POST /auth/register` — 用户注册
无需认证。
```json
// 请求
{
"username": "john",
"password": "secret123",
"email": "john@example.com",
"full_name": "John Doe",
"phone": "13800138000"
}
// 响应
{
"result": "success",
"message": "注册成功",
"user_id": "a1b2c3d4_12345_1720000000.123",
"timestamp": 1720000000.123
}
```
### `POST /auth/login` — 登录
无需认证。
```json
// 请求
{
"username": "admin",
"password": "admin"
}
// 响应
{
"result": "success",
"token": "eyJhbGciOiJIUzI1NiIs...",
"message": "登录成功",
"timestamp": 1720000000.123
}
```
JWT Token 有效期 3 小时,载荷包含:`sub`(用户ID)、`username`、`email`、`full_name`、`is_superuser` 等。
### `GET /auth/me?user_id=<id>` — 获取用户信息
需要 JWT 鉴权(`Authorization: Bearer <token>`)。
```json
// 响应
{
"result": "success",
"id": "a1b2c3d4_12345_1720000000.123",
"username": "john",
"email": "john@example.com",
"full_name": "John Doe",
"avatar_url": null,
"phone": "13800138000",
"is_active": true,
"is_verified": false,
"is_superuser": false,
"created_at": "2024-01-01T00:00:00+00:00",
"updated_at": "2024-01-01T00:00:00+00:00",
"last_login_at": null
}
```
### `POST /auth/change_password` — 修改密码
需要 JWT 鉴权。
```json
// 请求
{
"user_id": "a1b2c3d4_12345_1720000000.123",
"old_password": "secret123",
"new_password": "newSecret456"
}
// 响应
{
"result": "success",
"message": "密码修改成功",
"timestamp": 1720000000.123
}
```
### `POST /auth/update_profile` — 更新用户资料
需要 JWT 鉴权。仅更新非空字段。
```json
// 请求
{
"user_id": "a1b2c3d4_12345_1720000000.123",
"full_name": "John Updated",
"email": "",
"avatar_url": "https://example.com/avatar.png",
"phone": ""
}
// 响应
{
"result": "success",
"message": "资料更新成功",
"timestamp": 1720000000.123
}
```
---
## 默认管理员账户
首次启动时自动创建:
| 字段 | 值 |
| ------------ | --------------------- |
| **用户名** | `admin` |
| **密码** | `admin` |
| **邮箱** | `admin@example.com` |
| **超管权限** | `is_superuser = true` |
> ⚠️ **生产环境务必修改默认密码!**
---
## 安全说明
- 密码使用 **Argon2id**(Rust 实现),对抗 GPU/ASIC 暴力破解
- JWT 使用 **HS256** 算法,密钥在 `config.toml` 的 `[security]` 节配置
- JWT 有效期 3 小时,过期后需重新登录
- 支持用户软禁用(`is_active = false`)
- 接口级鉴权控制:每个端点可通过 TOML 的 `auth_required` 独立配置
---
## 依赖
| 工具包 | 用途 |
| -------------------- | ------------------------------------ |
| `chongming-database` | 异步 SQLite 数据库操作 |
| `chongming-jwt` | Rust JWT 编码/验证 |
| `chongming-password` | Rust Argon2id 密码哈希 |
| `chongming-config` | 全局配置加载 |
| `chongming-worker` | `@rpc_method` 装饰器 + Worker 运行时 |
+62 -21
View File
@@ -1,38 +1,79 @@
# Doubao Worker 插件
火山引擎豆包(Doubao)视觉理解模型集成,支持图像分析。
火山引擎豆包(Doubao)视觉理解模型集成,支持图像分析与描述生成。
---
## 功能
- 接收 base64 编码的图片和提示词
- 调用火山引擎豆包视觉模型(支持思考模式)
- 返回模型描述结果
- 接收 **base64 编码**的图片和**提示词**(prompt)
- 调用火山引擎豆包视觉模型 API
- 支持**思考模式**(thinking mode),模型会先推理再输出结果
- 返回模型生成的图像描述文本
---
## 配置
在 `config/workers/doubao/config.toml` 中设置:
在 `config/workers/doubao/config.toml` 和 `config/workers/config.toml` 中设置:
- `api_key`:火山引擎 API Key(也可通过环境变量 `VOLC_ACCESSKEY` 提供)
- `base_url`:API 端点(默认 `https://ark.cn-beijing.volces.com/api/v3`)
- `model`:模型名称(如 `doubao-seed-evolving`)
## 调用示例
```http
POST /api/v1/doubao/analyze
Content-Type: application/json
{
"image_base64": "data:image/png;base64,iVBORw0KG...",
"prompt": "描述这个图片的内容"
}
```toml
[workers.config]
type = "doubao"
api_key = "<your-api-key>" # 火山引擎 API Key(也可通过环境变量 VOLC_ACCESSKEY 提供)
base_url = "https://ark.cn-beijing.volces.com/api/v3" # API 端点
model = "doubao-seed-evolving" # 模型名称
```
响应示例:
---
## API 端点
### `POST /doubao/analyze` — 图像分析
```json
// 请求
{
"result": "图片中是一只猫坐在沙发上...",
"image_base64": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"prompt": "描述这张图片的内容"
}
// 响应
{
"result": "图片中是一只橘猫坐在米色沙发上,阳光从窗户照进来...",
"error": ""
}
```
| 参数 | 类型 | 必填 | 说明 |
| -------------- | ----- | ---- | ----------------------------------------------------------- |
| `image_base64` | `str` | ✅ | Base64 编码的图片数据(可含 `data:image/...;base64,` 前缀) |
| `prompt` | `str` | ✅ | 给模型的提示词,描述期望的分析内容 |
---
## 支持的模型
| 模型 | 说明 |
| ---------------------- | ---------------------------- |
| `doubao-seed-evolving` | 豆包视觉理解旗舰模型(默认) |
可根据火山引擎最新文档更换 `model` 配置项。
---
## 实现细节
- 使用 `httpx` 异步 HTTP 客户端调用火山引擎 API
- 支持流式响应(streaming)中的思考模式 token
- API Key 优先从环境变量 `VOLC_ACCESSKEY` 读取
- 超时时间默认 30 秒,在 `config.toml` 的 `defaults.timeout` 中配置
---
## 依赖
| 工具包 | 用途 |
| ------------------ | ------------------------------------ |
| `httpx` | 异步 HTTP 客户端 |
| `chongming-worker` | `@rpc_method` 装饰器 + Worker 运行时 |
+28 -7
View File
@@ -2,10 +2,14 @@
豆包视觉理解 Worker 插件
"""
import os
from openai import AsyncOpenAI
from volcenginesdkarkruntime import AsyncArk
from chongming_worker import create_workers as _create_workers
from chongming_worker import auto_bind_models
from .global_var import set_ark_client, cleanup_ark_client
from .global_var import (
set_ark_client, set_aicc_client, set_is_aicc,
cleanup_all,
)
from .router import *
# 自动绑定 gen_models 生成的 Pydantic 模型
@@ -15,18 +19,35 @@ async def create_workers(session, config, count=1, worker_manager=None):
"""
工厂函数:由 app 生命周期调用,创建 Worker 协程。
"""
# 从配置或环境变量获取 API Key
is_aicc = config.get("is_aicc", False)
set_is_aicc(is_aicc)
api_key = config.get("api_key") or os.environ.get("VOLC_ACCESSKEY")
if not api_key:
raise ValueError("缺少 API_KEY,请在配置中设置 api_key 或环境变量 VOLC_ACCESSKEY")
base_url = config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3")
model = config.get("model", "doubao-seed-evolving")
# 将模型名存入环境变量,供 router 使用
os.environ["DOUBAO_MODEL"] = model
# 创建全局客户端
if is_aicc:
# AICC 模式:懒加载,避免非 AICC 环境导入失败
from bytedance.volcengine_aicc_sdk.secure_http_client import (
AICCConfig, AsyncSecureHttpClient,
)
base_url = config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3")
os.environ["DOUBAO_BASE_URL"] = base_url
aicc_config = AICCConfig(ra_url=base_url, auth_token=api_key)
client = AsyncOpenAI(
api_key=api_key,
base_url=base_url,
http_client=AsyncSecureHttpClient(aicc_config=aicc_config),
)
set_aicc_client(client)
else:
# Ark 模式:使用 AsyncArk
base_url = config.get("base_url", "https://ark.cn-beijing.volces.com/api/v3")
client = AsyncArk(base_url=base_url, api_key=api_key)
set_ark_client(client)
@@ -35,5 +56,5 @@ async def create_workers(session, config, count=1, worker_manager=None):
async def cleanup(worker_item: str):
"""清理资源"""
cleanup_ark_client()
cleanup_all()
print(f"{worker_item} 清理完毕")
+33 -2
View File
@@ -1,7 +1,10 @@
from typing import Optional
from typing import Optional, Union
from volcenginesdkarkruntime import AsyncArk
from openai import AsyncOpenAI
_ark_client: Optional[AsyncArk] = None
_aicc_client: Optional[AsyncOpenAI] = None
_is_aicc: bool = False
def set_ark_client(client: AsyncArk):
global _ark_client
@@ -10,8 +13,36 @@ def set_ark_client(client: AsyncArk):
def get_ark_client() -> Optional[AsyncArk]:
return _ark_client
def set_aicc_client(client: AsyncOpenAI):
global _aicc_client
_aicc_client = client
def get_aicc_client() -> Optional[AsyncOpenAI]:
return _aicc_client
def set_is_aicc(value: bool):
global _is_aicc
_is_aicc = value
def get_is_aicc() -> bool:
return _is_aicc
def get_client() -> Union[AsyncArk, AsyncOpenAI, None]:
"""返回当前活跃的客户端(AICC 或 Ark)"""
if _is_aicc:
return _aicc_client
return _ark_client
def cleanup_ark_client():
global _ark_client
if _ark_client:
# AsyncArk 无显式 close,但可设为 None
_ark_client = None
def cleanup_aicc_client():
global _aicc_client
if _aicc_client:
_aicc_client = None
def cleanup_all():
cleanup_ark_client()
cleanup_aicc_client()
+9 -3
View File
@@ -1,7 +1,7 @@
import os
from chongming_worker import rpc_method
from ..models.pydantic import AnalyzePostRequest, AnalyzePostResponse
from ..global_var import get_ark_client
from ..global_var import get_client, get_is_aicc
@rpc_method(
subject="doubao.analyze",
@@ -11,7 +11,7 @@ from ..global_var import get_ark_client
)
async def analyze(request: AnalyzePostRequest) -> AnalyzePostResponse:
print(f"[DEBUG] analyze called with prompt={request.prompt}, image_base64 length={len(request.image_base64)}")
client = get_ark_client()
client = get_client()
if client is None:
return AnalyzePostResponse(result="", error="客户端未初始化")
@@ -29,6 +29,12 @@ async def analyze(request: AnalyzePostRequest) -> AnalyzePostResponse:
prompt = request.prompt or ""
try:
# AICC 模式的 AsyncOpenAI 不支持 thinking 参数,需通过 extra_body 传递
if get_is_aicc():
create_kwargs = {"extra_body": {"thinking": {"type": "disabled"}}}
else:
create_kwargs = {"thinking": {"type": "disabled"}}
response = await client.chat.completions.create(
model=os.getenv("DOUBAO_MODEL", "doubao-seed-evolving"),
messages=[
@@ -40,7 +46,7 @@ async def analyze(request: AnalyzePostRequest) -> AnalyzePostResponse:
]
} # type: ignore
],
thinking={"type": "disabled"}
**create_kwargs # type: ignore
)
result = response.choices[0].message.content # type: ignore
return AnalyzePostResponse(result=result) # type: ignore
+58 -9
View File
@@ -1,22 +1,71 @@
# Example Worker 插件
演示 `@rpc_method` 与显式模型绑定的用法。
演示 `@rpc_method` 装饰器与显式模型绑定的完整用法,是学习开发新插件的**入门参考**。
---
## 实现的功能
- 加、减、乘、除四则运算(GET / POST)
- 基于 `modelvault` 的访问计数器
### 四则运算(GET / POST)
## 如何工作
| 端点 | 方法 | 路径 | 说明 |
| ---- | ---------- | ---------------- | ------------------------ |
| 加法 | GET / POST | `/calc/add` | `a + b` |
| 减法 | GET | `/calc/subtract` | `a - b` |
| 乘法 | GET | `/calc/multiply` | `a * b` |
| 除法 | GET | `/calc/divide` | `a / b`(除数为 0 报错) |
1. 配置位于 `config/workers/example/*.toml`
2. 模型由 `gen_models` 生成,位于 `models/pydantic.py`
3. 路由实现在 `router/` 下,使用 `@rpc_method` 装饰
4. 工厂函数 `create_workers` 初始化数据库并启动 Worker
### 访问计数器
| 端点 | 方法 | 路径 | 说明 |
| ---- | ---- | ------------- | ----------------- |
| 计数 | GET | `/calc/count` | 每次调用计数器 +1 |
基于内存缓存实现,用于演示 GET 端点的无参数调用模式。
---
## 调用示例
```bash
# GET 加法
curl "http://localhost:8000/api/v1/calc/add?a=3&b=4"
# {"result":7,"operation":"add","timestamp":...}
# {"result":7.0,"operation":"add","timestamp":1720000000.123}
# POST 加法
curl -X POST "http://localhost:8000/api/v1/calc/add" \
-H "Content-Type: application/json" \
-d '{"a": 3.5, "b": 2.5}'
# {"result":6.0,"operation":"add","timestamp":1720000000.456}
# 除法(除数为零)
curl "http://localhost:8000/api/v1/calc/divide?a=10&b=0"
# {"result":0.0,"operation":"divide","error":"Division by zero"}
# 计数器
curl "http://localhost:8000/api/v1/calc/count"
# {"count":1}
```
---
## 如何工作
1. **配置**:`config/workers/example/*.toml` — 定义每个端点的参数和响应结构
2. **模型**:`models/pydantic.py` — 由 `gen_models` 根据 TOML 自动生成
3. **路由**:`router/` 下每个文件实现一个 RPC 端点,使用 `@rpc_method` 装饰器
4. **工厂**:`__init__.py` 中的 `create_workers` 初始化数据库并启动 Worker
---
## 开发参考
此插件展示了以下模式:
- GET 端点的 `params` 定义与 `@rpc_method` 参数绑定
- POST 端点的 JSON body 自动解析
- `response_model` 在 TOML 中定义并在装饰器中引用
- 全局变量 (`global_var.py`) 管理共享资源
- 错误处理(如除法除零返回错误信息)
- `auth_required` 控制端点是否需要 JWT 鉴权
- `cacheable` 控制结果是否缓存
+4 -3
View File
@@ -5,31 +5,32 @@
from .pydantic import AddGetRequest
from .pydantic import AddGetResponse
from .pydantic import AddMqttRequest
from .pydantic import AddPostRequest
from .pydantic import AddPostResponse
from .pydantic import CountGetResponse
from .pydantic import DivideGetRequest
from .pydantic import DivideGetResponse
from .pydantic import ExampleResponse
from .pydantic import HeartbeatMqttRequest
from .pydantic import MultiplyGetRequest
from .pydantic import MultiplyGetResponse
from .pydantic import SubtractGetRequest
from .pydantic import SubtractGetResponse
from .counter import Counter
__all__ = [
"AddGetRequest",
"AddGetResponse",
"AddMqttRequest",
"AddPostRequest",
"AddPostResponse",
"CountGetResponse",
"DivideGetRequest",
"DivideGetResponse",
"ExampleResponse",
"HeartbeatMqttRequest",
"MultiplyGetRequest",
"MultiplyGetResponse",
"SubtractGetRequest",
"SubtractGetResponse",
"Counter"
]
+10 -1
View File
@@ -30,6 +30,11 @@ class AddPostResponse(BaseModel):
operation: str = Field(default='add')
timestamp: Optional[float] = Field(default=0.0)
class AddMqttRequest(BaseModel):
"""Add two numbers via MQTT — 请求体"""
a: float = Field(...)
b: float = Field(...)
class CountGetResponse(BaseModel):
"""Get visit count and increment — 响应体"""
name: str = Field(...)
@@ -47,6 +52,10 @@ class DivideGetResponse(BaseModel):
timestamp: Optional[float] = Field(default=0.0)
error: Optional[str] = Field(default='')
class HeartbeatMqttRequest(BaseModel):
"""Heartbeat ping (no response) — 请求体"""
worker_id: str = Field(...)
class MultiplyGetRequest(BaseModel):
"""Multiply two numbers — 请求体"""
a: float = Field(...)
@@ -71,4 +80,4 @@ class SubtractGetResponse(BaseModel):
# 所有响应模型的联合类型
ExampleResponse = SubtractGetResponse | CountGetResponse | MultiplyGetResponse | AddPostResponse | AddGetResponse | DivideGetResponse
ExampleResponse = AddPostResponse | AddGetResponse | DivideGetResponse | SubtractGetResponse | CountGetResponse | MultiplyGetResponse
+4
View File
@@ -1,4 +1,6 @@
from .add import add, add_post
from .add_mqtt import add_mqtt
from .heartbeat import heartbeat
from .subtract import subtract
from .multiply import multiply
from .divide import divide
@@ -8,6 +10,8 @@ from .count import count_get
__all__ = [
"add",
"add_post",
"add_mqtt",
"heartbeat",
"subtract",
"multiply",
"divide",
+24
View File
@@ -0,0 +1,24 @@
"""
MQTT 端点处理器 — 通过 MQTT 主题 chongming/example/add 调用
与 GET/POST 的 add 共用同一个 subject (example.add),
通过 _method="MQTT" 区分。
"""
import time
from chongming_worker import rpc_method
from ..models.pydantic import AddMqttRequest
@rpc_method(
subject="example.add",
method="MQTT",
summary="Add two numbers via MQTT",
)
async def add_mqtt(request: AddMqttRequest) -> dict:
"""MQTT 加法 — 通过 MQTT 主题 chongming/example/add 调用,返回 dict"""
return {
"result": request.a + request.b,
"operation": "add_mqtt",
"timestamp": time.time(),
}
+20
View File
@@ -0,0 +1,20 @@
"""
心跳 MQTT 端点 — 仅上报,无需响应
通过 MQTT 主题 chongming/example/heartbeat 发送心跳,
Worker 返回 None,MQTT Bridge 不发布响应。
"""
import time
from chongming_worker import rpc_method
@rpc_method(
subject="example.heartbeat",
method="MQTT",
summary="Heartbeat ping via MQTT (no response)",
)
async def heartbeat(worker_id: str = "") -> None:
"""心跳上报 — 无返回值,MQTT Bridge 跳过响应发布"""
print(f"[Heartbeat] worker={worker_id} time={time.time()}")
return None # 显式返回 None,MQTT Bridge 不发布响应