init: chongming 项目模板(init.py 初始化骨架 + 内置插件最小示例)
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# 依赖与构建产物
|
||||||
|
.venv
|
||||||
|
build/
|
||||||
|
build_nuitka/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
resources/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
data/
|
||||||
|
cache_data/
|
||||||
|
|
||||||
|
# 进程插件开发目录(生产在独立容器部署,不入库;如需随仓库管理删掉本行)
|
||||||
|
workers/
|
||||||
|
|
||||||
|
# 其它
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# chongming 项目模板
|
||||||
|
|
||||||
|
以 **chongming 网关宿主项目**(chongming-core / chongming-zenoh)为蓝本的项目模板:
|
||||||
|
初始化出一个开箱可跑的网关项目——宿主骨架(`public/`)+ 内置插件最小示例
|
||||||
|
(`src/plugins/example/`)+ 配置(`config/`)+ 构建配置,**不含 `utils/`**:网关与全部
|
||||||
|
`chongming-*` 工具包都通过 uv 从私有源安装。
|
||||||
|
|
||||||
|
## 初始化新项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python init.py <目标目录> [--name 项目名] [--app-name 应用名] [--no-sync]
|
||||||
|
|
||||||
|
# 例:目录名即项目名
|
||||||
|
python init.py ~/proj/my-gateway
|
||||||
|
|
||||||
|
# 例:指定项目名与应用显示名
|
||||||
|
python init.py ../demo-gateway --name demo-gateway --app-name "演示网关"
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会:拒绝覆盖非空目录 → 复制模板(`README.project.md` → `README.md`)→ 替换
|
||||||
|
`{{project_name}}` / `{{app_name}}` 占位符 → 执行 `uv sync`。
|
||||||
|
|
||||||
|
生成后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <目标目录>
|
||||||
|
uv run serve # http://localhost:8000/docs
|
||||||
|
curl 'http://localhost:8000/api/v1/example/hello?name=world'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模板内容
|
||||||
|
|
||||||
|
```text
|
||||||
|
pyproject.toml # 宿主依赖(chongming-gateway + 骨架依赖)与 dev 工具
|
||||||
|
config.toml # 应用主配置:环境/服务器/缓存/MQTT/打包收集清单/module_system
|
||||||
|
plugins_deps.toml # 内置插件专属依赖(打包收集用,空模板)
|
||||||
|
.python-version # 解释器版本(uv 用)
|
||||||
|
init.py # 本初始化脚本(不复制进目标项目)
|
||||||
|
README.project.md # 生成项目里的 README(初始化时重命名为 README.md)
|
||||||
|
public/ # 可执行文件打包骨架:main.py / server.py / utils / ico / 运行时 config.toml
|
||||||
|
config/ # 项目配置:zenoh_config.json5 + workers/example(示例插件注册与端点)
|
||||||
|
src/plugins/example/ # 内置插件最小示例:Plugin 生命周期 + @rpc_method + gen_models 产物
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生成项目的常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run serve # 启动(热重载)
|
||||||
|
uv run gen_plugin <name> [operation ...] # 生成内置插件骨架(--process 生成进程插件项目)
|
||||||
|
uv run gen_models config/workers/<name> src/plugins/<name> # 按 TOML 生成 Pydantic 模型
|
||||||
|
uv run build pyinstaller # 打包可执行文件(Nuitka 用 uv run build nuitka)
|
||||||
|
uv run build_plugin <name> # 进程插件打包 → dist/<name>.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## 维护模板
|
||||||
|
|
||||||
|
- 骨架(`public/`)、示例插件与配置结构源自 chongming-core 仓库,改动时从那边同步;
|
||||||
|
同步后建议用 `init.py` 生成一个临时项目验证 `uv run serve` 可启动。
|
||||||
|
- 占位符只有两个:`{{project_name}}`(pyproject 的 name)、`{{app_name}}`
|
||||||
|
(config.toml 与 public/config.toml 的 `app.name` / `app.description`)。
|
||||||
|
- 依赖版本不写死:网关与工具包按私有源最新发布版解析;需要锁定时在生成的项目里
|
||||||
|
`uv lock` 提交 `uv.lock` 即可。
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# {{app_name}}
|
||||||
|
|
||||||
|
基于 **chongming 网关**的宿主项目(由 chongming 项目模板初始化):
|
||||||
|
|
||||||
|
- **网关本体**:`chongming-gateway` 包(FastAPI + Zenoh),从私有源按版本依赖,不在本仓库
|
||||||
|
- **本仓库负责**:宿主骨架(`public/`,可执行文件入口)、内置插件(`src/plugins/`)、
|
||||||
|
配置(`config/`)、构建与交付产物
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync # 同步依赖(网关与 chongming-* 工具包从私有源安装)
|
||||||
|
uv run serve # 启动服务(开发模式,热重载)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 通道 | 地址 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| HTTP | `http://localhost:8000` | REST API(`config.toml` 的 `prefix` 为 `/api/v1`) |
|
||||||
|
| Swagger | `http://localhost:8000/docs` | OpenAPI 交互文档(调试模式开启) |
|
||||||
|
| MQTT | `mqtt://localhost:1883` | 内嵌 Broker(`use_embedded_broker = true`) |
|
||||||
|
|
||||||
|
开箱示例(内置插件 `example`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'http://localhost:8000/api/v1/example/hello?name=world'
|
||||||
|
# {"message":"hello, world!","count":1}
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8000/api/v1/example/hello \
|
||||||
|
-H 'Content-Type: application/json' -d '{"name":"world"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
├── config.toml # 应用主配置(环境、服务器、缓存、MQTT、打包清单)
|
||||||
|
├── pyproject.toml # 宿主依赖(chongming-gateway 等)
|
||||||
|
├── plugins_deps.toml # 内置插件专属依赖(打包收集用)
|
||||||
|
├── public/ # 可执行文件打包骨架:main.py / server.py / utils / 运行时 config.toml
|
||||||
|
├── config/ # 项目配置
|
||||||
|
│ ├── zenoh_config.json5 # Zenoh 会话配置
|
||||||
|
│ └── workers/<type>/ # 插件注册(config.toml)与端点定义(<op>.toml)
|
||||||
|
├── src/plugins/ # 内置插件源码(inline,网关进程内加载)
|
||||||
|
│ └── example/ # 最小示例:Plugin 生命周期 + @rpc_method + 模型
|
||||||
|
├── workers/ # 进程插件本地开发目录(external,不入库;生产独立容器)
|
||||||
|
└── resources/ # 构建产物(plugins.mbank 等)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 新增插件 / 端点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run gen_plugin <name> [operation ...] # 一键生成内置插件骨架(含 config/ 注册与端点 TOML)
|
||||||
|
uv run gen_models config/workers/<name> src/plugins/<name> # 改 TOML 后重新生成模型
|
||||||
|
```
|
||||||
|
|
||||||
|
手写流程(参考 `src/plugins/example/` 的四个文件):
|
||||||
|
|
||||||
|
1. `config/workers/<name>/config.toml`:`[workers]` 注册(`type` / `module = "plugins.<name>"` /
|
||||||
|
`factory` / `cleanup` / `count` / `load_mode`)+ `[worker]` 路由前缀与端点默认值
|
||||||
|
2. `config/workers/<name>/<op>.toml`:端点定义(`[GET]` / `[POST]` / `[MQTT]` 分节 + `params` + `response_model`)
|
||||||
|
3. `uv run gen_models ...` 生成 `models/pydantic.py`
|
||||||
|
4. `router/<op>.py`:`@rpc_method(subject="<name>.<op>", method=...)` 实现;
|
||||||
|
在 `router/__init__.py` 导入
|
||||||
|
|
||||||
|
启动时网关会做**契约校验**:TOML 里的 HTTP 路由缺少对应 `@rpc_method` 会直接启动失败。
|
||||||
|
|
||||||
|
进程插件(`load_mode = "external"`,独立进程/容器部署):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run gen_plugin <name> --process # 生成自包含插件项目 workers/<name>/
|
||||||
|
cd workers/<name> && uv run chongming-worker --type <name> # 本地调试
|
||||||
|
uv run build_plugin <name> # 打包 → dist/<name>.zip(容器入口 start.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
- **应用配置**:根 `config.toml`(开发读 `[development]`;`[default].env` 切换环境)
|
||||||
|
- **插件注册与端点**:`config/workers/<type>/config.toml`、`config/workers/<type>/<op>.toml`
|
||||||
|
- **Zenoh**:`config/zenoh_config.json5`
|
||||||
|
|
||||||
|
常用开关:
|
||||||
|
|
||||||
|
| 配置 | 位置 | 说明 |
|
||||||
|
| ---- | ---- | ---- |
|
||||||
|
| `cache.backend` | 根 `config.toml` | `sqlite`(单机默认)/ `redis`(多实例共享) |
|
||||||
|
| `mqtt.use_embedded_broker` | 根 `config.toml` | `true` 自带 Broker;`false` 连外部 Broker |
|
||||||
|
| `workers.count` | 插件 `config.toml` | 副本数(多网关共享 cache 时按在线网关分摊) |
|
||||||
|
| `workers.load_mode` | 插件 `config.toml` | `inline`(默认)/ `external` |
|
||||||
|
|
||||||
|
## 打包与部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run build pyinstaller # 打包可执行文件 → build/chongming
|
||||||
|
uv run build nuitka # Nuitka 打包(Linux 需宿主 patchelf)
|
||||||
|
uv run build --encrypt --license # 对外分发:加密插件模块 + 注入授权公钥
|
||||||
|
uv run build --only mbank # 只重新生成 plugins.mbank(插件热更新只需替换该文件)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 无前端时跳过 vue 步骤:`uv run build pyinstaller --only mbank,prepare,pack`
|
||||||
|
- 插件代码在 `plugins.mbank` 里 → 换文件即可热更新;**网关本体是普通依赖** →
|
||||||
|
升级网关需改 `pyproject.toml` 版本(或 `uv lock --upgrade-package chongming-gateway`)
|
||||||
|
并重新 `uv run build`
|
||||||
|
- 新增插件用了新第三方包时:`pyproject.toml` 追加依赖 + `plugins_deps.toml` 声明
|
||||||
|
(否则打包产物运行到加载插件时才报 ImportError)
|
||||||
|
|
||||||
|
## 进一步文档
|
||||||
|
|
||||||
|
网关主体、插件开发、进程插件、Worker 运维等完整文档在 chongming-core(chongming-zenoh)仓库的
|
||||||
|
`README.md` 与 `docs/`;本仓库的管理端点、实例定向、MQTT 协议等用法与之一致。
|
||||||
+155
@@ -0,0 +1,155 @@
|
|||||||
|
[default]
|
||||||
|
app.debug = false
|
||||||
|
app.description = "{{app_name}} — chongming 网关项目"
|
||||||
|
app.name = "{{app_name}}"
|
||||||
|
app.version = "0.1.0"
|
||||||
|
env = "development"
|
||||||
|
prefix = "/api/v1"
|
||||||
|
upload_path = "uploads"
|
||||||
|
|
||||||
|
[zenoh]
|
||||||
|
json5_path = "./config/zenoh_config.json5"
|
||||||
|
|
||||||
|
[cache]
|
||||||
|
# 单机默认 sqlite(本机共享);多实例部署改 "redis"(或共享卷 sqlite),各实例指向同一后端
|
||||||
|
backend = "sqlite"
|
||||||
|
path = "./cache_data/cache.db"
|
||||||
|
# 副本数/运行时配置的取值来源:
|
||||||
|
# "toml"(默认)= 以文件为准(启动用 config/workers/<type>/config.toml 覆盖写入缓存)
|
||||||
|
# "cache" = 以缓存为单一事实来源(管理接口改动跨重启保持)
|
||||||
|
launch_worker_count = "toml"
|
||||||
|
|
||||||
|
# redis 后端专用(backend = "redis" 时生效)
|
||||||
|
redis_host = "127.0.0.1"
|
||||||
|
redis_port = 6379
|
||||||
|
redis_db = 0
|
||||||
|
redis_password = ""
|
||||||
|
redis_prefix = "chongming:"
|
||||||
|
|
||||||
|
[security]
|
||||||
|
# 打包装备(uv run build)会自动注入随机密钥;生产环境请改用环境变量或自行替换
|
||||||
|
algorithm = "HS256"
|
||||||
|
secret_key = "your-secret-key-change-in-production"
|
||||||
|
|
||||||
|
[development]
|
||||||
|
debug = true
|
||||||
|
env = "development"
|
||||||
|
|
||||||
|
[development.server]
|
||||||
|
# 三个启动入口(serve / granian / gunicorn)共用这些参数
|
||||||
|
access_log = false
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8000
|
||||||
|
reload = true
|
||||||
|
|
||||||
|
[development.logging]
|
||||||
|
file = "logs"
|
||||||
|
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
level = "DEBUG"
|
||||||
|
# access 日志采样率:0.1 = 只记录 10% 的成功请求(错误 >=400 始终全量)
|
||||||
|
access_sample_rate = 0.1
|
||||||
|
|
||||||
|
[development.cors]
|
||||||
|
allow_credentials = true
|
||||||
|
allow_headers = ["*"]
|
||||||
|
allow_methods = ["*"]
|
||||||
|
# allow_credentials=true 时 allow_origins 不能为 ["*"],需显式列出来源
|
||||||
|
allow_origins = [
|
||||||
|
"http://localhost:5173",
|
||||||
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:5174",
|
||||||
|
"http://127.0.0.1:5174",
|
||||||
|
]
|
||||||
|
|
||||||
|
[development.mqtt]
|
||||||
|
enabled = true
|
||||||
|
host = "127.0.0.1"
|
||||||
|
port = 1883
|
||||||
|
# true(默认)= 网关自带内嵌 amqtt Broker(开箱即用);
|
||||||
|
# false = 连接外部 Broker,host/port 改为对方地址
|
||||||
|
use_embedded_broker = true
|
||||||
|
# Broker 认证(可选,默认匿名;密码明文存放,生产建议外部 Broker + 环境变量注入)
|
||||||
|
# username = "chongming"
|
||||||
|
# password = "change-me"
|
||||||
|
|
||||||
|
[development.mqtt.broker]
|
||||||
|
# amqtt Broker 可选配置,默认允许匿名连接
|
||||||
|
|
||||||
|
# 旧式全局插件注册文件(仅作兜底兼容;新插件请在 config/workers/<type>/config.toml 各自声明)
|
||||||
|
[plugins_worker]
|
||||||
|
config_path = "./config/workers/config.toml"
|
||||||
|
|
||||||
|
[pyinstaller_build_import]
|
||||||
|
# 打包收集清单(PyInstaller --hidden-import / Nuitka --include-module),`uv run build` 的
|
||||||
|
# pack 步骤使用。网关本体依赖随 chongming-gateway 包发布、静态分析可跟到;这里保留:
|
||||||
|
# - 动态导入、静态分析看不到的(granian 内部子模块、amqtt 插件、sqlite_vfs 等)
|
||||||
|
# - public 骨架自身(server / utils)与服务器可选实现(uvloop 等)
|
||||||
|
# 内置插件专属依赖(网关本体不 import 的第三方包)写根目录 plugins_deps.toml,按插件分节声明。
|
||||||
|
base = [
|
||||||
|
"utils",
|
||||||
|
"utils.launch",
|
||||||
|
"utils.config",
|
||||||
|
"server",
|
||||||
|
"module_bank",
|
||||||
|
"granian",
|
||||||
|
"granian._compat",
|
||||||
|
"granian._futures",
|
||||||
|
"granian._granian",
|
||||||
|
"granian._imports",
|
||||||
|
"granian._internal",
|
||||||
|
"granian._loops",
|
||||||
|
"granian._signals",
|
||||||
|
"granian._types",
|
||||||
|
"granian.asgi",
|
||||||
|
"granian.constants",
|
||||||
|
"granian.errors",
|
||||||
|
"granian.http",
|
||||||
|
"granian.log",
|
||||||
|
"granian.net",
|
||||||
|
"granian.rsgi",
|
||||||
|
"granian.server",
|
||||||
|
"granian.server.common",
|
||||||
|
"granian.server.mp",
|
||||||
|
"granian.server.mt",
|
||||||
|
"granian.wsgi",
|
||||||
|
"httptools",
|
||||||
|
"uvicorn",
|
||||||
|
"sqlite_vfs",
|
||||||
|
"sqlite_vfs.core",
|
||||||
|
"sqlite_vfs.contrib",
|
||||||
|
"sqlite_vfs.contrib.fastapi_static",
|
||||||
|
"amqtt",
|
||||||
|
# amqtt 内嵌 Broker 的插件经 entry points 动态加载,静态分析看不到
|
||||||
|
"amqtt.plugins.authentication",
|
||||||
|
"amqtt.plugins.logging_amqtt",
|
||||||
|
"amqtt.plugins.sys.broker",
|
||||||
|
"amqtt.plugins.topic_checking",
|
||||||
|
# 数据库 / 缓存驱动:SQLAlchemy 与 chongming-cache 按配置动态加载,必须显式声明
|
||||||
|
"aiosqlite", # SQLite 异步驱动(网关内置 auth 库用)
|
||||||
|
"asyncpg", # PostgreSQL 驱动(插件配 database.url 时用)
|
||||||
|
"sqlmodel",
|
||||||
|
"chongming_cache.backends",
|
||||||
|
"chongming_cache.backends.sqlite_", # cache backend = "sqlite" 时使用
|
||||||
|
"chongming_worker.contracts",
|
||||||
|
"chongming_worker.route_config",
|
||||||
|
"chongming_worker.routes_publisher",
|
||||||
|
"python_multipart", # multipart 上传端点
|
||||||
|
"orjson",
|
||||||
|
"aiofiles",
|
||||||
|
]
|
||||||
|
linux = [
|
||||||
|
"uvloop",
|
||||||
|
]
|
||||||
|
windows = []
|
||||||
|
|
||||||
|
[production.module_system]
|
||||||
|
# 构建用(uv run build 的 mbank 步骤读这里):把插件源码打进 resources/plugins.mbank,
|
||||||
|
# 交付产物运行时经模块银行加载插件(网关本体是普通依赖包,不进 .mbank)
|
||||||
|
path = [
|
||||||
|
[
|
||||||
|
"resources/plugins.mbank",
|
||||||
|
"src/plugins",
|
||||||
|
"plugins",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
type = "module-bank"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
// Zenoh 会话配置 —— 由 config.toml 的 [zenoh].json5_path 指定(见 utils/gateway/src/chongming/app/core/lifespan_setup.py)
|
||||||
|
// 这是项目的zenoh 客户端会话配置,决定应用如何加入 zenoh 网络。
|
||||||
|
// 注意:此文件是应用侧(peer/client)配置,不是独立 router 的配置文件。
|
||||||
|
|
||||||
|
// mode —— 本节点的运行模式:
|
||||||
|
// "peer" : 既可作为客户端连接,也能参与对等组网(本项目默认,多进程间直连)
|
||||||
|
// "client" : 纯客户端,只连接 router,不自建对等网络
|
||||||
|
// "router" : 路由节点(仅独立 zenohd 使用,本项目不会以 router 身份启动)
|
||||||
|
// 本项目用 peer 即可;若已部署独立 Zenoh Router(如 deploy/docker-compose.yml 的 zenoh 服务),
|
||||||
|
// 也可保持 peer 通过下方 connect.endpoints 连接到 router。
|
||||||
|
mode: "peer",
|
||||||
|
|
||||||
|
connect: {
|
||||||
|
// connect.endpoints —— 本节点要主动连接的远端对等端/router 地址列表。
|
||||||
|
// 当前填 127.0.0.1:7447 指向本机(TCP 自连接模式,未接独立 router 时连到其他 peer)。
|
||||||
|
// 接入 docker 部署的 Zenoh Router 后:
|
||||||
|
// - 同机部署:保持 tcp/127.0.0.1:7447 即可(router 容器已把 7447 映射到宿主)
|
||||||
|
// - 跨机部署:改为 router 机器地址,如 "tcp/192.168.x.x:7447"
|
||||||
|
endpoints: ["tcp/127.0.0.1:7447"],
|
||||||
|
},
|
||||||
|
|
||||||
|
scouting: {
|
||||||
|
multicast: {
|
||||||
|
// 多播发现开关。关闭后不通过局域网 224.0.0.224:7446 自动发现对等端,
|
||||||
|
// 仅通过 connect.endpoints 显式连接。多实例/多机器下建议保持 false 避免互相发现串扰。
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
transport: {
|
||||||
|
shared_memory: {
|
||||||
|
// 共享内存传输开关。开启后同机进程间通过共享内存零拷贝交换,性能更好。
|
||||||
|
// 若应用与 router 跨容器/跨机器部署,共享内存不可用,需改为 false(退化为 TCP 传输)。
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""chongming 项目模板初始化。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python init.py <目标目录> [--name 项目名] [--app-name 应用名] [--no-sync]
|
||||||
|
|
||||||
|
行为:
|
||||||
|
1. 拒绝覆盖非空目录
|
||||||
|
2. 复制模板内容(不含 init.py 与模板自身 README;README.project.md 重命名为 README.md)
|
||||||
|
3. 替换占位符 {{project_name}}(发行名)/ {{app_name}}(应用显示名)
|
||||||
|
4. 默认在目标目录执行 uv sync(--no-sync 跳过)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
python init.py ~/proj/my-gateway
|
||||||
|
python init.py ../demo --name demo --app-name "演示网关"
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
TEMPLATE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# 模板自身文件 / 生成物:不复制
|
||||||
|
SKIP_NAMES = {"init.py", "README.md", ".git", "__pycache__", ".venv", "dist", "build"}
|
||||||
|
|
||||||
|
# 做占位符替换的文本文件
|
||||||
|
TEXT_SUFFIXES = {".py", ".toml", ".md", ".json5", ".json", ".yaml", ".yml", ".sh", ".txt"}
|
||||||
|
TEXT_NAMES = {".gitignore", ".env.example"}
|
||||||
|
|
||||||
|
# 生成项目里该文件重命名为 README.md
|
||||||
|
PROJECT_README = "README.project.md"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="chongming 项目模板初始化")
|
||||||
|
parser.add_argument("target", help="目标项目目录(不存在则创建;必须为空)")
|
||||||
|
parser.add_argument("--name", help="项目名(pyproject 的 name,默认取目标目录名)")
|
||||||
|
parser.add_argument("--app-name", help="应用显示名(config.toml 的 app.name,默认同项目名)")
|
||||||
|
parser.add_argument("--no-sync", action="store_true", help="跳过 uv sync")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
target = Path(args.target).expanduser().resolve()
|
||||||
|
project_name = args.name or target.name
|
||||||
|
app_name = args.app_name or project_name
|
||||||
|
|
||||||
|
if target.exists() and any(target.iterdir()):
|
||||||
|
print(f"❌ 目标目录非空,拒绝覆盖: {target}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
replacements = {"{{project_name}}": project_name, "{{app_name}}": app_name}
|
||||||
|
copied = 0
|
||||||
|
for src in sorted(TEMPLATE_DIR.rglob("*")):
|
||||||
|
rel = src.relative_to(TEMPLATE_DIR)
|
||||||
|
if any(part in SKIP_NAMES for part in rel.parts):
|
||||||
|
continue
|
||||||
|
dst = target / ("README.md" if rel.as_posix() == PROJECT_README else rel)
|
||||||
|
if src.is_dir():
|
||||||
|
dst.mkdir(parents=True, exist_ok=True)
|
||||||
|
continue
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if src.suffix in TEXT_SUFFIXES or src.name in TEXT_NAMES:
|
||||||
|
text = src.read_text(encoding="utf-8")
|
||||||
|
for key, value in replacements.items():
|
||||||
|
text = text.replace(key, value)
|
||||||
|
dst.write_text(text, encoding="utf-8")
|
||||||
|
else:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
copied += 1
|
||||||
|
|
||||||
|
print(f"✅ 已初始化: {target}")
|
||||||
|
print(f" 项目名={project_name} 应用名={app_name} 文件数={copied}")
|
||||||
|
|
||||||
|
if args.no_sync:
|
||||||
|
print("(--no-sync:跳过 uv sync)")
|
||||||
|
elif shutil.which("uv") is None:
|
||||||
|
print("⚠️ 未找到 uv,跳过依赖安装;请在目标目录自行执行 uv sync")
|
||||||
|
else:
|
||||||
|
print("→ uv sync(首次安装依赖,可能需要几分钟)")
|
||||||
|
if subprocess.call(["uv", "sync"], cwd=target) != 0:
|
||||||
|
print("⚠️ uv sync 未成功,请在目标目录手动重试")
|
||||||
|
|
||||||
|
print("\n下一步:")
|
||||||
|
print(f" cd {target}")
|
||||||
|
print(" uv run serve # 启动(http://localhost:8000/docs)")
|
||||||
|
print(" curl 'http://localhost:8000/api/v1/example/hello?name=world'")
|
||||||
|
print(" uv run gen_plugin <name> # 生成新插件骨架")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 内置插件专属依赖清单(打包收集用)
|
||||||
|
#
|
||||||
|
# 什么时候写这里:插件用了「网关本体不 import」的第三方包(如 openai / httpx / minio)。
|
||||||
|
# 这些包在 .mbank 内 import,PyInstaller/Nuitka 静态分析看不到,必须显式声明,
|
||||||
|
# 否则产物运行到加载插件时才 ImportError。
|
||||||
|
#
|
||||||
|
# 同时记得在根 pyproject.toml 的 dependencies 里安装(dev 运行 inline 插件与打包共用同一 venv)。
|
||||||
|
# 插件改为 external 进程插件后不需要本文件:依赖随插件项目 workers/<name>/pyproject.toml 自包含。
|
||||||
|
#
|
||||||
|
# 结构(节名 = src/plugins/ 下的插件名;顶层 imports 为多个插件共用):
|
||||||
|
# imports = ["httpx"]
|
||||||
|
#
|
||||||
|
# [doubao]
|
||||||
|
# imports = ["openai", "volcenginesdkarkruntime"]
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
@@ -0,0 +1,98 @@
|
|||||||
|
[default]
|
||||||
|
app.debug = false
|
||||||
|
app.description = "{{app_name}} — chongming 网关项目"
|
||||||
|
app.name = "{{app_name}}"
|
||||||
|
app.version = "0.1.0"
|
||||||
|
env = "production"
|
||||||
|
prefix = "/api/v1"
|
||||||
|
upload_path = "uploads"
|
||||||
|
|
||||||
|
[zenoh]
|
||||||
|
json5_path = "./config/zenoh_config.json5"
|
||||||
|
|
||||||
|
[cache]
|
||||||
|
backend = "sqlite"
|
||||||
|
path = "./cache_data/cache.db"
|
||||||
|
# 配置优先来源模式(同时决定 [workers.config] 与 count 在启动时的取舍):
|
||||||
|
# "cache"(本模板默认)= 以缓存为准:chongming-cache 为单一事实来源,插件初始化即读缓存值,
|
||||||
|
# 管理接口(/admin/workers/<type>/config、/scale)改动跨重启保持;
|
||||||
|
# toml 仅在缓存缺失时引导。启动日志会提示 toml 与缓存的差异键
|
||||||
|
# (以缓存为准);如需让 toml 改动生效,先删除 worker:cm:configs:<type>
|
||||||
|
# (运行时配置)/ worker:cm:counts:<type>(副本数)缓存键再重启。
|
||||||
|
# "toml" = 以文件为准:启动时用 config/workers/<type>/config.toml 的
|
||||||
|
# [workers.config] 与 count 覆盖写入缓存(前端读缓存 → 显示 toml 值);
|
||||||
|
# 管理接口改动仅本次运行有效,重启网关即被文件覆盖。
|
||||||
|
launch_worker_count = "cache"
|
||||||
|
|
||||||
|
[security]
|
||||||
|
algorithm = "HS256"
|
||||||
|
secret_key = "your-secret-key-change-in-production"
|
||||||
|
|
||||||
|
[production]
|
||||||
|
debug = false
|
||||||
|
env = "production"
|
||||||
|
|
||||||
|
[production.server]
|
||||||
|
# 启动参数对各服务器通用(由 main.py 的转换函数映射,无需按服务器改配置);
|
||||||
|
# 服务器/副本数可用环境变量覆盖:APP_SERVER=granian|uvicorn|gunicorn、APP_WORKERS=<N>
|
||||||
|
access_log = false
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8000
|
||||||
|
reload = false
|
||||||
|
timeout_keep_alive = 65
|
||||||
|
workers = 4
|
||||||
|
|
||||||
|
[production.logging]
|
||||||
|
backup_count = 5 # 最多保存 10 个日志文件
|
||||||
|
file = "logs"
|
||||||
|
format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
level = "INFO"
|
||||||
|
max_size = 10485760 # 10MB
|
||||||
|
access_sample_rate = 0.1
|
||||||
|
|
||||||
|
[production.cors]
|
||||||
|
allow_credentials = true
|
||||||
|
allow_headers = ["Content-Type", "Authorization", "Accept"]
|
||||||
|
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||||
|
allow_origins = ["http://localhost:5173", "http://127.0.0.1:5173"]
|
||||||
|
max_age = 600
|
||||||
|
|
||||||
|
[production.mqtt]
|
||||||
|
enabled = true
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 1883
|
||||||
|
# 是否由网关启动内嵌 amqtt Broker 子进程:
|
||||||
|
# true (默认)= 网关自带 amqtt Broker,host/port 为 Broker 监听地址
|
||||||
|
# false = 使用外部 MQTT Broker,网关仅启动 Bridge 客户端连接
|
||||||
|
# host/port 需指向外部 Broker 地址(如 "192.168.1.10")
|
||||||
|
use_embedded_broker = true
|
||||||
|
# MQTT Broker 认证(可选,默认匿名):
|
||||||
|
# - 内嵌 Broker(use_embedded_broker=true):配置后要求所有客户端(含网关 Bridge)
|
||||||
|
# 用该账号密码连接,未配置则保持匿名
|
||||||
|
# - 外部 Broker:网关 Bridge 用该账号密码连接(broker 侧自行配置对应账号)
|
||||||
|
# 注意:密码为明文存储,生产环境建议使用外部 Broker + 环境变量注入
|
||||||
|
# username = "chongming"
|
||||||
|
# password = "change-me"
|
||||||
|
|
||||||
|
# MQTT Bridge 额外订阅主题推荐声明在各插件自己的配置里:
|
||||||
|
# config/workers/<type>/config.toml → [[workers.mqtt_topics]]
|
||||||
|
# (如 aibox_sophon_adapter 插件的 /board_ping 心跳主题)
|
||||||
|
# 全局 [<env>.mqtt].custom_topics 仍可作为通用兜底(可选,不推荐)。
|
||||||
|
|
||||||
|
[production.mqtt.broker]
|
||||||
|
# amqtt Broker 可选配置,默认允许匿名连接
|
||||||
|
# 详细参数参考: https://github.com/Yakifo/amqtt
|
||||||
|
|
||||||
|
[production.module_system]
|
||||||
|
# 运行期模块银行加载清单(frozen 产物用):网关本体是普通依赖包(chongming-gateway,
|
||||||
|
# 已由 PyInstaller/Nuitka 打进产物),只有内置插件经模块银行加载。
|
||||||
|
path = [
|
||||||
|
"resources/plugins.mbank",
|
||||||
|
]
|
||||||
|
type = "module-bank"
|
||||||
|
|
||||||
|
# 静态前端(可选):接入 Vue 前端后由 `uv run build` 的 vue 步骤打包为 static.svfs,
|
||||||
|
# 再启用下面这段(否则静态文件服务会因文件缺失而启动失败):
|
||||||
|
# [production.file_system]
|
||||||
|
# path = "static.svfs"
|
||||||
|
# type = "svfs"
|
||||||
+192
@@ -0,0 +1,192 @@
|
|||||||
|
# main.py - 修复 reload 问题
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FastAPI chongming 示例
|
||||||
|
主入口文件,适配 chongming 打包系统
|
||||||
|
|
||||||
|
服务器选择(APP_SERVER 环境变量,默认见 DEFAULT_SERVER):
|
||||||
|
- gunicorn:现状保留(Linux 默认,uvloop worker + preload 多进程)
|
||||||
|
- uvicorn:Windows 默认(gunicorn 依赖 fork,Windows 不可用)
|
||||||
|
- granian:Rust HTTP 内核,多进程
|
||||||
|
|
||||||
|
启动参数保持 config.toml `[<env>.server]` 现状(host / port / workers /
|
||||||
|
reload / access_log / timeout_keep_alive),由 `_to_uvicorn`、`_to_granian`、
|
||||||
|
`_to_gunicorn_args` 转换为对应服务器的参数。
|
||||||
|
"""
|
||||||
|
import asyncio # noqa: F401
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
from multiprocessing import freeze_support
|
||||||
|
|
||||||
|
import server # noqa: F401 # 预加载:master 内完成插件模块银行装载,fork 出的 worker 共享
|
||||||
|
import uvloop # noqa: F401 # 显式导入保证打包时收集 uvloop(uvicorn/granian loop 依赖)
|
||||||
|
from chongming import RELOAD_PATHS # 热重载监听目录(网关包内定义)
|
||||||
|
from chongming.app.core.logger import get_logger
|
||||||
|
from chongming.app.core.parent_death import guard_parent_death
|
||||||
|
from utils.launch import launch
|
||||||
|
from uvicorn.workers import UvicornWorker
|
||||||
|
|
||||||
|
use_shell = platform.system() == "Windows"
|
||||||
|
|
||||||
|
# 未设置 APP_SERVER 时的平台默认(保持现状)
|
||||||
|
# DEFAULT_SERVER = "uvicorn" if use_shell else "gunicorn"
|
||||||
|
DEFAULT_SERVER = "granian"
|
||||||
|
SUPPORTED_SERVERS = ("gunicorn", "uvicorn", "granian")
|
||||||
|
|
||||||
|
|
||||||
|
class UvloopUvicornWorker(UvicornWorker):
|
||||||
|
CONFIG_KWARGS = {"loop": "uvloop"}
|
||||||
|
|
||||||
|
|
||||||
|
def _server_params(app_config: dict) -> dict:
|
||||||
|
"""把 `[<env>.server]` 的现状参数整理为各服务器共用的启动参数。
|
||||||
|
|
||||||
|
只做缺省兜底与运行环境覆盖(打包环境强制禁 reload、APP_WORKERS 覆盖副本数),
|
||||||
|
键名保持 config.toml 现状,改名交给下面的转换函数。
|
||||||
|
"""
|
||||||
|
server_config = app_config.get("server", {})
|
||||||
|
# 打包环境强制禁用重载
|
||||||
|
is_frozen = getattr(sys, "frozen", False)
|
||||||
|
# 支持通过环境变量 APP_WORKERS 覆盖 worker 数量(方便嵌入式设备调优)
|
||||||
|
default_workers = server_config.get("workers", 4)
|
||||||
|
workers = int(os.environ.get("APP_WORKERS", str(default_workers)))
|
||||||
|
return {
|
||||||
|
"host": server_config.get("host", "0.0.0.0"),
|
||||||
|
"port": server_config.get("port", 8000),
|
||||||
|
"workers": workers,
|
||||||
|
"reload": False if is_frozen else server_config.get("reload", False),
|
||||||
|
"access_log": server_config.get("access_log", False),
|
||||||
|
"timeout_keep_alive": server_config.get("timeout_keep_alive", 5),
|
||||||
|
"log_level": "debug" if app_config.get("debug", False) else "info",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _to_uvicorn(params: dict) -> dict:
|
||||||
|
"""共用参数 → uvicorn.run 关键字参数(键名一致)。"""
|
||||||
|
return {
|
||||||
|
"host": params["host"],
|
||||||
|
"port": params["port"],
|
||||||
|
"reload": params["reload"],
|
||||||
|
"workers": params["workers"],
|
||||||
|
"log_level": params["log_level"],
|
||||||
|
"access_log": params["access_log"],
|
||||||
|
"timeout_keep_alive": params["timeout_keep_alive"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _to_granian(params: dict) -> dict:
|
||||||
|
"""共用参数 → granian.Granian 关键字参数。
|
||||||
|
|
||||||
|
仅两处改名:host → address、access_log → log_access;
|
||||||
|
timeout_keep_alive 在 granian 无对应项(HTTP/1 keep-alive 只有开关、无空闲
|
||||||
|
超时配置),忽略。
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"address": params["host"],
|
||||||
|
"port": params["port"],
|
||||||
|
"interface": "asgi",
|
||||||
|
"workers": params["workers"],
|
||||||
|
"reload": params["reload"],
|
||||||
|
"log_level": params["log_level"],
|
||||||
|
"log_access": params["access_log"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _to_gunicorn_args(params: dict) -> list:
|
||||||
|
"""共用参数 → gunicorn 命令行参数(保持现状:uvloop worker + preload)。"""
|
||||||
|
return [
|
||||||
|
"gunicorn",
|
||||||
|
"server:app",
|
||||||
|
"-k",
|
||||||
|
"__main__.UvloopUvicornWorker",
|
||||||
|
"--preload",
|
||||||
|
"-w",
|
||||||
|
str(params["workers"]),
|
||||||
|
"-b",
|
||||||
|
f"{params['host']}:{params['port']}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _select_server(logger) -> str:
|
||||||
|
"""按 APP_SERVER 环境变量选择服务器,非法值/平台不兼容时回退默认。"""
|
||||||
|
server_kind = (os.environ.get("APP_SERVER") or DEFAULT_SERVER).strip().lower()
|
||||||
|
if server_kind not in SUPPORTED_SERVERS:
|
||||||
|
logger.warning(
|
||||||
|
f"未知 APP_SERVER={server_kind!r}(可选: {', '.join(SUPPORTED_SERVERS)}),"
|
||||||
|
f"回退 {DEFAULT_SERVER}"
|
||||||
|
)
|
||||||
|
server_kind = DEFAULT_SERVER
|
||||||
|
if server_kind == "gunicorn" and use_shell:
|
||||||
|
logger.warning("Windows 不支持 gunicorn(依赖 fork),回退 uvicorn")
|
||||||
|
server_kind = "uvicorn"
|
||||||
|
return server_kind
|
||||||
|
|
||||||
|
|
||||||
|
def app_run(app_config: dict, default_config: dict):
|
||||||
|
"""
|
||||||
|
应用运行函数 - 适配模块银行环境
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_config: 应用配置字典,包含服务器配置、环境设置等
|
||||||
|
default_config: 默认配置字典,包含应用名称、版本等基础信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: 此函数不返回值,直接启动服务器并阻塞运行
|
||||||
|
"""
|
||||||
|
logger = get_logger("app")
|
||||||
|
|
||||||
|
params = _server_params(app_config)
|
||||||
|
server_kind = _select_server(logger)
|
||||||
|
|
||||||
|
# 显示启动信息
|
||||||
|
env = app_config.get("env", "development")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info(f"🚀 启动 {default_config.get('app.name', 'FastAPI 应用')}")
|
||||||
|
logger.info(f"📦 版本: {default_config.get('app.version', '1.0.0')}")
|
||||||
|
logger.info(f"🌍 环境: {env}")
|
||||||
|
logger.info(f"🖥️ 服务器: {server_kind}")
|
||||||
|
logger.info(f"📍 地址: http://{params['host']}:{params['port']}")
|
||||||
|
logger.info(f"📚 文档: http://{params['host']}:{params['port']}/docs")
|
||||||
|
logger.info(f"🔧 调试: {'启用' if app_config.get('debug', False) else '禁用'}")
|
||||||
|
logger.info(f"🔄 重载: {'启用' if params['reload'] else '禁用'}")
|
||||||
|
logger.info(f"👥 Workers: {params['workers']}")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
# 根据所选服务器启动(默认平台分支保持现状)
|
||||||
|
if server_kind == "granian":
|
||||||
|
import granian
|
||||||
|
|
||||||
|
granian.Granian(
|
||||||
|
target="server:app",
|
||||||
|
**_to_granian(params),
|
||||||
|
reload_paths=RELOAD_PATHS,
|
||||||
|
).serve()
|
||||||
|
elif server_kind == "uvicorn":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run("server:app", **_to_uvicorn(params))
|
||||||
|
else:
|
||||||
|
from gunicorn.app.wsgiapp import run
|
||||||
|
|
||||||
|
sys.argv = _to_gunicorn_args(params)
|
||||||
|
run()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
主函数入口 - 初始化多进程支持并启动应用
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None: 此函数不返回值,调用launch函数启动应用
|
||||||
|
"""
|
||||||
|
# 父进程死亡防护(master 侧):引导进程(onefile 引导/终端 shell)死亡时
|
||||||
|
# ——含被 SIGKILL——内核给本进程发 SIGTERM,granian 走优雅停机收掉全部 worker;
|
||||||
|
# worker 侧在 app 的 lifespan 里各自设置(PR_SET_PDEATHSIG 不跨 fork 继承)。
|
||||||
|
guard_parent_death(get_logger("app"))
|
||||||
|
freeze_support()
|
||||||
|
launch(app_run, "production")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# server.py
|
||||||
|
import sys
|
||||||
|
from utils.launch import init_module_bank
|
||||||
|
from utils.config import ConfigLoader
|
||||||
|
|
||||||
|
# 加载配置文件并初始化模块银行(当前只剩内置插件 plugins.mbank)
|
||||||
|
config = ConfigLoader("config.toml")
|
||||||
|
init_module_bank(config)
|
||||||
|
|
||||||
|
# 网关本体是普通依赖包(chongming-gateway),直接导入即可——打包时
|
||||||
|
# PyInstaller/Nuitka 也沿这行静态收集网关及其依赖。
|
||||||
|
# 插件(plugins 包)由上面的模块银行提供,在 lifespan 启动时被网关按配置加载。
|
||||||
|
try:
|
||||||
|
from chongming.app import app as app_instance
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"无法导入网关(chongming.app): {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 暴露 ASGI 应用实例,供 uvicorn 服务器使用
|
||||||
|
app = app_instance
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# config_loader.py - 读取config.toml的示例代码
|
||||||
|
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
# 根据Python版本选择合适的TOML解析库
|
||||||
|
if sys.version_info >= (3, 11):
|
||||||
|
import tomllib
|
||||||
|
else:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigLoader:
|
||||||
|
"""TOML配置文件加载器
|
||||||
|
|
||||||
|
提供配置文件的加载、访问和管理功能。支持自动查找配置文件、
|
||||||
|
点号分隔的嵌套键访问、配置节获取以及配置重载等功能。
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
config_path: 配置文件的路径
|
||||||
|
_config: 存储解析后的配置数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
初始化配置加载器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: 配置文件路径,如果为None则自动查找
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: 当无法找到配置文件时抛出异常
|
||||||
|
"""
|
||||||
|
if config_path is None:
|
||||||
|
# 自动查找配置文件
|
||||||
|
config_path = self._find_config_file()
|
||||||
|
|
||||||
|
self.config_path = Path(config_path)
|
||||||
|
self._config: Dict[str, Any] = {}
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def _find_config_file(self) -> str:
|
||||||
|
"""
|
||||||
|
自动查找配置文件
|
||||||
|
|
||||||
|
按照预定义的优先级顺序搜索配置文件:
|
||||||
|
1. 当前目录下的config.toml
|
||||||
|
2. config子目录下的config.toml
|
||||||
|
3. 用户配置目录~/.config/myapp/config.toml
|
||||||
|
4. 系统配置目录/etc/myapp/config.toml
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 找到的第一个配置文件路径
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: 当所有可能的位置都未找到配置文件时抛出异常
|
||||||
|
"""
|
||||||
|
possible_paths = [
|
||||||
|
"config.toml",
|
||||||
|
"config/config.toml",
|
||||||
|
"~/.config/myapp/config.toml",
|
||||||
|
"/etc/myapp/config.toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possible_paths:
|
||||||
|
expanded_path = os.path.expanduser(path)
|
||||||
|
if os.path.exists(expanded_path):
|
||||||
|
return expanded_path
|
||||||
|
|
||||||
|
raise FileNotFoundError("未找到配置文件")
|
||||||
|
|
||||||
|
def load(self) -> None:
|
||||||
|
"""
|
||||||
|
加载配置文件
|
||||||
|
|
||||||
|
读取并解析TOML格式的配置文件,将配置数据存储到内部字典中。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: 当配置文件不存在时抛出异常
|
||||||
|
tomllib.TOMLDecodeError: 当配置文件格式不正确时抛出异常
|
||||||
|
"""
|
||||||
|
if not self.config_path.exists():
|
||||||
|
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||||||
|
|
||||||
|
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||||
|
self._config = tomllib.loads(f.read())
|
||||||
|
|
||||||
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
|
"""
|
||||||
|
获取配置值,支持点号分隔的路径
|
||||||
|
|
||||||
|
通过点号分隔的键路径访问嵌套的配置值。例如:
|
||||||
|
- "database.host" 访问 {"database": {"host": "localhost"}}
|
||||||
|
- "server.port" 访问 {"server": {"port": 8080}}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 配置键,支持点号分隔的嵌套路径
|
||||||
|
default: 当键不存在时返回的默认值,默认为None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: 配置值,如果键不存在则返回默认值
|
||||||
|
"""
|
||||||
|
keys = key.split(".")
|
||||||
|
value = self._config
|
||||||
|
|
||||||
|
try:
|
||||||
|
for k in keys:
|
||||||
|
value = value[k]
|
||||||
|
return value
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
def get_section(self, section: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取整个配置节
|
||||||
|
|
||||||
|
返回指定配置节的完整字典内容,适用于需要访问某个配置节下
|
||||||
|
所有配置项的场景。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
section: 配置节名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 配置节的内容字典,如果节不存在则返回空字典
|
||||||
|
"""
|
||||||
|
return self._config.get(section, {})
|
||||||
|
|
||||||
|
def reload(self) -> None:
|
||||||
|
"""
|
||||||
|
重新加载配置文件
|
||||||
|
|
||||||
|
从磁盘重新读取配置文件并更新内部配置数据。
|
||||||
|
适用于配置文件在运行时被修改的场景。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: 当配置文件不存在时抛出异常
|
||||||
|
"""
|
||||||
|
self.load()
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
"""
|
||||||
|
支持字典式访问
|
||||||
|
|
||||||
|
允许使用方括号语法访问配置值,如:config["database.host"]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 配置键
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: 配置值
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: 当键不存在且无默认值时抛出异常(通过get方法处理)
|
||||||
|
"""
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
def __contains__(self, key: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查配置是否存在
|
||||||
|
|
||||||
|
支持使用in操作符检查配置键是否存在,如:"database.host" in config
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 要检查的配置键
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果配置键存在且值不为None则返回True,否则返回False
|
||||||
|
"""
|
||||||
|
return self.get(key) is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
获取所有配置
|
||||||
|
|
||||||
|
返回完整的配置字典,适用于需要遍历或导出所有配置的场景。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 包含所有配置的字典
|
||||||
|
"""
|
||||||
|
return self._config
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from .config import ConfigLoader
|
||||||
|
import os
|
||||||
|
from module_bank import PythonToSQLite
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
# module_bank 依赖在每次加载 PyArmor 加密模块时都会 print 一行
|
||||||
|
# "已部署 PyArmor 运行时",启动期会刷屏(约 50 行)。依赖包不便改动,
|
||||||
|
# 这里用模块级 print 遮蔽(模块内名字解析 globals 优先于 builtins)静默掉。
|
||||||
|
import module_bank.sqlite_module_loader as _sqlite_module_loader
|
||||||
|
|
||||||
|
_sqlite_module_loader.print = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
|
||||||
|
def init_module_bank(config):
|
||||||
|
"""
|
||||||
|
初始化模块银行系统,将指定路径下的Python模块打包并安装导入器。
|
||||||
|
|
||||||
|
该函数会验证所有模块路径的有效性,创建PythonToSQLite打包器实例,
|
||||||
|
并为每个打包器安装导入器以支持模块的动态加载。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (dict): 配置字典,必须包含 'production' -> 'module_system' -> 'path' 键,
|
||||||
|
其值为模块路径列表
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 当配置的模块路径中有任何一个不存在时抛出异常
|
||||||
|
"""
|
||||||
|
MODULE_BANK_PATH_LIST = config["production"]["module_system"]["path"]
|
||||||
|
for module_path in MODULE_BANK_PATH_LIST:
|
||||||
|
if not os.path.exists(module_path):
|
||||||
|
raise ValueError(f"模块路径不存在: {module_path}")
|
||||||
|
|
||||||
|
# 为每个模块路径创建PythonToSQLite打包器实例
|
||||||
|
packer_list = [
|
||||||
|
PythonToSQLite(module_path) for module_path in MODULE_BANK_PATH_LIST
|
||||||
|
]
|
||||||
|
# 遍历所有打包器,安装导入器(模块清单不再打印,避免启动刷屏)
|
||||||
|
for packer in packer_list:
|
||||||
|
key = packer.get_key()
|
||||||
|
packer.install_importer(key=key)
|
||||||
|
|
||||||
|
|
||||||
|
def launch(run_fun: Callable[[dict, dict], None], run_mode: str):
|
||||||
|
"""
|
||||||
|
启动应用程序,加载配置并执行指定的运行函数。
|
||||||
|
|
||||||
|
该函数从config.toml文件中加载配置,根据运行模式获取对应的应用配置和默认配置,
|
||||||
|
然后调用传入的运行函数执行应用程序逻辑。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
run_fun (Callable[[dict, dict], None]): 运行函数,接收两个参数:
|
||||||
|
- app_config (dict): 当前运行模式的配置
|
||||||
|
- default_config (dict): 默认配置
|
||||||
|
run_mode (str): 运行模式,用于从配置文件中获取对应模式的配置项
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
config = ConfigLoader("config.toml")
|
||||||
|
|
||||||
|
# 获取对应运行模式的配置
|
||||||
|
default_config = config.get("default", {})
|
||||||
|
app_config = config.get(run_mode, {})
|
||||||
|
|
||||||
|
run_fun(app_config, default_config)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[project]
|
||||||
|
# 宿主项目:骨架(public/)+ 内置插件(src/plugins/)+ 配置(config/)+ 构建配置。
|
||||||
|
# 网关本体与 chongming-* 工具包都是独立发布的依赖,本文件只声明宿主自己需要的依赖。
|
||||||
|
name = "{{project_name}}"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "{{app_name}} — chongming 网关项目"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
# —— 网关本体(含 fastapi / granian / zenoh / chongming-worker 等传递依赖)——
|
||||||
|
"chongming-gateway",
|
||||||
|
# —— public 骨架(exe 入口)自己的依赖 ——
|
||||||
|
"granian[reload,uvloop]>=2.8.4",
|
||||||
|
"gunicorn",
|
||||||
|
"httptools",
|
||||||
|
"module-bank",
|
||||||
|
"uvicorn",
|
||||||
|
"uvloop",
|
||||||
|
# 内置插件(src/plugins/)专属依赖写在这下面,并同步 plugins_deps.toml(打包收集清单);
|
||||||
|
# 示例插件无需额外包
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"chongming-cli", # build / gen_plugin / gen_models / build_plugin / migrate_plugin
|
||||||
|
"greenlet>=3.5.6", # SQLAlchemy 异步(sqlite/postgres 插件)需要
|
||||||
|
"nuitka[onefile]", # Nuitka 打包(onefile extra 带 zstandard 压缩)
|
||||||
|
"patchelf; sys_platform == 'linux'", # Nuitka standalone/onefile 在 Linux 必需
|
||||||
|
"pyarmor", # --encrypt 时的混淆
|
||||||
|
"pyinstaller",
|
||||||
|
"ruff",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
# 宿主项目自身不出包(无 Python 包),uv sync 只装依赖与 dev 工具
|
||||||
|
package = false
|
||||||
|
|
||||||
|
# 需要本地联调 chongming-gateway 源码时取消注释(路径指向网关源码目录),
|
||||||
|
# 联调结束恢复按发布版依赖并 uv lock --upgrade-package chongming-gateway:
|
||||||
|
# [tool.uv.sources]
|
||||||
|
# chongming-gateway = { path = "../chongming-zenoh/utils/gateway", editable = true }
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
url = "http://124.71.68.6:3000/api/packages/chongming/pypi/simple/"
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# example 插件(内置 / inline 最小示例)
|
||||||
|
|
||||||
|
一个可删除、可改名的最小插件:一个 `hello` 端点(GET + POST),演示内置插件的标准结构。
|
||||||
|
|
||||||
|
## 文件职责
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
| ---- | ---- |
|
||||||
|
| `../../config/workers/example/config.toml` | 插件注册(`[workers]`)+ 路由前缀与端点默认值(`[worker]`)+ 运行时配置(`[workers.config]`) |
|
||||||
|
| `../../config/workers/example/hello.toml` | 端点定义:`[GET]` / `[POST]` 分节,`params` 声明入参,`response_model` 声明出参 |
|
||||||
|
| `models/pydantic.py` | 由 `uv run gen_models` 生成的请求/响应模型(勿手改) |
|
||||||
|
| `router/hello.py` | `@rpc_method` 端点实现,引用生成的模型 |
|
||||||
|
| `router/__init__.py` | 导入所有端点模块(触发注册) |
|
||||||
|
| `__init__.py` | `Plugin` 子类统一生命周期(`on_init` / `on_cleanup`)+ 模块级入口 |
|
||||||
|
| `global_var.py` | 模块级共享状态(示例为内存计数;真实插件放 Database / API client) |
|
||||||
|
|
||||||
|
## 调用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl 'http://localhost:8000/api/v1/example/hello?name=world' # GET(query 参数)
|
||||||
|
curl -X POST http://localhost:8000/api/v1/example/hello \
|
||||||
|
-H 'Content-Type: application/json' -d '{"name":"world"}' # POST(JSON body)
|
||||||
|
```
|
||||||
|
|
||||||
|
MQTT 通道同理:topic `chongming/example/hello`,载荷为信封格式
|
||||||
|
(`{"meta": {"method": "GET"}, "data": {"name": "world"}}`)。
|
||||||
|
|
||||||
|
## 新增端点
|
||||||
|
|
||||||
|
1. 写 `config/workers/example/<op>.toml`
|
||||||
|
2. `uv run gen_models config/workers/example src/plugins/example`
|
||||||
|
3. 写 `router/<op>.py`(`@rpc_method(subject="example.<op>", ...)`),引用生成的模型
|
||||||
|
4. 在 `router/__init__.py` 里导入
|
||||||
|
|
||||||
|
## 不要这个示例时
|
||||||
|
|
||||||
|
删除 `src/plugins/example/` 与 `config/workers/example/` 两个目录即可(网关支持零插件启动)。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""example —— 内置(inline)插件最小示例。
|
||||||
|
|
||||||
|
展示三件事:
|
||||||
|
1. 端点声明:`config/workers/example/*.toml` 定义参数与响应模型(见同目录 hello.toml)
|
||||||
|
2. 端点实现:`router/<op>.py` 用 `@rpc_method` 注册,与 TOML 的 subject/方法一一对应
|
||||||
|
3. 生命周期:`Plugin` 子类统一管理初始化与清理,模块级共享状态放 `global_var.py`
|
||||||
|
|
||||||
|
新增端点:
|
||||||
|
1. 写 `config/workers/example/<op>.toml`(按 [GET] / [POST] / [MQTT] 分节)
|
||||||
|
2. `uv run gen_models config/workers/example src/plugins/example` 生成模型
|
||||||
|
3. 写 `router/<op>.py` 实现 `@rpc_method`,引用生成的模型
|
||||||
|
4. 在 `router/__init__.py` 中导入(触发注册)
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from chongming_worker.plugin import Plugin
|
||||||
|
|
||||||
|
from .router import * # noqa: F401,F403 触发 @rpc_method 注册
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ExamplePlugin(Plugin):
|
||||||
|
"""example 插件:on_init 准备共享状态,on_cleanup 释放资源。"""
|
||||||
|
|
||||||
|
type = "example"
|
||||||
|
auto_bind = True # 首次初始化时自动绑定本插件 models 目录中的 Pydantic 模型
|
||||||
|
|
||||||
|
async def on_init(self):
|
||||||
|
"""首次 create_workers 时调用一次:初始化资源。"""
|
||||||
|
# 模块级共享状态:router 用 global_var.require("state") 读取
|
||||||
|
self.set_var("state", {
|
||||||
|
"count": 0,
|
||||||
|
"greeting": self.config.get("greeting", "hello"),
|
||||||
|
})
|
||||||
|
logger.info("[example] 初始化完成")
|
||||||
|
|
||||||
|
async def on_cleanup(self):
|
||||||
|
"""插件停止 / 配置热更新时调用:释放自定义客户端等(数据库由基类处理)。"""
|
||||||
|
logger.info("[example] 清理完成")
|
||||||
|
|
||||||
|
|
||||||
|
# 暴露给网关 lifespan 的模块级入口(bound method)
|
||||||
|
plugin = ExamplePlugin()
|
||||||
|
create_workers = plugin.create_workers
|
||||||
|
cleanup = plugin.cleanup
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""example 插件全局运行时状态(模块级)。
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- 模块只声明默认值,不产生导入期副作用
|
||||||
|
- 由 `Plugin.set_var` 写入(白名单见 `_ALLOWED`),路由里用 `require()` 读取
|
||||||
|
- `require()` 取"必须已初始化"的值,未初始化时给出明确错误
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# 允许写入的全局变量白名单(拼写错误会立即抛错,而不是静默创建无用变量)
|
||||||
|
_ALLOWED = frozenset({"state"})
|
||||||
|
|
||||||
|
# 运行时状态:示例用内存计数;需要持久化时改为 Database / API client 等
|
||||||
|
state: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_global_var(name: str, value: Any) -> None:
|
||||||
|
"""白名单校验版 setter(`Plugin.set_var` 会调用它)。"""
|
||||||
|
if name not in _ALLOWED:
|
||||||
|
raise KeyError(f"global_var: 未知变量 '{name}'(允许: {sorted(_ALLOWED)})")
|
||||||
|
globals()[name] = value
|
||||||
|
|
||||||
|
|
||||||
|
def require(name: str) -> Any:
|
||||||
|
"""取必须已初始化的全局变量,未初始化时抛清晰错误。"""
|
||||||
|
if name not in _ALLOWED:
|
||||||
|
raise KeyError(f"global_var: 未知变量 '{name}'(允许: {sorted(_ALLOWED)})")
|
||||||
|
value = globals().get(name)
|
||||||
|
if value is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"global_var: '{name}' 未初始化 —— 请确认插件 create_workers 已执行且配置正确"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = sorted(_ALLOWED) + ["set_global_var", "require"]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
模型导出 — example
|
||||||
|
自动生成,请勿手动修改
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .pydantic import ExampleResponse
|
||||||
|
from .pydantic import HelloGetRequest
|
||||||
|
from .pydantic import HelloGetResponse
|
||||||
|
from .pydantic import HelloPostRequest
|
||||||
|
from .pydantic import HelloPostResponse
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExampleResponse",
|
||||||
|
"HelloGetRequest",
|
||||||
|
"HelloGetResponse",
|
||||||
|
"HelloPostRequest",
|
||||||
|
"HelloPostResponse",
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""
|
||||||
|
自动生成的 Pydantic 模型 — example
|
||||||
|
来源: config/workers/example/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
class HelloGetRequest(BaseModel):
|
||||||
|
"""打招呼(GET,query 参数) — 请求体"""
|
||||||
|
name: str = Field(...)
|
||||||
|
|
||||||
|
class HelloGetResponse(BaseModel):
|
||||||
|
"""打招呼(GET,query 参数) — 响应体"""
|
||||||
|
message: str = Field(...)
|
||||||
|
count: int = Field(...)
|
||||||
|
|
||||||
|
class HelloPostRequest(BaseModel):
|
||||||
|
"""打招呼(POST,JSON body) — 请求体"""
|
||||||
|
name: str = Field(...)
|
||||||
|
|
||||||
|
class HelloPostResponse(BaseModel):
|
||||||
|
"""打招呼(POST,JSON body) — 响应体"""
|
||||||
|
message: str = Field(...)
|
||||||
|
count: int = Field(...)
|
||||||
|
|
||||||
|
|
||||||
|
# 所有响应模型的联合类型
|
||||||
|
ExampleResponse = HelloGetResponse | HelloPostResponse
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .hello import hello_get, hello_post
|
||||||
|
|
||||||
|
__all__ = ["hello_get", "hello_post"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""hello 端点 — 演示 @rpc_method 的 GET(query 参数)与 POST(JSON body)两种形态。"""
|
||||||
|
from chongming_worker import rpc_method
|
||||||
|
|
||||||
|
from .. import global_var
|
||||||
|
from ..models.pydantic import (
|
||||||
|
HelloGetRequest,
|
||||||
|
HelloGetResponse,
|
||||||
|
HelloPostRequest,
|
||||||
|
HelloPostResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc_method(
|
||||||
|
subject="example.hello",
|
||||||
|
method="GET",
|
||||||
|
summary="打招呼(GET)",
|
||||||
|
response_model=HelloGetResponse,
|
||||||
|
)
|
||||||
|
async def hello_get(request: HelloGetRequest) -> HelloGetResponse:
|
||||||
|
state = global_var.require("state")
|
||||||
|
state["count"] += 1
|
||||||
|
return HelloGetResponse(
|
||||||
|
message=f"{state['greeting']}, {request.name}!",
|
||||||
|
count=state["count"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc_method(
|
||||||
|
subject="example.hello",
|
||||||
|
method="POST",
|
||||||
|
summary="打招呼(POST)",
|
||||||
|
response_model=HelloPostResponse,
|
||||||
|
)
|
||||||
|
async def hello_post(request: HelloPostRequest) -> HelloPostResponse:
|
||||||
|
state = global_var.require("state")
|
||||||
|
state["count"] += 1
|
||||||
|
return HelloPostResponse(
|
||||||
|
message=f"{state['greeting']}, {request.name}!",
|
||||||
|
count=state["count"],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user