diff --git a/.cursor/skills/add-resource/SKILL.md b/.cursor/skills/add-resource/SKILL.md index 1b67a8729..03bb434ec 100644 --- a/.cursor/skills/add-resource/SKILL.md +++ b/.cursor/skills/add-resource/SKILL.md @@ -81,7 +81,8 @@ self.warehouses = { - **max_volume 单位为 μL**:500mL = 500000 - **尺寸单位为 mm**:`diameter`, `height`, `size_x/y/z`, `dx/dy/dz` - **BottleCarrier 必须设置 `num_items_x/y/z`**:用于前端渲染布局 -- **Deck 的 `__init__` 必须接受 `setup=False`**:图文件中 `config.setup=true` 触发 `setup()` +- **默认子资源用 `setup=False` 控制**:只有需要首次创建时自动添加默认 children/layout 的资源才实现 `setup()`,例如工作站 Deck 自动放置 WareHouse;`setup` 必须默认为 `False`,只在图文件或调用方显式 `setup=True` 时执行,避免反序列化已有 `children` 时重复创建、覆盖持久化状态或污染外部同步物料 +- **类形式(class)的资源必须被实际 import**:运行时解析通过 `find_subclass(type_name, PLRResource)` 查找,只遍历**已 import** 的 `__subclasses__()`;AST 扫描只生成注册表条目,不会 import 模块。Deck / WareHouse / 自定义 Plate 等 class 形式资源需在 `resources/__init__.py` 中 eager import;工厂函数(`def`)形式不受影响 - **按项目分组文件**:同一工作站的资源放在 `unilabos/resources//` 下 - **`__init__` 必须接受 `serialize()` 输出的所有字段**:`serialize()` 输出会作为 `config` 回传到 `__init__`,因此必须通过显式参数或 `**kwargs` 接受,否则反序列化会报错 - **持久化运行时状态用 `serialize_state()`**:通过 `_unilabos_state` 字典存储可变信息(如物料内容、液体量),只存 JSON 可序列化的基本类型 @@ -195,7 +196,7 @@ def my_warehouse_4x4(name: str) -> "WareHouse": #### Deck 组装 WareHouse -Deck 通过 `setup()` 将多个 WareHouse 放置到指定坐标: +需要默认 children/layout 的资源可通过 `setup()` 初始化,工作站 Deck 是最常见例子。`setup` 必须默认为 `False`,只有首次创建默认布局时显式打开: ```python from pylabrobot.resources import Deck, Coordinate @@ -319,6 +320,7 @@ class MyLabDeck(Deck): ``` unilabos/resources/ ├── / # 按项目分组 +│ ├── __init__.py # eager import class 形式资源,确保 find_subclass 可发现 │ ├── bottles.py # Bottle 工厂函数 │ ├── bottle_carriers.py # Carrier 工厂函数 │ ├── warehouses.py # WareHouse 工厂函数 @@ -333,6 +335,9 @@ unilabos/resources/ # 资源可导入 python -c "from unilabos.resources.my_project.bottles import My_Reagent_Bottle; print(My_Reagent_Bottle('test'))" +# class 形式资源可通过运行时入口被 PLR 发现 +python -c "import unilabos.resources.my_project; from pylabrobot.utils.object_parsing import find_subclass; from pylabrobot.resources import Resource as R; assert find_subclass('MyStation_Deck', R)" + # 启动测试(AST 自动扫描) unilab -g .json ``` diff --git a/.cursor/skills/add-workstation/SKILL.md b/.cursor/skills/add-workstation/SKILL.md index c039687b2..46d32d8d8 100644 --- a/.cursor/skills/add-workstation/SKILL.md +++ b/.cursor/skills/add-workstation/SKILL.md @@ -19,6 +19,14 @@ description: Guide for adding new workstations to Uni-Lab-OS (接入新工作站 --- +## 补充参考 + +- 接入通用外部 LIMS/MES/供应商系统、配置 RPC/回调/物料同步时,读 [reference.md](reference.md)。 +- 新增或修改工作流可见的动作节点函数、定义`@action`、handles、变量分组、manual-confirm 节点、日志或错误/状态语义时,读 [workflow-node-guidelines.md](workflow-node-guidelines.md)。 +- 接入 Bioyond/Bioyond Studio/奔曜工作站、供应商物料同步或回调报告时,读 [bioyond.md](bioyond.md)。 + +--- + ## @device 装饰器(工作站) 工作站也使用 `@device` 装饰器注册,参数与普通设备一致: @@ -421,6 +429,8 @@ class ROS2SerialNode(BaseROS2DeviceNode): ## Deck 与物料生命周期 +Deck / WareHouse / Plate / Bottle 等资源类本身的创建规范可参考 add-resource;这里重点说明工作站图文件和生命周期中的用法。 + ### 1. Deck 入参与两种初始化模式 系统根据设备节点 `config.deck` 的写法,自动反序列化 Deck 实例后传入 `__init__` 的 `deck` 参数。目前 `deck` 是固定字段名,只支持一个主 Deck。建议一个设备拥有一个台面,台面上抽象二级、三级子物料。 @@ -515,6 +525,8 @@ def _initialize_default_deck(self): self.deck.assign_child_resource(My_Plate("T2"), spot=1) ``` +如果资源需要首次创建时自动添加默认 children/layout,可实现 `setup()` 并用 `setup=False` 作为默认值;工作站 Deck 自动放置 WareHouse 是典型例子。若图文件写 `config.setup=true`,它只是传给 Deck 构造函数的参数,只有该 Deck 的 `__init__(..., setup=False, **kwargs)` 显式读取并调用 `self.setup()` 时才生效。默认保持 `False` 可避免反序列化已有 `children` 时重复创建或覆盖持久化状态。 + ### 3. 物料双向同步 当工作站对接外部系统(LIMS/MES)时,需要实现 `ResourceSynchronizer` 处理双向物料同步: @@ -603,6 +615,7 @@ class MyPlate(Plate): - `serialize()` 输出的所有字段都会作为 `config` 回传到 `__init__`,所以 `__init__` 必须能接受它们(显式声明或 `**kwargs`) - `serialize_state()` 输出的 `data` 用于持久化运行时状态(如物料信息、液体量等) - `_unilabos_state` 中只存可 JSON 序列化的基本类型(str, int, float, bool, list, dict, None) +- 如果 Deck 已经带 `children` 反序列化进来,工作站不要再次初始化默认子物料,避免重复或陈旧资源 ### 5. 子物料自动同步 @@ -678,9 +691,9 @@ Deck 节点要点: ## 关键规则 -1. **`__init__` 必须接受 `deck` 和 `**kwargs`** — `WorkstationBase.**init**`需要`deck` 参数 +1. **`__init__` 必须接受 `deck` 和 `**kwargs`** — `WorkstationBase.__init__` 需要 `deck` 参数 2. **Deck 通过 `config.deck._resource_type` 反序列化传入** — 不要在 `__init__` 中手动创建 Deck -3. **Deck 为空时自行初始化内容** — 在 `post_init` 中检查并填充默认物料 +3. **Deck 为空时自行初始化内容** — 在 `post_init` 中检查并填充默认物料;如需默认 children/layout,用 `setup=False` 控制并只在首次创建时显式打开 4. **外部同步实现 `ResourceSynchronizer`** — `sync_from_external` / `sync_to_external` 5. **通过 `self._children` 访问子设备** — 不要自行维护子设备引用 6. **`post_init` 中启动后台服务** — 不要在 `__init__` 中启动网络连接 @@ -698,8 +711,21 @@ python -c "from unilabos.devices.workstation.. import " # 启动测试(AST 自动扫描) unilab -g .json + +# 如果工站是外部设备包,通过外部包的 registry/check-mode 验证 +unilab --check_mode --devices ./ --external_devices_only + +# 外部设备包的离线契约测试 +pytest tests/ ``` +验证层级建议: + +1. AST/check-mode:确认 `@device`/`@action`/`@resource` 元数据能被扫描。 +2. 离线 pytest:确认 action schema、manual-confirm metadata、参数过滤、资源构造/反序列化等契约。 +3. live read-only API:确认当前部署的设备、operation、仓库、物料、报告 envelope。 +4. live write/action:只在测试环境、凭证和安全边界明确后执行。 + --- ## 现有工作站参考 diff --git a/.cursor/skills/add-workstation/bioyond.md b/.cursor/skills/add-workstation/bioyond.md new file mode 100644 index 000000000..9299bd06d --- /dev/null +++ b/.cursor/skills/add-workstation/bioyond.md @@ -0,0 +1,382 @@ +# Bioyond 工作站接入补充指南 + +本文件是 `add-workstation` 的 Bioyond/Bioyond Studio/奔曜供应商系统补充说明。遇到以下场景时读取: + +- 新接入或迁移 Bioyond 风格的外部系统工作站。 +- 继承或参考 `BioyondWorkstation`、`BioyondV1RPC`、`WorkstationHTTPService`、Bioyond 物料同步代码。 +- 需要暴露 Bioyond LIMS 订单、设备 operation、物料同步、人工确认、错误处理、报告/附件动作。 +- 外部设备包通过 `--devices ./ --external_devices_only` 加载,而不是直接改 Uni-Lab-OS monorepo。 + +## 目录 + +1. 接入姿势 +2. 基类提供什么,哪些必须按工站适配 +3. 后端路线,不使用前端路线 +4. 现场 API 测试 +5. 设备 operation 封装 +6. 注册表 / action / 人工确认契约 +7. 物料、Deck、仓库同步 +8. 回调、错误处理、报告和附件 +9. 随包 vendored 代码与运行路径 +10. 测试分层 +11. 完成检查表 + +--- + +## 1. 接入姿势 + +Bioyond 这类工作站优先按“外部系统工作站”接入,而不是按 protocol compiler 接入。默认做法: + +- 用 `WorkstationBase` 或已有 Bioyond 基类承载工站。 +- 用显式 `@action` 暴露订单创建、等待、取消、报告、物料同步、复位、错误处理等业务动作。 +- 用 `NodeType.MANUAL_CONFIRM` 做人工确认/审批/异常处置门禁。 +- 用 sub-device proxy 暴露具体设备 operation,例如机械臂、LCMS、液体处理器、合成仪、离心机等。 +- 用 PLR `Deck`/`Resource` 表示 Uni-Lab 侧资源树,用映射把 Bioyond 物料类型、仓库和库位转成 PLR 资源。 +- 用现场 API 结果修正 wrapper/schema;不要只凭旧快照或另一个工作站。 + +若项目明确要求把 Bioyond 工作流编译成 Uni-Lab protocol,再考虑 protocol 路径。否则优先站在“外部系统已有 workflow engine,Uni-Lab 负责调度、展示、同步和人工确认”的模型上。 + +--- + +## 2. 基类提供什么,哪些必须按工站适配 + +不同 Uni-Lab-OS 版本、monorepo 版本和外部设备包可能不一致。实现前先读当前环境源码,并在代码/文档里标出依赖来自哪一层。 + +### 2.1 Uni-Lab-OS 工作站框架通常提供 + +| 能力 | 通常来源 | 使用方式 | 注意 | +|------|----------|----------|------| +| 工作站注册 | `@device(..., category=["workstation"])` | 被装饰的工作站类 | AST 扫描只看可导入定义;继承/helper 里的动作需要薄 wrapper。 | +| 动作注册 | `@action` | station 或 sub-device 方法 | docstring、签名、handles、manual-confirm metadata 都会影响前端。 | +| Deck 注入 | 图文件 `config.deck` + `WorkstationBase.__init__` | `__init__(..., deck=None, **kwargs)` 后 `super().__init__(deck=deck, **kwargs)` | 不要在 `__init__` 手动新建主 Deck。 | +| 初始化阶段 | `post_init(ros_node)` | 保存 `_ros_node`,启动 HTTP 服务/监控/同步器 | 网络服务和后台线程放这里,不放 `__init__`。 | +| 子设备容器 | `ROS2WorkstationNode` | `self._ros_node.sub_devices[...]` 或当前版本的 `_children` | 以当前源码为准;不要自己维护另一套子设备列表。 | +| 资源上传 | ROS node `update_resource` | 上传 Deck 或变更资源到云端 | 注意上传根节点,避免只上传内层 child。 | +| 硬件代理 | workstation 子设备初始化 | 适合串口/IO/Modbus 型硬件 | Bioyond API proxy 通常不是这个机制,而是 RPC wrapper。 | +| 物料同步抽象 | `ResourceSynchronizer` | 自定义 `sync_from_external`/`sync_to_external`/`handle_external_change` | 抽象不负责 Bioyond payload、仓库映射、缓存策略。 | +| HTTP 回调框架 | `WorkstationHTTPService` | route 分发到 `process_*`/`handle_*` 方法 | 版本可能有 bug;Bioyond material-change 是否真正委托给 station 要测试。 | + +### 2.2 Bioyond 基类可能提供 + +如果使用已有 `BioyondWorkstation` 或 vendored Bioyond base,常见可复用能力包括: + +- `hardware_interface` 保存 Bioyond RPC client。优先访问 `self.hardware_interface`,不要假设存在 `self.rpc`。 +- workflow 队列/topic 辅助,例如 `workflow_sequence` 或 `append_to_workflow_sequence()`,但方法名和语义要看当前源码。 +- callback/report 的部分默认处理,例如 step/sample/order/material/error report 入口。 +- debug call log、请求日志、运行目录解析。 +- Bioyond resource conversion、material sync、cache 或 publish 辅助。 + +必须确认当前基类是否真的提供这些方法。不要把参考站里的 station-specific 方法当成基类能力。例如 `process_and_execute_workflow()`、`wait_for_order_finish()`、某个 station 的 material allocator、某个 station 的 workflow builder,都可能只属于那个站。 + +### 2.3 每个 Bioyond 工站通常必须适配 + +| 主题 | 必须适配的内容 | +|------|----------------| +| 订单/实验 | `order_id`、`order_code`、项目 ID、样本 ID、plate ID 的关系;创建、取消、等待、报告状态码。 | +| workflow | workflow UUID/name、步骤参数、Day1/Day2 等业务路线、运行状态转换。 | +| 设备 operation | 设备名称、operation 中文名/英文名、参数范围、下拉枚举、是否允许执行。 | +| 物料类型 | PLR 类名到 Bioyond material type UUID/显示名的映射。 | +| 仓库/库位 | warehouse UUID、site UUID、坐标命名、y 轴/前端展示方向、空位策略。 | +| 资源模型 | 是否建 wells/tips/slots 子资源;哪些外部明细只是 metadata。 | +| 同步策略 | 外部为准、Uni-Lab 为准、双向、move-first、clear-stale、虚拟暂存区。 | +| 人工确认 | 展示表列、审批默认值、assignee、异常/取消/复位如何阻断或继续流程。 | +| 报告附件 | 哪些文件类型要上传 notebook,ZIP 是否等待,失败如何重试/告警。 | + +--- + +## 3. 后端路线,不使用前端路线 + +Bioyond 前端 route 和后端 LIMS/API route 不是同一个接口层。运行时代码使用后端 API。不要用前端 route/HAR payload 实现工作站动作;前端观察最多用于理解业务页面展示,不能替代后端 schema、当前源码或 live 后端 API 结果。 + +--- + +## 4. 现场 API 测试 + +Bioyond 接入必须安排现场 API 验证。静态快照和 OpenAPI 只能保证“看起来合理”,不能证明当前部署能执行。 + +### 4.1 先做只读探测 + +优先验证这些只读事实: + +- `device-list`:设备数量、设备名称、每台设备的 operation 数量、operation 参数 schema。 +- 仓库/库位:仓库 UUID、site UUID、库位命名、空位和不可用位。 +- 物料类型:type UUID、显示名、单位、必填字段。 +- 物料列表:实际 Bioyond 返回的容器/plate/子项/位置字段。 +- 订单列表/报告:已知订单的 `order_id`、`order_code`、status、报告文件 URL。 +- 错误/异常枚举:异常处置 option、token、ijk 等字段是否和文档一致。 + +记录每次探测的 host、时间、endpoint、样本 ID、业务 `code/message/data`。如果 API 返回 HTTP 200 但业务 `code` 非成功,wrapper 必须按失败处理。 + +### 4.2 再做受控写入 + +写接口只在测试环境、凭证、样本数据和安全边界明确后执行。建议顺序: + +1. 对单个低风险 action 做最小写入。 +2. 验证 Bioyond UI/后端状态变化。 +3. 验证 Uni-Lab 资源树、日志、callback、报告是否一致。 +4. 再扩大到完整 workflow。 + +### 4.3 快照怎么用 + +`device-list` operation 快照适合生成 wrapper、dropdown 和参数范围;执行前仍要用当前 live `device-list` 验证。不同部署可能设备数相同但 operation 为空,也可能 operation 名相同但参数枚举不同。 + +--- + +## 5. 设备 operation 封装 + +Bioyond sub-device proxy 通常是薄封装:每个 `@action` 调用统一的 `_execute_operation(, params)`。 + +建议模式: + +- wrapper 类只做参数规范化、schema 描述、错误包装,不写复杂业务流程。 +- operation name 和参数 schema 来自当前部署快照 + live read-only 验证。 +- 每个 action 的错误返回包含 endpoint、device、operation、request params、业务 `code/message` 和必要 response 摘要。 +- HTTP status 不是成功标准;必须检查业务 envelope。 +- 若 operation live 不存在,动作应失败并给出清晰错误,不要静默跳过。 + +不要直接复用另一个工作站的 operation payload。Bioyond 设备 operation 具有部署相关性。 + +--- + +## 6. 注册表 / action / 人工确认契约 + +Bioyond 工作站通常会暴露许多流程动作,前端可用性取决于 registry metadata(注册元数据)。把 action schema 当成用户界面契约维护。 + +先读 [workflow-node-guidelines.md](workflow-node-guidelines.md) 里的通用规则:展示名来源、默认值、变量排序、变量分组、handles、日志、阻塞/非阻塞错误和工作流状态语义。然后再应用当前 Bioyond 工站或当前仓库定义的具体显示名、必填标记、内部字段命名等约定。 + +### 6.1 AST 可见性 + +- 所有工作流可见 action 写在当前被装饰的 station/sub-device class 上。 +- 如果能力来自 base/helper,用薄 wrapper 暴露。 +- `TypedDict`、变量分组结构、枚举、`Field(...)` 要可导入可见。 +- `@resource` Deck/labware class 要可导入可见;必要时在 package `resources/__init__.py` eager import,避免 PLR 通过 `__subclasses__()` 找不到类。 + +### 6.2 参数说明 + +推荐规则: + +- `Args:` 使用 `param[显示名]` 格式。 +- 必填标记、内部字段展示方式和业务字段命名以当前仓库约定为准;不要从某个参考工站搬运专属 schema。 +- 若同时存在用户可读编号和系统内部 ID,在说明里讲清二者来源、用途和传递方式。 +- 方法签名、`goal_default`、handles、分组字段和 docstring 的顺序尽量一致。 +- 分组参数用可导入可见的 `TypedDict` + `Annotated[..., Field(description=...)]`。 + +### 6.3 人工确认(manual-confirm) + +Bioyond 常见人工门禁: + +- 上下料确认。 +- 等待订单完成后的物料卸载/异常确认。 +- 复位前确认。 +- 错误处理 option 选择。 +- 报告/附件选择或人工审阅。 + +规则: + +- handle 输入值用于展示和依赖传递,默认只读。 +- 审批结果用 goal params,默认值必须保守。 +- 失败或拒绝时抛异常,使 workflow 真正停止。 +- 不要为 manual-confirm 节点增加没有独立业务含义的 `confirmed=True` 参数;节点本身就是确认门禁。 +- assignee、表格 placeholder、renderer 字段要在当前前端/云端验证。 + +--- + +## 7. 物料、Deck、仓库同步 + +Bioyond 物料同步是最容易反复出错的部分。先明确同步方向和身份模型,再写代码。 + +### 7.1 同步方向 + +常见策略: + +- 外部为准:从 Bioyond 拉取物料,Uni-Lab Deck 反映外部状态。 +- Uni-Lab 为准:Uni-Lab 资源变化推送到 Bioyond。 +- 双向:callback + 主动同步都可能移动资源,需要 cache/source 防止回环。 + +如果外部系统有独立物料管理,通常先实现“外部为准 + Uni-Lab 展示/操作后回写”的保守路径。 + +### 7.2 资源身份 + +为每个资源保留外部身份字段,例如 material UUID、barcode、warehouse/site UUID、order/sample 关联。移动物料时优先保留资源 identity,不要删除再新建,除非外部系统明确认为身份变化。 + +### 7.3 move-first 同步 + +推荐思路: + +1. 先按外部 material UUID 或 barcode 找已有 PLR 资源。 +2. 若存在,移动到新位置并更新 state。 +3. 若不存在,创建资源并 assign 到目标位置。 +4. 对无位置物料放入虚拟 holding/limbo,或按业务规则删除。 +5. 最后做 stale sweep,只清理本轮快照确认消失的根资源。 + +### 7.4 publish root + +更新资源树时上传合适的根节点。嵌套 plate/child 变化通常要发布 material root、warehouse root 或 Deck root,而不是只上传内层 child。否则前端可能不刷新,或云端只知道局部对象。 + +### 7.5 PLR 往返 + +每个 labware/resource class 必测: + +- `cls(name="X")` +- `cls(**resource.serialize())` +- `Resource.deserialize(...)` +- 带 `children` 的 Deck 反序列化 + +构造器建议: + +- 接受 `*args, **kwargs`。 +- 对 `size_x/size_y/size_z/model/category/sites/children/ordering` 等 serialize 字段容错。 +- 用 `kwargs.setdefault(...)` 补默认值。 +- itemized plate/tip/tube rack 提供 `ordered_items` 或 `ordering`。 + +### 7.6 不要过度建模 wells/tips + +Bioyond 明细行不一定需要成为 PLR 子资源。若每个 plate 的 wells/tips 让资源树过大、同步慢、前端卡顿,而流程不需要单孔实体动作,可以把外部 detail rows 保存为 metadata/state。只有当 Uni-Lab 侧动作需要单孔寻址、容量、液体状态或位置碰撞时,再建真实子资源。 + +--- + +## 8. 回调、错误处理、报告和附件 + +### 8.1 HTTP 回调 + +若使用 `WorkstationHTTPService`,验证每条 route 是否真正调用 station 方法: + +- `/report/step_finish` +- `/report/sample_finish` +- `/report/order_finish` +- `/report/material_change` +- `/report/error_handling` + +不要只看 HTTP ack。用本地 fake POST 和 station spy/mock 验证 `process_*` 或 `handle_*` 被调用。若当前 Uni-Lab-OS 版本对 Bioyond callback 有 bug,可以 vendored 修复,但必须写测试说明为什么 vendor。 + +### 8.2 订单完成与等待 + +等待订单完成通常要处理: + +- `order_id` 和 `order_code` 二者只有一个可用时如何反查。 +- 多订单并行时如何 disambiguate。 +- callback 先到、polling 后到或超时。 +- 成功、异常停止、人工停止等状态如何映射。 +- 完成后是否立即做 materials-by-order-id 同步。 + +不要假设参考站的 `wait_for_order_finish` 是基类方法;当前站应显式实现或 thin-wrapper。 + +### 8.3 错误处理 + +推荐两段式: + +1. 普通动作或等待动作收集错误上下文并输出给下游。 +2. manual-confirm 动作展示错误和 options,操作员选择后调用 Bioyond 错误处理接口。 + +错误处理 action 要保留 Bioyond 要求的 token/ijk/option 等字段,并对 option 过期、缺字段、重复提交给出清楚错误。 + +### 8.4 报告和 Notebook 附件 + +如果要把 Bioyond 报告写入 Uni-Lab notebook,明确区分两组后端 API:Bioyond LIMS 报告 API 提供报告文件,Uni-Lab Lab/OSS API 负责把文件作为 Notebook 附件保存。 + +Bioyond LIMS 报告 API: + +- 报告摘要:`POST {bioyond_api_host}/api/lims/order/order-report` +- 报告文件列表:`POST {bioyond_api_host}/api/lims/order/order-report-files` +- 常见 payload:`{"apiKey": "...", "requestTime": "...", "data": "ORDER_ID"}` +- 成功标准以 Bioyond 业务 envelope 为准,例如 `code == 1` 后读取 `data`;不要只看 HTTP status。 +- 文件列表可能返回相对路径,写入 Notebook 前要按当前 `api_host` 转成可下载 URL。 + +Uni-Lab Notebook/OSS API: + +- Lab API base URL 通常来自运行时 `HTTPConfig.remote_addr`,常见形态是 `https://.../api/v1`;下面的 `{lab_base_url}` 指这个后端 API base,不是浏览器页面 URL。 +- 鉴权 header 的值是 `Lab ` 加 `BasicConfig.auth_secret()`。 +- 读取 Notebook:`GET {lab_base_url}/lab/notebook/detail?uuid=NOTEBOOK_UUID`。 +- 上传附件文件:`GET {lab_base_url}/lab/storage/token?scene=file&filename=FILENAME&content_type=MIME_TYPE`,再 `PUT` 文件字节到 token 返回的 signed URL。 +- 保存完整 `lab_record`:`GET {lab_base_url}/lab/storage/token?scene=record&filename=RECORD_JSON_FILENAME&content_type=application/json&sub_path=NOTEBOOK_UUID`,再 `PUT` record JSON 到 signed URL。 +- 写回 Notebook:当前外部 runtime helper 使用 `PATCH {lab_base_url}/lab/notebook/lab-record`,payload 包含 `uuid`、`lab_record=RECORD_PUBLIC_URL`、`lab_record_status=editing`。如果当前 Uni-Lab 后端 router 改为 `PATCH /api/v1/lab/notebook/content` 等新接口,按当前 router/client 替换并做 live API 测试。 + +实现规则: + +- 从运行上下文解析 notebook/lab record ID;显式入参可覆盖。 +- 拉取 order report files,区分 PDF、Excel、ZIP、图片等。 +- ZIP 可能延迟生成;可短暂 retry,但不要无限等待。 +- Notebook 附件节点使用 Lab OSS 返回的 `public_url`、`path`、`name`、`size`、`mimeType`,不要把 Bioyond 外部 URL 直接当内部附件。 +- patch notebook `lab_record` 前先读取既有内容,追加块后整体保存,避免覆盖已有记录。 +- 用 mocked HTTP 做离线测试,live 测试只验证少量安全样本。 + +--- + +## 9. 随包 vendored 代码与运行路径 + +外部设备包常遇到 installed Uni-Lab-OS 和 monorepo 最新代码不一致。可以 vendor,但要克制。 + +适合 vendor 的情况: + +- 当前安装版缺少 Bioyond 必需行为。 +- 上游 callback 分发/资源转换/RPC envelope 有已确认 bug。 +- 迁移期间必须保持外部包在旧环境可运行。 + +vendor 要求: + +- 在文件或 package doc 中写明为何 vendor、和上游差异是什么。 +- 保持公共入口尽量与上游一致,方便未来删除 vendor。 +- 对 vendor 差异写离线测试,尤其 callback route、material change、resource conversion。 +- 运行路径使用 Uni-Lab working dir、配置项或 `cwd/unilabos_data`,不要依赖 monorepo 深度如 `Path(__file__).parents[4]`。 +- debug monitor/ping-pong 类后台线程默认不要过于嘈杂;失败要可观测但不淹没日志。 + +--- + +## 10. 测试分层 + +### 10.1 AST/check-mode + +外部设备包至少跑: + +```bash +unilab --check_mode --devices ./ --external_devices_only +``` + +确认: + +- station/sub-device/resource 都能扫描。 +- action metadata、handles、manual-confirm placeholder 不丢。 +- graph `_resource_type` 可导入。 + +### 10.2 离线 pytest + +离线契约测试应覆盖: + +- decorator fallback/stub:没有完整 ROS/unilabos 环境也能检查 `_action_registry_meta`。 +- action 参数过滤:未知字段跳过,`0`/`False`/空字符串按业务规则保留。 +- 人工确认 metadata:`NodeType.MANUAL_CONFIRM`、goal defaults、handles、必填 label。 +- PLR resource 构造/serialize/deserialize。 +- callback service fake POST。 +- report/notebook client mocked HTTP。 +- runtime log path。 + +### 10.3 live read-only + +测试当前部署事实: + +- `device-list` +- warehouse/site/material type +- known order report +- known material lookup +- report files + +### 10.4 live write/action + +只在测试环境和明确批准后执行。每次记录输入、外部系统状态变化、Uni-Lab resource tree、callback/log/report 结果。 + +--- + +## 11. 完成检查表 + +- 已确认使用后端 Bioyond API,不使用前端 route。 +- 已列出当前基类提供的方法,station-specific 方法有 explicit wrapper 或实现。 +- 已用 live read-only API 验证设备 operation、仓库、物料类型、报告样本。 +- RPC wrapper 检查业务 `code/message/data`,不只看 HTTP status。 +- action schema 的 `Args:`、handles、必填标记、`Field(...)` 一致。 +- manual-confirm 节点的展示数据、goal params、失败语义和 assignee 已验证。 +- `@resource`/Deck/labware import-visible,必要 eager import 已存在。 +- PLR 资源通过 `cls(name=...)`、`cls(**serialized)`、Deck with children 反序列化测试。 +- resource sync 保留 identity,处理 virtual holding,发布正确 root,stale sweep 不删保留 root 下的 child。 +- callback route 用 fake POST 验证确实调用 station 方法。 +- 随包 vendored 代码有原因说明和覆盖测试。 +- 外部设备包有 `--check_mode --devices ... --external_devices_only` 验证和离线 pytest。 diff --git a/.cursor/skills/add-workstation/reference.md b/.cursor/skills/add-workstation/reference.md index 0c1b9f0d5..214185650 100644 --- a/.cursor/skills/add-workstation/reference.md +++ b/.cursor/skills/add-workstation/reference.md @@ -5,9 +5,73 @@ Agent 在需要实现这些功能时按需阅读。 --- -## 1. 外部系统集成模式 +## 1. 外部系统工作站通用守则 -### 1.1 RPC 客户端 +外部系统工作站的难点通常不在类模板,而在事实来源、现场 API、注册表可见性和物料同步边界。开始实现前先按下面规则定边界。 + +### 1.1 先确定事实来源 + +建议按优先级取证: + +1. 当前仓库源码与当前运行环境中的 Uni-Lab-OS 源码。 +2. 当前项目的后端 OpenAPI/schema、供应商 API 文档、示例配置。 +3. 当前部署的只读现场 API 结果,例如设备列表、仓库/库位、物料列表、订单报告。 +4. 其他工作站源码、旧文档、历史日志、HAR/浏览器观察,只作为模式参考。 + +前端 route 和后端 route 可能完全不同。运行时代码使用后端 API,不要把前端/HAR route 当成工作站 API。 + +### 1.2 区分基类能力和工站差异 + +先查当前基类已经提供什么,再决定 thin wrapper、override 或重写。 + +通常由 Uni-Lab-OS 工作站框架提供: + +- `deck` 反序列化并传入 `__init__(..., deck=None, **kwargs)`。 +- `post_init(ros_node)` 阶段拿到 ROS 节点和子设备。 +- `self._ros_node.sub_devices` / `self._children` 等子设备访问入口(以当前源码为准)。 +- `ResourceSynchronizer` 这样的同步抽象和 `update_resource` 上传资源树机制。 +- `WorkstationHTTPService` 这类回调分发框架(若当前版本提供)。 + +通常必须按每个工站适配: + +- API endpoint、认证方式、请求/响应 envelope、状态码含义。 +- workflow/order 创建参数、用户可读编号与内部 ID 的关系、报告轮询逻辑。 +- 设备 operation 名称、下拉枚举、范围参数、错误恢复选项。 +- 物料类型、仓库/库位映射、坐标方向、空位和虚拟暂存策略。 +- 人工确认节点的展示表、审批默认值、assignee、失败语义。 +- 资源树粒度:是否真的需要 wells/tips/子孔,还是只保存外部明细元数据。 + +不要假设参考工作站里的方法都是基类能力。继承方法或 helper 行为若要暴露给流程图,通常需要在当前 `@device` 类上写薄 wrapper,让 AST 扫描和前端注册表都能看到。 + +### 1.3 注册表、动作 schema 与人工确认 + +- 工作流动作必须在被装饰的 `@device` 类上 AST/import 可见。 +- 资源/Deck 类也要 import-visible;图文件的 `_resource_type` 应指向可导入的 decorated 类。class 形式资源必须在运行时被 import,不能只依赖 AST 扫描。 +- `Args:` docstring、参数名、`Field(title/description)`、handle key、默认值和 required 语义要一致。显示名和必填标记等 UI 约定以当前仓库文档为准。 +- `NodeType.MANUAL_CONFIRM` 用于人工门禁。handle 传入值应视为只读展示/依赖数据;审批结果用 goal params 承载并给安全默认值。确认失败时抛异常,避免仅返回 `{"success": False}` 后被当成普通动作完成。 + +### 1.4 现场 API 测试不可省 + +供应商系统可能出现“HTTP 200 但业务失败”或“同一设备数但 operation 为空”。写 wrapper 时要解析业务 envelope(如 `code`/`message`/`data`),并在错误中带上 endpoint、设备、operation 和原始关键信息。 + +快照只能帮助设计 wrapper 和 schema;执行前仍要用当前部署的现场 API 验证。写接口需要真实测试环境和明确批准;只读探测也要记录 host、时间、返回 envelope 和样本 ID。 + +### 1.5 物料与资源同步先做往返测试 + +PLR 资源类至少要通过两种构造路径: + +- 注册/同步时的 `cls(name=...)` 或工厂式创建。 +- 反序列化时的 `cls(**serialized_dict)`。 + +优先使用容错构造器、`**kwargs`、`kwargs.setdefault(...)`,并为 itemized 资源提供 `ordered_items` 或 `ordering`。如果 Deck 已带有反序列化出的 `children`,不要再次初始化默认子物料,避免重复或陈旧资源。 + +需要默认 children/layout 的资源可实现 `setup()`,工作站 Deck 自动放置 WareHouse 是典型例子。`setup` 必须默认为 `False`,只有首次创建默认布局时显式打开;若图文件传入 `config.setup=true`,也只是构造参数,只有对应资源类的 `__init__` 显式处理时才会生效。默认保持 `False` 可避免反序列化已有 `children` 时重复创建或覆盖持久化状态。 + +--- + +## 2. 外部系统集成模式 + +### 2.1 RPC 客户端 与外部 LIMS/MES 系统通信的标准模式。继承 `BaseRequest`,所有接口统一用 POST。 @@ -41,7 +105,7 @@ class MySystemRPC(BaseRequest): 参考:`unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py`(`BioyondV1RPC`) -### 1.2 HTTP 回调服务 +### 2.2 HTTP 回调服务 接收外部系统报送的标准模式。使用 `WorkstationHTTPService`,在 `post_init` 中启动。 @@ -96,7 +160,7 @@ def process_order_finish_report(self, report_request, used_materials) -> Dict[st 参考:`unilabos/devices/workstation/workstation_http_service.py` -### 1.3 连接监控 +### 2.3 连接监控 独立线程周期性检测外部系统连接状态,状态变化时发布 ROS 事件。 @@ -128,11 +192,11 @@ class ConnectionMonitor: --- -## 2. Config 结构模式 +## 3. Config 结构模式 工作站的 `config` 在图文件中定义,传入 `__init__`。以下是常见字段模式: -### 2.1 外部系统连接 +### 3.1 外部系统连接 ```json { @@ -141,7 +205,7 @@ class ConnectionMonitor: } ``` -### 2.2 HTTP 回调服务 +### 3.2 HTTP 回调服务 ```json { @@ -152,7 +216,7 @@ class ConnectionMonitor: } ``` -### 2.3 物料类型映射 +### 3.3 物料类型映射 将 PLR 资源类名映射到外部系统的物料类型(名称 + UUID)。用于双向物料转换。 @@ -165,7 +229,7 @@ class ConnectionMonitor: } ``` -### 2.4 仓库映射 +### 3.4 仓库映射 将仓库名映射到外部系统的仓库 UUID 和库位 UUID。用于入库/出库操作。 @@ -183,7 +247,7 @@ class ConnectionMonitor: } ``` -### 2.5 工作流映射 +### 3.5 工作流映射 将内部工作流名映射到外部系统的工作流 ID。 @@ -195,7 +259,7 @@ class ConnectionMonitor: } ``` -### 2.6 物料默认参数 +### 3.6 物料默认参数 ```json { @@ -212,9 +276,9 @@ class ConnectionMonitor: --- -## 3. 资源同步机制 +## 4. 资源同步机制 -### 3.1 ResourceSynchronizer +### 4.1 ResourceSynchronizer 抽象基类,用于与外部物料系统双向同步。定义在 `workstation_base.py`。 @@ -246,7 +310,7 @@ class MyResourceSynchronizer(ResourceSynchronizer): return True ``` -### 3.2 update_resource — 上传资源树到云端 +### 4.2 update_resource — 上传资源树到云端 将 PLR Deck 序列化后通过 ROS 服务上传。典型使用场景: @@ -268,7 +332,7 @@ ROS2DeviceNode.run_async_func( --- -## 4. 工作流序列管理 +## 5. 工作流序列管理 工作站通过 `workflow_sequence` 属性管理任务队列(JSON 字符串形式)。 @@ -301,7 +365,7 @@ class MyWorkstation(WorkstationBase): --- -## 5. 站间物料转移 +## 6. 站间物料转移 工作站之间转移物料的模式。通过 ROS ActionClient 调用目标站的动作。 @@ -332,7 +396,7 @@ async def transfer_materials_to_another_station( --- -## 6. post_init 完整模式 +## 7. post_init 完整模式 `post_init` 是工作站初始化的关键阶段,此时 ROS 节点和子设备已就绪。 diff --git a/.cursor/skills/add-workstation/workflow-node-guidelines.md b/.cursor/skills/add-workstation/workflow-node-guidelines.md new file mode 100644 index 000000000..867ebb723 --- /dev/null +++ b/.cursor/skills/add-workstation/workflow-node-guidelines.md @@ -0,0 +1,408 @@ +# 工作流节点指南 + +新增或修改工作站、子设备对工作流暴露的 `@action` 节点时阅读本文件。本文说明通用的节点 schema、参数分组、展示名、handles(连线端口)、日志和错误语义。具体工作站的命名、必填标记、内部字段展示方式等项目约定,仍以当前仓库文档或对应专项参考为准。 + +## 目录 + +1. 把暴露动作当成契约 +2. 注册表可见性 +3. 展示名和说明文本 +4. 默认值与必填语义 +5. 变量排序 +6. 变量分组 +7. Handles(连线端口)与返回数据 +8. 人工确认(manual-confirm)节点 +9. `always_free` +10. 日志与状态写法 +11. 阻塞错误与非阻塞错误 +12. 工作流执行状态 +13. 验证清单 + +--- + +## 1. 把暴露动作当成契约 + +暴露给工作流的 `@action` 不只是一个 Python 方法。它同时是以下几层之间的契约: + +- Python 运行时; +- registry AST 扫描器; +- 工作流图编辑器; +- 上游/下游 handles; +- 操作员看到的 UI; +- 工作流执行器的状态和错误处理。 + +修改一个 action 输入时,要同步检查这些表面: + +- 方法签名; +- `@action(...)` metadata; +- `goal_default`; +- handles; +- 变量分组结构; +- docstring 的 `Args:`; +- output handle 依赖的返回字段; +- check-mode 或契约测试。 + +--- + +## 2. 注册表可见性 + +工作流要调用的方法,尽量直接写在被 `@device` 装饰的类体内。AST 扫描最可靠识别的是被装饰类里直接声明的方法。 + +如果真实逻辑在基类、helper、mixin 或组合对象中,在被装饰类上加薄 wrapper: + +```python +@action(description="提交实验到外部调度系统") +def submit_experiment(self, params: SubmitExperimentParams) -> Dict[str, Any]: + return self._submit_experiment_impl(params) +``` + +不希望暴露给工作流的公共 helper 用 `@not_action` 标记。变量分组结构、`TypedDict`、枚举和 decorator 依赖也要保持 import-visible。 + +--- + +## 3. 展示名和说明文本 + +展示名常来自 docstring、变量分组字段和 handles。除非有明确 UI 理由,否则几处命名要保持一致。本节先说明顶层 action 参数和 handles;分组字段见第 6 节。 + +### 3.1 Docstring `Args:` + +顶层 action 参数的展示名和用户说明写在 `Args:` 中: + +```python +def submit_experiment( + self, + inputs: SubmitExperimentInputs, + options: Optional[SubmitExperimentOptions] = None, + sample_file_path: str = "", +) -> Dict[str, Any]: + """提交实验。 + + Args: + inputs[实验输入]: 创建实验所需的主要参数。 + options[实验选项]: 设置实验命名、同步和报告等可选行为。 + sample_file_path[样品文件路径]: 通常由上游上传节点传入的样品文件路径。 + """ +``` + +方括号前写真实 Python 参数名,方括号内写 UI 展示名。说明文本解释用户行为和业务含义,不写内部 HTTP route 或 payload 细节。 + +### 3.2 Handles + +handle 的 `label` 用来命名图里的端口: + +```python +ActionInputHandle( + key="sample_file_path", + data_type="sample_file_path", + label="样品文件路径", + data_key="sample_file_path", + data_source=DataSource.HANDLE, + io_type="source", +) +``` + +`key` 和 `data_key` 尽量与实际输入字段或返回字段一致。`label` 负责展示名。只有图端口需要比表单字段更短时,才有意让 label 和表单展示名不同。 + +--- + +## 4. 默认值与必填语义 + +必填有多层含义: + +- Python 签名:没有默认值通常表示运行时调用必须提供该参数。 +- `goal_default`:UI/工作流 payload 默认值;不会替代 Python 运行时默认值。 +- `TypedDict`:`total=True` 时组内字段必填;`total=False` 时组内字段可选。 +- `Field(default=...)`:变量分组字段的 schema/UI 默认值。 +- 项目展示约定:仓库可能另有视觉必填标记。 + +函数签名中的可变对象或昂贵默认值用 `None`,在函数体内归一化: + +```python +@action(goal_default={"order_ids": [], "timeout_seconds": 3600}) +def wait_for_orders( + self, + order_ids: Optional[list[str]] = None, + timeout_seconds: int = 3600, +) -> Dict[str, Any]: + order_ids = list(order_ids or []) +``` + +不要依赖 `goal_default` 给 Python 运行时补齐必需参数。 + +--- + +## 5. 变量排序 + +在兼容性允许时,函数签名、`goal_default`、handles、变量分组字段、docstring 和测试中使用同一顺序。 + +推荐排序: + +1. 核心业务输入,机器必填字段先于可选字段。 +2. 可选业务输入。 +3. 由 handles 或运行上下文传入的上游/运行时值。 +4. timeout、retry、poll interval、assignee 等运行控制。 +5. 高级、调试、兼容性开关。 + +同一组内按工作流重要性排序,不按字母排序。让人识别本次操作的字段放前面,timeout/retry/log 开关靠后。 + +如果旧方法签名必须保持兼容,可以保留运行时签名,但仍把 docstring、handles 和 UI 字段排成对用户最友好的顺序。 + +--- + +## 6. 变量分组 + +当节点输入很多,或字段面向不同使用者时,用分组降低 UI 和 schema 复杂度。 + +推荐分组: + +- **核心业务输入**:操作员或流程作者必须理解的值。 +- **可选业务输入**:名称、备注、方法选择、改变业务行为的开关。 +- **上游/运行时输入**:通常由 handle 传入的 ID、文件路径、缓存 payload、上下文值。 +- **运行控制输入**:timeout、retry、poll interval、assignee 等。 +- **高级/调试输入**:dry-run、verbose logging、原始 payload override、兼容性开关。 + +少量关键字段可以放在 action 顶层参数。字段较多时,用 `TypedDict` 或类似结构表达变量分组。分组中的每个字段用 `Field(title=..., description=...)` 设置展示名和说明;字段名仍是机器 key,`title` 负责 UI label,`description` 说明用户需要做什么决定。 + +```python +from typing import Optional, TypedDict +from typing_extensions import Annotated +from pydantic import Field + + +class SubmitExperimentInputs(TypedDict): + sample_file: Annotated[ + str, + Field( + title="样品文件", + description="用于创建实验的样品文件。", + ), + ] + method_name: Annotated[ + str, + Field( + title="实验方法", + description="本次实验使用的方法或工作流模板。", + ), + ] + + +class SubmitExperimentOptions(TypedDict, total=False): + experiment_name: Annotated[ + str, + Field( + title="实验名称", + description="可选的实验展示名称。", + ), + ] + sync_materials_after_submit: Annotated[ + bool, + Field( + default=True, + title="提交后同步物料", + description="实验创建后是否刷新本地资源树。", + ), + ] +``` + +内部 ID、缓存 payload、调试字段不要混进人填写的核心业务分组,除非操作员确实需要查看或填写它们。 + +--- + +## 7. Handles(连线端口)与返回数据 + +handles 是工作流连线端口,不自动等同于必填。 + +建议: + +- 上游节点提供的值用 `ActionInputHandle`。 +- 本节点返回给下游的值用 `ActionOutputHandle`。 +- `data_type` 保持稳定,方便上下游匹配。 +- `data_key` 指向真实输入 key 或返回 key。 +- 下游依赖 output handle 时,返回对象结构要稳定。 +- action 可能局部失败但继续时,返回稳定形状的 `warnings`、`errors` 或逐项结果数组。 + +示例: + +```python +@action( + handles=[ + ActionOutputHandle( + key="experiment_id", + data_type="experiment_id", + label="实验 ID", + data_key="experiment_id", + data_source=DataSource.EXECUTOR, + ), + ], +) +def create_experiment(self, inputs: SubmitExperimentInputs) -> Dict[str, Any]: + return { + "success": True, + "experiment_id": "...", + "warnings": [], + } +``` + +--- + +## 8. 人工确认(manual-confirm)节点 + +人工门禁、审批、复核、异常选择使用 `NodeType.MANUAL_CONFIRM`。 + +通用规则: + +- 硬件动作和不可逆状态变更尽量放在普通 action 中。 +- manual-confirm 节点负责展示、审批和收集操作员决策。 +- handle 传入的值默认视为复核/展示数据,除非当前 UI 明确支持编辑。 +- 操作员决策放在 goal params 中,并给保守默认值。 +- 按当前 renderer 要求设置 `placeholder_keys`。 +- 被拒绝或超时后需要停止 workflow 时,抛异常;不要只返回 `success=False`。 + +--- + +## 9. `always_free` + +`always_free=True` 会绕过普通忙碌队列,要有明确理由。 + +通常适合: + +- manual-confirm 节点; +- 轻量状态查询、缓存查询; +- 资源发布或 metadata 刷新; +- 等待/轮询外部调度系统,且不应占住本地设备锁的动作; +- 只把任务送入远端调度系统并快速返回的 proxy 动作。 + +通常避免: + +- 本地硬件运动; +- 必须和其他 action 串行保护的物理状态变更; +- 共享非线程安全 client 或资源且没有锁的动作。 + +--- + +## 10. 日志与状态写法 + +做 live workflow 调试前先设计日志。日志要能解释 action 做了什么、调了哪个外部系统、给下游留下了哪些关键结果。 + +### 10.1 日志入口 + +普通日志入口优先沿用当前仓库写法。Uni-Lab-OS 代码里常见两类: + +```python +from unilabos.utils.log import logger +``` + +或者在 driver 已保存 ROS 节点时用 `self._ros_node.lab_logger()`,它会给日志加设备 namespace。 + +`configure_logger()` 只配置 console/file handler。真正产生日志的是代码执行到 `logger.info(...)`、`logger.debug(...)`、`logger.warning(...)`、`logger.error(...)`,或对应的 `self._ros_node.lab_logger().info(...)` 调用。只 import logger 不会产生日志。 + +Uni-Lab-OS 框架会自动记录一部分运行日志,例如 action 收到原始 goal、action 函数抛异常时的 traceback、HostNode 收到 job result 后的 success/failed 状态。业务步骤、外部 API 返回摘要、关键 ID、可恢复降级等信息不会自动出现,需要 action 或 helper 代码在对应位置显式调用 logger。 + +不要为每个 action 机械包一层 `try/except` 只为了打 traceback;`@action` 外层执行器已经会捕获异常、记录错误,并把 action 结果标成失败。只有需要补充业务上下文时才捕获,例如 endpoint、order ID、material ID,然后继续抛异常或返回明确的非阻塞状态。 + +### 10.2 记录内容 + +在 action 边界记录: + +- action 名称; +- correlation ID,或 workflow/job/order ID; +- 归一化后的输入,注意脱敏; +- 外部 endpoint/operation(如果有); +- 开始/结束时间和耗时; +- 外部调用的状态码、业务 code 或错误摘要; +- 下游 handles 会用到的关键返回 ID、数量、文件名或状态。 + +大 payload 只记录脱敏摘要和原始调试文件路径,不要把 secrets、API key、完整凭证或大型私密文件写入普通日志。 + +### 10.3 等级约定 + +按 workflow 影响选择日志等级和返回形状: + +- `info`:action 开始/完成、关键 ID、计数、endpoint/operation。 +- `debug`/`trace`:归一化参数、脱敏 payload 摘要、原始调试文件路径;大对象不要直接打到普通日志。 +- `warning`:可恢复或非阻塞降级;同时在返回值里写 `warnings` 或明确 status。 +- `error`:捕获到并处理的业务/API 失败。日志本身不会改变 action 状态;如果当前 action 必须失败,抛异常或让异常继续冒出。如果 workflow 可以继续,返回结构化结果,不要只靠 `logger.error()` 表达失败。 + +--- + +## 11. 阻塞错误与非阻塞错误 + +写 action 前先明确失败语义。 + +### 11.1 阻塞错误 + +下游继续执行会不安全或没有意义时,使用阻塞错误: + +- 缺少必需输入; +- 硬件状态未知; +- 物料身份无法解析; +- 外部 API 拒绝了核心操作; +- manual-confirm 被拒绝或超时; +- 下游 handles 会收到误导性的 ID 或资源状态。 + +阻塞错误应抛异常,并提供足够上下文。除非已经测试过当前 executor 行为,否则不要依赖 `{"success": False}` 来停止 workflow;对 `@action` 来说,正常 return 一个包含 `success=False` 的 dict 仍可能只是一次正常返回。 + +### 11.2 非阻塞错误 + +主流程可以继续时,使用非阻塞错误: + +- 可选报告文件暂不可用; +- best-effort sync 失败,但核心 action 已成功; +- batch 中某一项失败,其余项成功; +- cache refresh 失败,但旧状态仍可用; +- 外部系统给出 warning,但不应停止执行。 + +非阻塞错误返回稳定的成功对象,并放入 `warnings`、`errors` 或逐项状态。返回数据要清楚说明发生了什么、下游还能信任哪些字段。 + +```python +return { + "success": True, + "experiment_id": experiment_id, + "warnings": ["报告压缩包重试后仍未就绪。"], + "attachments": attached_files, +} +``` + +如果 workflow 需要按失败分支,返回明确的 status 字段供下游判断。 + +--- + +## 12. 工作流执行状态 + +开发 action 时把三件事分开验证: + +- Python 返回值:给 handles 和下游节点使用的数据。 +- action/job 状态:平台记录这次节点执行是否成功。 +- workflow 是否继续:图配置、executor 语义或错误处理节点决定后续节点是否运行。 + +不要假设 action 返回 `success=False` 一定会停止整个 workflow。在某些 Uni-Lab-OS 流程中,节点 execution status 可能显示为 `false`,但下游 workflow 仍继续执行。 + +按这个现实设计: + +- 必须停止 workflow 的失败要 raise。 +- 可分支的结果返回明确 status 字段。 +- 只有下游可安全使用时,才保持 output handles 稳定输出。 +- 非阻塞失败不要只返回 `success=False`,可使用类似 `"completed_with_warnings"` 的清楚状态。 +- 可由操作员处理的阻塞失败,进入 manual-confirm 或 error-handling 节点。 +- 如果继续/停止行为与安全相关,要给 executor 行为补测试。 + +把已有 action 从“返回 false”改成“raise”之前,先检查下游 workflow 是否有意在 false 节点状态后继续。 + +--- + +## 13. 验证清单 + +提交前检查: + +- 暴露方法直接在被装饰类上可见,或有薄 wrapper。 +- 顶层输入已按使用者和行为分组。 +- 函数签名、`goal_default`、handles、分组字段、docstring 和测试使用一致 key。 +- 变量顺序对用户友好,并在 metadata 表面保持一致。 +- 展示名通过 `Args:`、`Field(title=...)` 和 handle `label` 设置。 +- 描述说明用户/业务含义,不写内部 route。 +- Python 默认值、`goal_default`、分组字段默认值有意对齐。 +- handles 指向真实输入/返回 key。 +- manual-confirm 的拒绝/超时阻塞门禁会 raise。 +- 日志记录 action 开始/结束、外部调用结果、下游关键 ID,且已脱敏。 +- 阻塞错误 raise;非阻塞失败返回结构化 warnings/status。 +- 测试或 check-mode 覆盖 action metadata 和工作流关键状态语义。