modify: 插件初始化
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
```text
|
||||
<plugin_name>/
|
||||
├── __init__.py # 入口:create_workers() 工厂 + cleanup() + auto_bind_models()
|
||||
├── __init__.py # 入口:Plugin 子类 + create_workers/cleanup 绑定
|
||||
├── global_var.py # 模块级全局状态(Database、API client 等)
|
||||
├── router/ # RPC 端点实现
|
||||
│ ├── __init__.py # 导入所有端点模块(`from . import *` 或显式导入)
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||
## 已有插件概览
|
||||
|
||||
| 插件 | 特点 | 适合参考 |
|
||||
| ------------------------ | ------------------------------------------------- | ---------- |
|
||||
| **example** | 最简示例,`auto_bind_models` + Database + cleanup | 入门学习 |
|
||||
| **auth** | SQLModel ORM + 管理员初始化 + 多进程幂等 | 数据库操作 |
|
||||
| **doubao** | 外部 API 客户端(Ark/OpenAI)+ AICC 安全通道 | 外部集成 |
|
||||
| **aibox_sophon_adapter** | 自定义 `APIRouter` + MQTT 转发 + 后台任务 | 高级模式 |
|
||||
| 插件 | 特点 | 适合参考 |
|
||||
| ------------------------ | -------------------------------------------- | ---------- |
|
||||
| **example** | 最简示例,Plugin 基类 + Database + 生命周期 | 入门学习 |
|
||||
| **auth** | SQLModel ORM + 管理员初始化 + 多进程幂等 | 数据库操作 |
|
||||
| **doubao** | 外部 API 客户端(Ark/OpenAI)+ AICC 安全通道 | 外部集成 |
|
||||
| **aibox_sophon_adapter** | 自定义 `APIRouter` + MQTT 转发 + 后台任务 | 高级模式 |
|
||||
|
||||
---
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
|
||||
### 第一步:创建目录结构
|
||||
|
||||
> **推荐**:直接用脚手架生成统一风格的插件骨架,无需手动创建目录:
|
||||
>
|
||||
> ```bash
|
||||
> uv run gen_plugin <name> [operation ...]
|
||||
> ```
|
||||
>
|
||||
> 以下为手动创建方式(了解结构时参考)。
|
||||
|
||||
```bash
|
||||
mkdir -p src/plugins/<name>/router
|
||||
mkdir -p src/plugins/<name>/models
|
||||
@@ -124,38 +132,44 @@ from . import my_action
|
||||
__all__ = ["my_action"]
|
||||
```
|
||||
|
||||
### 第七步:实现工厂函数
|
||||
### 第七步:实现插件入口(推荐 Plugin 基类)
|
||||
|
||||
`src/plugins/<name>/__init__.py`:
|
||||
`src/plugins/<name>/__init__.py`(由 `gen_plugin` 脚手架生成):
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from chongming_worker import auto_bind_models
|
||||
from chongming_worker.runtime import create_workers as _create_workers
|
||||
from .router import * # 触发 @rpc_method 注册
|
||||
import logging
|
||||
|
||||
auto_bind_models("<name>") # 自动绑定生成的 Pydantic 模型
|
||||
from chongming_worker.plugin import Plugin
|
||||
|
||||
async def create_workers(session, config, count, worker_manager):
|
||||
"""插件入口:初始化资源,启动 Worker"""
|
||||
from chongming_database import Database
|
||||
from .global_var import set_db
|
||||
from . import router # noqa: F401 触发 @rpc_method 注册
|
||||
# from .routes import router # 可选:自定义 FastAPI 路由(网关自动发现)
|
||||
|
||||
db_path = Path(config.get("data_dir", "./data")) / "<name>.db"
|
||||
db = Database(db_path)
|
||||
await db.initialize()
|
||||
set_db(db)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
return _create_workers(session, config, count, worker_manager)
|
||||
|
||||
async def cleanup(worker_manager):
|
||||
"""清理资源"""
|
||||
from .global_var import db
|
||||
if db:
|
||||
await db.dispose()
|
||||
class MyPlugin(Plugin):
|
||||
type = "<name>"
|
||||
db_name = "<name>.db" # 自动在 data_dir 下建 SQLite 库(不需要则置 None)
|
||||
auto_bind = True # 首次初始化时自动 auto_bind_models("<name>")
|
||||
|
||||
async def on_init(self):
|
||||
"""首次 create_workers 时调用一次:初始化资源、启动后台任务。"""
|
||||
self.set_var("db", self.db) # 发布到 global_var 供业务代码读取
|
||||
# self.spawn_task(self._background_loop())
|
||||
|
||||
async def on_cleanup(self):
|
||||
"""cleanup 时调用:关闭自定义客户端等(后台任务与数据库由基类处理)。"""
|
||||
|
||||
|
||||
# 暴露给 app lifespan 的模块级入口(bound method,签名与旧写法完全一致)
|
||||
_plugin = MyPlugin()
|
||||
create_workers = _plugin.create_workers
|
||||
cleanup = _plugin.cleanup
|
||||
```
|
||||
|
||||
> **注意**:`create_workers` 必须是 `async` 函数,因为 `Database.initialize()` 是异步的。框架在 `asyncio.gather` 中并发调用所有插件的工厂函数。
|
||||
基类自动处理:幂等、统一日志、数据库建库/释放、后台任务取消、清理钩子、模型自动绑定。详见 `utils/worker/README.md` 的「Plugin 基类」章节。
|
||||
|
||||
> **兼容旧写法**:不依赖基类的传统 `create_workers` / `cleanup` 模块级函数写法仍受支持(见 `utils/worker/README.md`)。
|
||||
|
||||
---
|
||||
|
||||
@@ -199,32 +213,89 @@ from .routes import router # 网关通过 getattr 自动发现
|
||||
|
||||
---
|
||||
|
||||
## 全局变量模式
|
||||
## 共享对象(global_var)在两种路由中的使用
|
||||
|
||||
每个插件通过 `global_var.py` 管理模块级共享资源:
|
||||
### 数据流
|
||||
|
||||
```python
|
||||
# global_var.py
|
||||
from chongming_database import Database
|
||||
共享对象(Database、API client 等)在 `Plugin.on_init` 中初始化,通过 `set_var` 发布到 `global_var` 模块;两种路由的业务代码(模块级函数)在函数体内 `from .global_var import ...` 读取。
|
||||
|
||||
db: Database | None = None
|
||||
|
||||
def set_db(database: Database):
|
||||
global db
|
||||
db = database
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["Plugin.on_init<br/>self.db = ..."] -->|"self.set_var('db', self.db)"| GV["global_var 模块<br/>db / require()"]
|
||||
R1["router/*.py<br/>@rpc_method 函数"] -->|"from ..global_var import require"| GV
|
||||
R2["routes.py<br/>APIRouter 处理器"] -->|"from .global_var import db"| GV
|
||||
```
|
||||
|
||||
在端点中直接引用:
|
||||
> **为什么需要 `global_var.py`**:`@rpc_method` 函数和 APIRouter 处理器都是模块级函数,拿不到 Plugin 实例,只能通过模块级容器读取;`global_var.py` 独立成模块,可避免与 `__init__.py` 循环导入。`set_var()` 内部会调用 `global_var.set_global_var()`(白名单校验)完成发布。
|
||||
|
||||
### 注入:on_init → global_var
|
||||
|
||||
`src/plugins/<name>/__init__.py`:
|
||||
|
||||
```python
|
||||
from ..global_var import db
|
||||
class MyPlugin(Plugin):
|
||||
type = "<name>"
|
||||
db_name = "<name>.db"
|
||||
|
||||
@rpc_method(subject="<name>.action", method="GET", ...)
|
||||
async def handler(request):
|
||||
async def on_init(self):
|
||||
self.set_var("db", self.db) # 发布到 global_var 模块
|
||||
```
|
||||
|
||||
### 路由一:TOML 驱动标准路由(@rpc_method)
|
||||
|
||||
`router/<op>.py`(请求经 Zenoh → Worker Queryable → 调用函数):
|
||||
|
||||
```python
|
||||
from chongming_worker.decorators import rpc_method
|
||||
from ..models.pydantic import OpGetRequest, OpGetResponse
|
||||
|
||||
@rpc_method(subject="<name>.op", method="GET", response_model=OpGetResponse)
|
||||
async def op(request: OpGetRequest) -> OpGetResponse:
|
||||
from ..global_var import require
|
||||
db = require("db") # 未初始化立即抛清晰错误
|
||||
async with db.get_session() as session:
|
||||
...
|
||||
return OpGetResponse(...)
|
||||
```
|
||||
|
||||
### 路由二:插件自定义路由(APIRouter)
|
||||
|
||||
`routes.py`(FastAPI 直接调用,适合 multipart 上传等 TOML 无法描述的端点):
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, UploadFile, File
|
||||
|
||||
router = APIRouter(prefix="/<name>", tags=["<name>"])
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload(file: UploadFile = File(...)):
|
||||
from .global_var import db
|
||||
if db is None: # 容忍未初始化,优雅降级
|
||||
return {"error": "db 未初始化"}
|
||||
async with db.get_session() as session:
|
||||
...
|
||||
return {"ok": True}
|
||||
```
|
||||
|
||||
在 `__init__.py` 顶部暴露给网关:`from .routes import router`(网关通过 `getattr(module, "router")` 自动发现)。
|
||||
|
||||
### 两种路由对比
|
||||
|
||||
| | 路由一 `@rpc_method` | 路由二 `APIRouter` |
|
||||
| ---------- | ----------------------------------------- | --------------------------------------------- |
|
||||
| 文件位置 | `router/<op>.py` | `routes.py` |
|
||||
| 入口发现 | 装饰器注册 → Worker Queryable | `__init__.py` 暴露 `router` 属性 |
|
||||
| 函数签名 | `(request: Pydantic模型) -> Pydantic模型` | FastAPI 风格(`Request`/`UploadFile`/`Form`) |
|
||||
| 适用场景 | 标准 JSON / query 端点 | multipart 上传、手动解析报文 |
|
||||
| 读共享对象 | `from ..global_var import require` | `from .global_var import db` |
|
||||
| 请求路径 | HTTP → Zenoh → Worker 回调 | FastAPI 直接调用 |
|
||||
|
||||
### 经验
|
||||
|
||||
- **在函数体内 import**(不要放文件顶部):模块 import 时 `create_workers` 尚未执行、`global_var` 还是空;请求到达时变量才注入完成。
|
||||
- **两种读取风格**:`require("db")`(必须有,未初始化立即报错)vs `from .global_var import db` + `if db is None`(允许优雅降级)。
|
||||
- **参考实现**:`aibox_sophon_adapter/routes.py` 的 `_save_alarm_record`(自定义路由中 `from .global_var import db` + `async with db.get_session()`)。
|
||||
|
||||
---
|
||||
|
||||
## @rpc_method 装饰器
|
||||
|
||||
Reference in New Issue
Block a user