From 741d7cda09881ea0cd6a1a46a2b8fe89d33e6e01 Mon Sep 17 00:00:00 2001 From: chakcy <947105045@qq.com> Date: Tue, 25 Aug 2026 17:03:35 +0800 Subject: [PATCH] =?UTF-8?q?modify:=E8=BF=9B=E7=A8=8B=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83=E7=9B=AE=E5=BD=95=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 33 ++++---- docs/process-plugin.md | 141 +++++++++++++++++++--------------- test/test_build_plugin.py | 80 ++++++++++++++----- test/test_process_mode_e2e.py | 28 ++++--- test/test_worker_isolation.py | 10 +++ 5 files changed, 188 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 70e5bca..14d31c9 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,7 @@ load_mode = "external" # 独立进程运行,经 Zenoh 注册路由 - 网关进程内 import;路由由网关盘扫 TOML 生成;契约校验在网关启动时执行 -**external 进程插件**(本地开发目录 `workers/`,不入库): +**external 进程插件**(本地开发目录 `workers/`,自包含项目,不入库): - 与网关**零导入关系**:只依赖 `chongming-worker` SDK + zenoh,仅经 Zenoh 通讯 - 路由注册:worker 启动时 put 描述符到 `chongming/routes//...`,退出时 delete;每 10s 周期重发布自愈 @@ -376,12 +376,15 @@ load_mode = "external" # 独立进程运行,经 Zenoh 注册路由 路由注册与网关侧插件盘扫(`config/workers/*/config.toml`)解耦:**网关容器无需 放置 external 插件的配置文件**,盘扫发现的 external 条目仅用于启动日志观测; inline 插件仍按盘扫在网关进程内加载,不受影响 -- 代码结构与 inline 插件完全一致(Plugin 基类 + router/ + models/),示例见本地 `workers/demo/` -- 运行: +- 每个插件是**自包含项目**(`workers//` = 打包 zip 根 = 容器工作目录, + 开发即产物),代码结构与 inline 插件完全一致(Plugin 基类 + router/ + models/), + 示例见本地 `workers/demo/`;新项目用 `uv run gen_plugin --process` 生成骨架 +- 运行(在插件项目根 `workers//` 下): ```bash - uv run worker_serve --type demo # 本地调试 - chongming-worker --type demo # 容器入口(chongming-worker SDK console script) + cd workers/demo + uv run worker_serve --type demo # 本地调试(首次先 uv sync 装依赖) + ./start.sh # 与容器一致的启动方式(uv sync + 启动) ``` - 生产部署:进程插件与网关**分别打包到不同容器** @@ -391,28 +394,32 @@ load_mode = "external" # 独立进程运行,经 Zenoh 注册路由 ### 进程插件打包 -将 external 进程插件一键打包为可部署 zip(插件代码 + 配置 + 依赖清单 + 容器入口): +`build_plugin` 把插件项目根直接压缩为可部署 zip(开发目录即产物, +插件代码 + 配置 + 依赖清单 + 容器入口都在 `workers//` 内): ```bash uv run build_plugin demo # → dist/demo.zip uv run build_plugin demo --output /tmp/pkg ``` -产物布局(zip 根即容器工作目录): +产物布局(= 项目根,zip 根即容器工作目录): ```text workers// 插件代码(模块 workers.) config/workers// 插件注册 + 端点配置 config/zenoh_config.json5 Zenoh 会话配置 -config.toml 最小根配置(自动生成,仅 [zenoh] 节,不泄露网关配置) +config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需) pyproject.toml uv 依赖清单(chongming-worker 内网 git 源) start.sh 容器入口:uv sync + worker_serve --type ``` -- 插件扩展依赖(可选):在 `workers//requirements.txt` 中逐行声明,打包时自动合并进 `pyproject.toml` 的 dependencies +- 打包自动排除 `__pycache__` / `*.pyc` / `.venv` 等,`start.sh` 权限强制 0o755 +- 插件扩展依赖(可选):直接在项目根 `pyproject.toml` 的 dependencies 中追加 - 容器内部署:`unzip .zip -d /app && cd /app && ./start.sh`(镜像需预装 uv) -- 打包前校验:`workers//` 与 `config/workers//config.toml` 必须存在,`load_mode` 非 `external` 时告警 -- `config/workers//` 仅部署在**插件容器**(worker 自读);网关侧无需放置(RouteRegistry 常驻订阅 Zenoh 路由描述符,与盘扫解耦) +- 打包前校验:`workers//` 项目根、项目内 `config/workers//config.toml` + (含 `[workers]` 节)、`config/zenoh_config.json5` 与根 `config.toml`(含 `[zenoh]`)必须存在, + `load_mode` 非 `external` 时告警 +- 插件配置随 zip 部署在**插件容器**(worker 自读);网关侧无需放置(RouteRegistry 常驻订阅 Zenoh 路由描述符,与盘扫解耦) --- @@ -562,7 +569,7 @@ uv pip install -e utils/ # 安装/更新工具包 | **doubao** | `src/plugins/doubao/` | 豆包视觉理解,支持 AICC 安全通信通道 | | **aibox_sophon_adapter** | `src/plugins/aibox_sophon_adapter/` | AI Box 设备心跳监控、离线检测、告警上报与查询、视频上传 | | **example** | `src/plugins/example/` | 四则运算 + 心跳演示(含 MQTT 端点示例) | -| **demo**(进程插件示例) | `workers/demo/`(本地目录,不入库) | 独立进程运行(load_mode=external),经 Zenoh 注册路由 | +| **demo**(进程插件示例) | `workers/demo/`(本地自包含项目,不入库) | 独立进程运行(load_mode=external),经 Zenoh 注册路由 | --- @@ -634,7 +641,7 @@ uv run build --only mbank,prepare,vue ```bash uv run serve # 启动开发服务器 -uv run gen_plugin # 生成统一风格插件骨架 +uv run gen_plugin # 生成统一风格插件骨架(--process 生成进程插件项目) uv run gen_models # 生成 Pydantic 模型 uv run build pyinstaller # PyInstaller 打包 uv run build nuitka # Nuitka 打包 diff --git a/docs/process-plugin.md b/docs/process-plugin.md index 56a2397..576d590 100644 --- a/docs/process-plugin.md +++ b/docs/process-plugin.md @@ -13,7 +13,7 @@ | 模式 | 运行位置 | 目录 | 路由来源 | 契约校验位置 | | ---- | -------- | ---- | -------- | ------------ | | **inline**(默认) | 网关进程内 import | `src/plugins//`(chongming-plugins 仓库) | 网关盘扫 TOML 生成 | 网关启动时 | -| **external(进程插件)** | 独立进程 | `workers//`(本地开发目录,**不入库**) | Worker 经 Zenoh 发布路由描述符,网关动态挂载 | **Worker 进程内** | +| **external(进程插件)** | 独立进程 | `workers//`(本地开发目录,**自包含项目**,不入库) | Worker 经 Zenoh 发布路由描述符,网关动态挂载 | **Worker 进程内** | 进程插件的特点: @@ -89,40 +89,48 @@ WorkerManager 据此做负载均衡,网关侧无需任何改动。 ## 3. 目录结构约定 -进程插件代码放在本地开发目录 `workers//`(**gitignore,不入库**): +进程插件是**自包含项目**:`workers//` 即项目根,同时也是打包 zip 的根 +(**gitignore,不入库**)。开发目录与容器工作目录完全一致,开发即产物: ```text -workers// -├── __init__.py # Plugin 子类 + create_workers / cleanup 模块级入口 -├── global_var.py # 模块级共享状态(Database、API client 等) -├── router/ # RPC 端点实现 -│ ├── __init__.py # 导入所有端点模块 -│ └── <操作名>.py # 每个端点一个文件,@rpc_method 装饰器 -├── models/ # 数据模型(gen_models 自动生成,勿手动修改) -│ ├── __init__.py -│ └── pydantic.py # 请求/响应 Pydantic 模型 -├── requirements.txt # 扩展依赖(可选,每行一个包,打包时并入 pyproject.toml) -└── README.md # 插件说明(可选) +workers// # 项目根 = zip 根 = 容器工作目录 +├── workers// # 插件代码(模块 workers.) +│ ├── __init__.py # Plugin 子类 + create_workers / cleanup 模块级入口 +│ ├── global_var.py # 模块级共享状态(Database、API client 等) +│ ├── router/ # RPC 端点实现 +│ │ ├── __init__.py # 导入所有端点模块 +│ │ └── <操作名>.py # 每个端点一个文件,@rpc_method 装饰器 +│ ├── models/ # 数据模型(gen_models 自动生成,勿手动修改) +│ │ ├── __init__.py +│ │ └── pydantic.py # 请求/响应 Pydantic 模型 +│ └── README.md # 插件说明(可选) +├── config/ +│ ├── zenoh_config.json5 # Zenoh 会话配置 +│ └── workers// # 插件注册 + 端点配置 +│ ├── config.toml # [workers] 节注册 + [worker] 元信息 + [workers.config] +│ └── <操作名>.toml # 每个端点一个定义文件 +├── config.toml # 最小根配置(仅 [zenoh] 节,worker 启动必需) +├── pyproject.toml # uv 依赖清单(chongming-worker + 扩展依赖) +└── start.sh # 容器入口(uv sync + uv run worker_serve --type ) ``` -> 参考示例:`config/workers/demo/` 中的注册与端点配置(`demo` / `hello.toml`), -> 以及 `workers/demo/`(本地目录,配合 `test_worker_isolation.py` 使用)。 +> 参考示例:`workers/demo/`(完整项目,含 5 个端点,配合 `test_worker_isolation.py` / +> `test_process_mode_e2e.py` 使用)。 --- ## 4. 开发流程 -### 第 1 步:创建插件代码 +### 第 1 步:生成项目骨架 ```bash -mkdir -p workers/demo/router -mkdir -p workers/demo/models -touch workers/demo/__init__.py -touch workers/demo/global_var.py -touch workers/demo/router/__init__.py +uv run gen_plugin demo --process # 生成完整进程插件项目(含示例操作) +uv run gen_plugin demo hello storage --process # 指定初始操作 +uv run gen_plugin demo add --process # 为已有项目增量添加操作 ``` -`workers/demo/__init__.py`(与 inline 插件完全一致,推荐 Plugin 基类风格): +生成的代码位于 `workers/demo/workers/demo/`,其中 `__init__.py`(与 inline 插件 +完全一致,推荐 Plugin 基类风格): ```python import logging @@ -154,15 +162,16 @@ cleanup = _plugin.cleanup ``` > 注意:`module` 注册为 `workers.` 时,`sys.path` 需包含项目根目录—— -> `worker_serve` 已自动处理(console script 启动时会显式把 CWD 加入 `sys.path`)。 +> `worker_serve` 已自动处理(console script 启动时会显式把 CWD 加入 `sys.path`), +> 只需在项目根 `workers//` 下运行。 ### 第 2 步:插件注册配置 -创建 `config/workers//config.toml`,`[workers]` 节注册插件 +创建 `workers//config/workers//config.toml`,`[workers]` 节注册插件 (**关键:`load_mode = "external"`**): ```toml -# config/workers/demo/config.toml +# workers/demo/config/workers/demo/config.toml [workers] type = "demo" # 插件类型(worker type) module = "workers.demo" # Python 模块路径(进程插件用 workers.) @@ -188,7 +197,7 @@ greeting_prefix = "Hello" ### 第 3 步:端点 TOML 定义 -每个端点一个文件,如 `config/workers//hello.toml`: +每个端点一个文件,如 `workers//config/workers//hello.toml`: ```toml [GET] @@ -207,14 +216,14 @@ timestamp = { type = "float", required = true } ### 第 4 步:生成 Pydantic 模型 ```bash -uv run gen_models config/workers/demo workers/demo +uv run gen_models workers/demo/config/workers/demo workers/demo/workers/demo ``` -在 `workers//models/` 下生成 `pydantic.py` 与 `__init__.py`。 +在 `workers//workers//models/` 下生成 `pydantic.py` 与 `__init__.py`。 ### 第 5 步:实现 RPC 端点 -`workers//router/hello.py`: +`workers//workers//router/hello.py`: ```python import time @@ -232,7 +241,7 @@ async def hello(request: HelloGetRequest) -> HelloGetResponse: ) ``` -`workers//router/__init__.py`: +`workers//workers//router/__init__.py`: ```python from . import hello @@ -253,17 +262,23 @@ __all__ = ["hello"] ### 第 6 步:本地调试 +在**插件项目根** `workers//` 下运行(worker 以 CWD 为工作目录读配置/import 代码): + ```bash -uv run worker_serve --type demo # 本地调试(开发环境) -uv run worker_serve --type demo --count 1 # 覆盖副本数 -chongming-worker --type demo # 容器入口(console script) +cd workers/demo +uv sync # 首次:安装依赖(项目独立 venv,与容器一致) +uv run worker_serve --type demo # 本地调试 +uv run worker_serve --type demo --count 1 # 覆盖副本数 +./start.sh # 与容器一致的启动方式 +# 开发期复用仓库 venv(已装好 SDK)时,用 --project 指向仓库根: +uv run --project .. worker_serve --type demo ``` 启动后预期日志: ```text -[ demo ] 路由发布完成: 1 端点 + 无元信息 -[ demo ] Worker 进程就绪,等待退出信号... +[demo] 路由发布完成: 5 端点 + 无元信息 +[demo] Worker 进程就绪,等待退出信号... ``` > 若配置了 `load_mode` 非 `"external"`,会输出告警:网关也会在进程内加载该插件, @@ -272,10 +287,10 @@ chongming-worker --type demo # 容器入口(console script ### 第 7 步:验证 ```bash -# 1. 进程隔离性(阻断 chongming 导入 + AST 检查 import 语句) +# 1. 进程隔离性(阻断 chongming 导入 + AST 检查 import 语句;自动切到 demo 项目根) uv run python test_worker_isolation.py -# 2. 端到端(需先启动网关 uv run serve,且被测插件 load_mode = "external"): +# 2. 端到端(需先启动网关 uv run serve):从 demo 项目根拉起 worker 进程, # 路由动态注册 → 跨进程 RPC → 退出摘除(404) → 重启恢复 uv run python test_process_mode_e2e.py ``` @@ -287,26 +302,28 @@ uv run build_plugin demo # → dist/demo.zip uv run build_plugin demo --output /tmp/pkg # 指定输出目录 ``` -打包前校验(任一不满足即失败退出): +打包 = 项目根直接压缩(开发目录即产物,zip 根即容器工作目录),自动排除 +`__pycache__` / `*.pyc` / `.venv` / `*.egg-info` / `.git`,`start.sh` 权限强制 0o755。 -- `workers//` 插件代码目录存在 -- `config/workers//config.toml` 存在且含 `[workers]` 节 -- `config/zenoh_config.json5` 存在 -- `load_mode` 非 `"external"` 时**告警**(不阻断,但会造成重复服务) - -产物布局(**zip 根即容器工作目录**): +产物布局与 §3 完全一致(**zip 根即容器工作目录**): ```text workers// 插件代码(模块 workers.) config/workers// 插件注册 + 端点配置 config/zenoh_config.json5 Zenoh 会话配置 -config.toml 最小根配置(自动生成,仅 [zenoh] 节,不泄露网关配置) +config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需) pyproject.toml uv 依赖清单(chongming-worker 内网 git 源 + orjson + 扩展依赖) start.sh 容器入口:uv sync --no-dev + uv run worker_serve --type ``` -扩展依赖:`workers//requirements.txt` 中逐行声明(`#` 开头为注释), -打包时自动合并进 `pyproject.toml` 的 dependencies。 +打包前校验(任一不满足即失败退出): + +- `workers//` 项目根存在 +- `workers//config/workers//config.toml` 存在且含 `[workers]` 节 +- `workers//config/zenoh_config.json5` 与 `workers//config.toml`(含 `[zenoh]`)存在 +- `load_mode` 非 `"external"` 时**告警**(不阻断,但会造成重复服务) + +扩展依赖:直接在项目根 `pyproject.toml` 的 `dependencies` 中追加(如 `"minio"`)。 ### 第 9 步:容器部署 @@ -321,7 +338,7 @@ cd /app - 生产部署时进程插件与网关**分别打包到不同容器**: - 网关容器:`chongming` + 依赖 - 进程插件容器:插件代码 + `chongming-worker` + 依赖 -- `config/workers//` 仅部署在**插件容器**(worker 自读);网关侧无需放置 +- 插件配置随 zip 部署在**插件容器**(worker 自读);网关侧无需放置 (RouteRegistry 常驻订阅 Zenoh 路由描述符,与盘扫解耦) --- @@ -335,7 +352,7 @@ cd /app ### 5.1 配置 ```toml -# config/workers//config.toml +# workers//config/workers//config.toml [workers.config.storage] endpoint = "192.168.1.10:9000" # Minio 地址 host:port(不带协议前缀) access_key = "minioadmin" @@ -395,7 +412,7 @@ API 一览(`chongming_worker.storage.StorageClient`): ### 5.3 参考示例 -`workers/demo` 演示了 4 个文件传输端点(`config/workers/demo/` 下对应 TOML): +`workers/demo` 演示了 4 个文件传输端点(项目内 `config/workers/demo/` 下对应 TOML): - `POST /demo/presign_upload` — filename → object_key + 预签名 PUT URL(设备直传) - `GET /demo/presign_download` — object_key → 预签名 GET URL(设备直取) @@ -405,7 +422,7 @@ API 一览(`chongming_worker.storage.StorageClient`): 配置后重启 worker 即可用;未配置时这些端点返回「storage 未配置」错误(降级演示)。 > 依赖:minio SDK 随 `chongming-worker` 一起安装(utils/worker pyproject 已声明), -> 进程插件无需额外 `requirements.txt`。SDK 侧 `chongming_worker.storage` 不依赖网关包, +> 进程插件无需额外依赖。SDK 侧 `chongming_worker.storage` 不依赖网关包, > 隔离性约束与打包流程均不受影响。 --- @@ -416,7 +433,7 @@ API 一览(`chongming_worker.storage.StorageClient`): - `chongming-worker`(worker SDK:`chongming_worker.*`) - `zenoh` / `pydantic` 等第三方库 -- 插件自身的扩展依赖(requirements.txt) +- 插件自身的扩展依赖(项目根 `pyproject.toml` 中声明) 由 `test_worker_isolation.py` 双重保证: @@ -445,12 +462,12 @@ Worker 进程启动时自读 TOML 并**在自己进程内**做三方一致性校 | 现象 | 原因 / 处理 | | ---- | ----------- | -| 启动告警「load_mode 不是 external」 | 进程插件必须在 `config/workers//config.toml` `[workers]` 节设置 `load_mode = "external"`,否则网关内副本与外部进程重复服务 | -| 契约校验失败、进程退出 | 检查 TOML 端点(subject/方法节)与 `@rpc_method(subject=..., method=...)` 是否一致;模型缺失时重跑 `uv run gen_models config/workers/ workers/` | +| 启动告警「load_mode 不是 external」 | 进程插件必须在 `workers//config/workers//config.toml` `[workers]` 节设置 `load_mode = "external"`,否则网关内副本与外部进程重复服务 | +| 契约校验失败、进程退出 | 检查 TOML 端点(subject/方法节)与 `@rpc_method(subject=..., method=...)` 是否一致;模型缺失时重跑 `uv run gen_models workers//config/workers/ workers//workers/` | | `TypeError: rpc_method() got an unexpected keyword argument 'stream'` | venv 里是旧版 chongming-worker(git 源拉取),本地未 push 的框架改动不生效;联调时 `uv pip install -e utils/worker` | | 路由没出现 | worker 是否就绪(日志「路由发布完成」);网关 RouteRegistry 是否订阅(启动流程 5.1);每 10s 自愈重发布,稍等或重启 worker | | 路由一直 404 | worker 是否已退出(优雅退出会摘除路由);进程被 SIGKILL 时描述符残留,重启 worker 后重发布自愈覆盖 | -| 找不到插件配置 | `worker_serve` 须在项目根目录下运行(读 `config/workers//config.toml`,`--config` 可指定全局配置路径) | +| 找不到插件配置 | `worker_serve` 须在**插件项目根** `workers//` 下运行(读 `config/workers//config.toml`,`--config` 可指定全局配置路径) | | storage.* 端点报「storage 未配置」 | 插件 config.toml 未配置 `[workers.config.storage]`(或 `enabled = false` / 连接失败降级)。补齐配置后重启 worker;启动日志可见「对象存储已就绪」或「storage 连接失败」告警 | | 容器内 import `workers.demo` 失败 | 确认在容器工作目录(zip 根)启动,`start.sh` 内含 `cd "$(dirname "$0")"`,worker_serve 会把 CWD 加入 sys.path | @@ -459,12 +476,14 @@ Worker 进程启动时自读 TOML 并**在自己进程内**做三方一致性校 ## 9. 相关命令速查 ```bash -uv run worker_serve --type # 本地调试独立 Worker 进程 -chongming-worker --type # 容器入口(console script) -uv run build_plugin [--output ] # 进程插件打包 → dist/.zip -uv run gen_models config/workers/ workers/ # 生成模型到进程插件目录 -uv run python test_worker_isolation.py # 进程隔离性测试 -uv run python test_process_mode_e2e.py # 端到端测试(先启动网关) +uv run gen_plugin --process # 生成进程插件项目骨架 +cd workers/ # 以下均在插件项目根下执行 +uv run worker_serve --type # 本地调试独立 Worker 进程 +./start.sh # 与容器一致的启动方式(uv sync + 启动) +cd .. && uv run build_plugin # 进程插件打包 → dist/.zip +uv run gen_models workers//config/workers/ workers//workers/ # 生成模型 +uv run python test_worker_isolation.py # 进程隔离性测试 +uv run python test_process_mode_e2e.py # 端到端测试(先启动网关) ``` --- diff --git a/test/test_build_plugin.py b/test/test_build_plugin.py index 1cbf791..689d075 100644 --- a/test/test_build_plugin.py +++ b/test/test_build_plugin.py @@ -2,9 +2,10 @@ build_plugin 打包命令冒烟测试 验证进程插件打包产物的完整性与错误路径: - 1. demo 插件打包成功,zip 内含代码/配置/最小根配置/依赖清单/可执行 start.sh - 2. 插件代码目录缺失时报 FileNotFoundError - 3. 插件 config.toml 缺失时报 FileNotFoundError + 1. demo 插件打包成功,zip = 项目根直接压缩(代码/配置/最小根配置/依赖清单/可执行 start.sh) + 2. 打包排除 __pycache__ / .venv / *.pyc 等非产物内容 + 3. 插件项目目录缺失时报 FileNotFoundError + 4. 插件项目内配置缺失时报 FileNotFoundError 运行: uv run python test_build_plugin.py """ @@ -16,9 +17,10 @@ import zipfile from pathlib import Path SRC = Path(__file__).parent -sys.path.insert(0, str(SRC / "utils" / "cli" / "src")) +sys.path.insert(0, str(SRC.parent / "utils" / "cli" / "src")) from chongming_cli import build_plugin as bp # noqa: E402 +from chongming_cli import gen_plugin as gp # noqa: E402 failures = [] @@ -47,9 +49,9 @@ with zipfile.ZipFile(zip_path) as z: check("包含 start.sh", "start.sh" in names) check("不包含 __pycache__", not any("__pycache__" in n for n in names)) - # 最小根配置:仅 [zenoh] 节,不含网关侧敏感配置 + # 最小根配置:worker 启动需 [zenoh] 节,且不含网关侧敏感配置 root_conf = tomllib.loads(z.read("config.toml").decode()) - check("根配置仅含 [zenoh] 节", set(root_conf.keys()) == {"zenoh"}, str(sorted(root_conf))) + check("根配置含 [zenoh] 节", "zenoh" in root_conf, str(sorted(root_conf))) check( "根配置不泄露网关 [security] 密钥", "secret_key" not in z.read("config.toml").decode(), @@ -66,7 +68,7 @@ with zipfile.ZipFile(zip_path) as z: ) check( "pyproject 声明 chongming-worker git 源", - pyproject["tool"]["uv"]["sources"]["chongming-worker"]["git"] == bp.CHONGMING_WORKER_GIT, + pyproject["tool"]["uv"]["sources"]["chongming-worker"]["git"] == gp.CHONGMING_WORKER_GIT, ) # start.sh:可执行 + 指向正确的插件 type @@ -76,31 +78,71 @@ with zipfile.ZipFile(zip_path) as z: check("start.sh 可执行(0o755)", perms == 0o755, oct(perms)) -# ========== 2. 错误路径:插件代码目录缺失 ========== +# ========== 2. 打包排除非产物内容(临时最小项目) ========== +def make_mini_project(proj: Path) -> None: + """构造一个结构合法的迷你插件项目(含需要排除的垃圾文件)。""" + (proj / "workers" / "mini" / "router").mkdir(parents=True) + (proj / "workers" / "mini" / "__init__.py").write_text("", encoding="utf-8") + (proj / "workers" / "mini" / "router" / "__init__.py").write_text("", encoding="utf-8") + conf = proj / "config" / "workers" / "mini" + conf.mkdir(parents=True) + (conf / "config.toml").write_text( + '[workers]\ntype = "mini"\nmodule = "workers.mini"\nload_mode = "external"\n', + encoding="utf-8", + ) + (proj / "config" / "zenoh_config.json5").write_text("{}", encoding="utf-8") + (proj / "config.toml").write_text('[zenoh]\njson5_path = "config/zenoh_config.json5"\n', encoding="utf-8") + (proj / "pyproject.toml").write_text("[project]\nname = \"mini-worker\"\n", encoding="utf-8") + (proj / "start.sh").write_text("#!/usr/bin/env bash\necho mini\n", encoding="utf-8") + (proj / "start.sh").chmod(0o755) + # 垃圾内容:缓存目录、字节码、虚拟环境 + (proj / "workers" / "mini" / "__pycache__" / "x.pyc").parent.mkdir(parents=True) + (proj / "workers" / "mini" / "__pycache__" / "x.pyc").write_bytes(b"x") + (proj / "workers" / "mini" / "y.pyc").write_bytes(b"y") + (proj / ".venv" / "bin" / "pip").parent.mkdir(parents=True) + (proj / ".venv" / "bin" / "pip").write_text("", encoding="utf-8") + + orig_workers = bp.WORKERS_DIR -bp.WORKERS_DIR = tmp / "no_workers" +mini_proj = tmp / "workers" / "mini" +make_mini_project(mini_proj) +bp.WORKERS_DIR = tmp / "workers" try: - bp.build_plugin_package("ghost", output_dir=tmp / "dist") - check("缺插件代码目录应抛 FileNotFoundError", False) -except FileNotFoundError: - check("缺插件代码目录应抛 FileNotFoundError", True) + zip_path = bp.build_plugin_package("mini", output_dir=tmp / "dist") + with zipfile.ZipFile(zip_path) as z: + names = set(z.namelist()) + check("排除 __pycache__", not any("__pycache__" in n for n in names), str(sorted(names))) + check("排除 *.pyc", not any(n.endswith(".pyc") for n in names)) + check("排除 .venv", not any(n.startswith(".venv") for n in names)) + check("保留代码与配置", {"workers/mini/__init__.py", "config/workers/mini/config.toml"} <= names) finally: bp.WORKERS_DIR = orig_workers -# ========== 3. 错误路径:插件配置缺失 ========== -orig_conf_dir = bp.PLUGINS_CONFIG_DIR -bp.PLUGINS_CONFIG_DIR = tmp / "no_config" +# ========== 3. 错误路径:插件项目目录缺失 ========== +bp.WORKERS_DIR = tmp / "no_workers" try: - bp.build_plugin_package("demo", output_dir=tmp / "dist") + bp.build_plugin_package("ghost", output_dir=tmp / "dist") + check("缺插件项目目录应抛 FileNotFoundError", False) +except FileNotFoundError: + check("缺插件项目目录应抛 FileNotFoundError", True) +finally: + bp.WORKERS_DIR = orig_workers + + +# ========== 4. 错误路径:项目内插件配置缺失 ========== +# 只建代码目录、不建 config/workers//config.toml +(orig_workers / "ghost").mkdir(parents=True) +try: + bp.build_plugin_package("ghost", output_dir=tmp / "dist") check("缺插件配置应抛 FileNotFoundError", False) except FileNotFoundError: check("缺插件配置应抛 FileNotFoundError", True) finally: - bp.PLUGINS_CONFIG_DIR = orig_conf_dir + shutil.rmtree(orig_workers / "ghost", ignore_errors=True) -# ========== 4. 错误路径:插件名不合法 ========== +# ========== 5. 错误路径:插件名不合法 ========== try: bp.build_plugin_package("Bad-Name", output_dir=tmp / "dist") check("非法插件名应抛 ValueError", False) diff --git a/test/test_process_mode_e2e.py b/test/test_process_mode_e2e.py index 300c857..19654a1 100644 --- a/test/test_process_mode_e2e.py +++ b/test/test_process_mode_e2e.py @@ -1,12 +1,12 @@ """ -混合模式(external 插件)端到端测试 +进程插件(external 模式)端到端测试 前置条件: - 1. 网关正常运行(uv run serve);example 插件配置为 external: - config/workers/example/config.toml [workers] 节 load_mode = "external" - 2. 本脚本自己拉起/停止独立 worker 进程(uv run worker_serve --type example) + 1. 网关正常运行(uv run serve) + 2. demo 进程插件(workers/demo/,load_mode = "external") -验证: +本脚本从 demo 插件项目根(= 打包 zip 根)拉起/停止独立 worker 进程 +(worker_serve --type demo),验证: 1. worker 启动后 → HTTP 路由动态出现且跨进程 RPC 可调 2. worker 优雅退出 → 路由动态摘除(404) 3. worker 重启 → 路由恢复 @@ -18,13 +18,17 @@ import urllib.request from pathlib import Path GATEWAY_URL = "http://localhost:8000" -TEST_URL = f"{GATEWAY_URL}/api/v1/calc/add?a=3&b=4" +TEST_URL = f"{GATEWAY_URL}/api/v1/demo/hello?name=chongming" POLL_TIMEOUT = 30.0 -# 优先使用 venv 控制台脚本(uv sync 后生成),不存在时退回 uv run -_WORKER_CMD = [str(Path(sys.executable).parent / "worker_serve"), "--type", "example"] +# 插件项目根(= 容器工作目录 = zip 根):worker 在项目根下读配置/import 代码 +REPO = Path(__file__).parent.parent +PROJECT = REPO / "workers" / "demo" + +# 优先使用仓库 venv 控制台脚本(避免在插件项目内新建 venv),否则退回 uv run --project +_WORKER_CMD = [str(Path(sys.executable).parent / "worker_serve"), "--type", "demo"] if not Path(_WORKER_CMD[0]).exists(): - _WORKER_CMD = ["uv", "run", "worker_serve", "--type", "example"] + _WORKER_CMD = ["uv", "run", "--project", str(REPO), "worker_serve", "--type", "demo"] PASS = 0 FAIL = 0 @@ -63,9 +67,10 @@ def wait_http(target_status=200, timeout=POLL_TIMEOUT): def run(): - # 1. 拉起独立 worker 进程 + # 1. 拉起独立 worker 进程(CWD = 插件项目根,与容器一致) proc = subprocess.Popen( _WORKER_CMD, + cwd=str(PROJECT), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, @@ -83,6 +88,7 @@ def run(): # 3. 重启 worker → 路由恢复 proc2 = subprocess.Popen( _WORKER_CMD, + cwd=str(PROJECT), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, @@ -103,5 +109,5 @@ def run(): if __name__ == "__main__": - print("提示: 请先以 load_mode=process 启动网关 (uv run serve)\n") + print(f"提示: 请先启动网关 (uv run serve);worker 将从 {PROJECT} 拉起\n") sys.exit(run()) diff --git a/test/test_worker_isolation.py b/test/test_worker_isolation.py index b5b1f5c..66b8e24 100644 --- a/test/test_worker_isolation.py +++ b/test/test_worker_isolation.py @@ -4,12 +4,22 @@ Worker 进程隔离性测试 验证进程插件与网关完全隔离:阻断 chongming(网关包)的两种导入路径后, worker 侧全链路(SDK 导入 → 插件配置自读 → 插件 import → 契约校验)仍可用。 +与容器行为一致:chdir 到插件项目根 workers/demo/(= 打包 zip 根), +worker_serve 即以该目录为 CWD 读配置、并把 CWD 加入 sys.path。 + 任何对 chongming.* / src.chongming.* 的导入都会直接触发 ImportError。 """ +import os import sys from pathlib import Path SRC = Path(__file__).parent +REPO = SRC.parent + +# 模拟容器:CWD = 插件项目根(zip 根),并把它置于 sys.path 首位 +PROJECT = REPO / "workers" / "demo" +os.chdir(PROJECT) +sys.path.insert(0, str(PROJECT)) sys.path.insert(0, str(SRC)) # 阻断网关包导入(两种访问路径)。chongming_worker / zenoh / pydantic 等不受影响。