mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 03:47:21 +00:00
Compare commits
3 Commits
212f9ec448
...
f14e1bc4a0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f14e1bc4a0 | ||
|
|
247a0ee4c6 | ||
|
|
a084031af0 |
461
plan/2026-05-21_01_peptide_reset_four_checkbox_plan.md
Normal file
461
plan/2026-05-21_01_peptide_reset_four_checkbox_plan.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Peptide Four-Checkbox Reset Plan
|
||||
|
||||
Date: 2026-05-21 16:30
|
||||
Status: Proposal only / not executed
|
||||
|
||||
## Scope
|
||||
|
||||
This plan replaces `2026-05-21_1556_peptide_reset_sirna_reference_plan.md` for Peptide reset work.
|
||||
|
||||
User direction captured here:
|
||||
|
||||
- `take_out` is unnecessary for Peptide reset.
|
||||
- Do not add a material-cache refresh checkbox.
|
||||
- Change reset to four checkbox-controlled operations:
|
||||
- 调度器复位
|
||||
- 订单状态复位
|
||||
- 库位复位
|
||||
- 仪器复位
|
||||
- The first three checkboxes default to checked.
|
||||
- The fourth checkbox, 仪器复位 / `reset_devices`, defaults to unchecked.
|
||||
- Replace the current public `reset` action with:
|
||||
- `reset_auto`: normal ILab action node. This is the renamed/replaced version of the current reset implementation.
|
||||
- `reset_manual`: manual-confirm action node with a physical cleanup confirmation message.
|
||||
|
||||
## Evidence Summary
|
||||
|
||||
Current Peptide source:
|
||||
|
||||
- Reset action code is currently in `unilabos/devices/workstation/bioyond_studio/peptide_station/peptide_station.py`.
|
||||
- Current Peptide reset selects `scheduler_reset`, `reset_order_status`, and `reset_location`, and passes ids to order/location resets.
|
||||
- `BioyondV1RPC.reset_devices()` already calls `/api/lims/device/reset-devices` with only `apiKey` and `requestTime`.
|
||||
- `BioyondV1RPC.scheduler_reset()` already calls `/api/lims/scheduler/reset` with only `apiKey` and `requestTime`.
|
||||
- `BioyondV1RPC.reset_order_status(order_id)` and `reset_location(location_id)` currently send `data`, but live probes showed that omitted `data` succeeds.
|
||||
|
||||
Live Peptide no-data reset probes using `temp_benyao/peptide/peptide_station_config.example.json`:
|
||||
|
||||
- `POST /api/lims/order/reset-order-status` with request keys `["apiKey", "requestTime"]` returned HTTP 200 and `code=1`.
|
||||
- `POST /api/lims/scheduler/reset` with request keys `["apiKey", "requestTime"]` returned HTTP 200 and `code=1`.
|
||||
- `POST /api/lims/storage/reset-location` with request keys `["apiKey", "requestTime"]` returned HTTP 200 and `code=1`.
|
||||
- `reset-devices` was not live-probed in this session, but the current RPC wrapper already sends no `data`.
|
||||
|
||||
Raw findings:
|
||||
|
||||
- `temp_benyao/peptide/_findings/2026-05-21_1613_reset_order_status_no_data_live.md`
|
||||
- `temp_benyao/peptide/_findings/2026-05-21_1615_remaining_resets_no_data_live.md`
|
||||
|
||||
## Proposed Public Actions
|
||||
|
||||
### `reset_auto`
|
||||
|
||||
Normal action node. This is the auto/no-manual-confirm path. It replaces the current public `reset` action; do not leave a second public `reset` action unless a later compatibility request explicitly asks for an alias.
|
||||
|
||||
Checkbox schema rule:
|
||||
|
||||
- Use plain `bool` annotations in the action signature.
|
||||
- Do not use `Annotated[bool, Field(...)]` for these checkbox params in this implementation plan.
|
||||
- The current AST registry schema path does not unwrap `Annotated[...]`; plain `bool` is required so generated JSON Schema marks the fields as boolean and the renderer can show checkboxes.
|
||||
- Put human-facing labels/descriptions in the method docstring or action description. If field-level `Field(description=...)` metadata is required later, add registry `Annotated` support and a schema test as a separate change.
|
||||
|
||||
Decorator shape:
|
||||
|
||||
```python
|
||||
@action(
|
||||
always_free=True,
|
||||
goal_default={
|
||||
"reset_scheduler": True,
|
||||
"reset_order_status": True,
|
||||
"reset_location": True,
|
||||
"reset_devices": False,
|
||||
},
|
||||
description="自动复位调度器/订单状态/库位,可选仪器复位",
|
||||
)
|
||||
def reset_auto(
|
||||
self,
|
||||
reset_scheduler: bool = True,
|
||||
reset_order_status: bool = True,
|
||||
reset_location: bool = True,
|
||||
reset_devices: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""自动复位调度器/订单状态/库位,可选仪器复位。
|
||||
|
||||
Args:
|
||||
reset_scheduler[调度器复位]: 调用 /api/lims/scheduler/reset,默认勾选。
|
||||
reset_order_status[订单状态复位]: 调用 /api/lims/order/reset-order-status,默认勾选。
|
||||
reset_location[库位复位]: 调用 /api/lims/storage/reset-location,默认勾选。
|
||||
reset_devices[仪器复位]: 调用 /api/lims/device/reset-devices,默认不勾选。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- Use real plain-`bool` parameters, not hidden `**kwargs` and not `Annotated`, so the action renderer can expose four checkboxes.
|
||||
- Rename/replace the existing `reset` action as `reset_auto`; the implementation should not keep the old id-shaped `reset` action as another public path by default.
|
||||
- Keep the three routine reset defaults checked.
|
||||
- Keep `reset_devices` unchecked because it can be broader and more disruptive.
|
||||
- Do not require or resolve order ids or location ids.
|
||||
- Do not call `take_out`.
|
||||
- Do not call `refresh_material_cache`.
|
||||
|
||||
### `reset_manual`
|
||||
|
||||
Manual-confirm node. It should show the operator a physical cleanup warning, then execute the same reset helper as `reset_auto` after the operator confirms.
|
||||
|
||||
Actual manual-confirm decorator pattern in this repo:
|
||||
|
||||
- Use `@action(node_type=NodeType.MANUAL_CONFIRM)`.
|
||||
- Set `always_free=True`.
|
||||
- Add `placeholder_keys={"assignee_user_ids": "unilabos_manual_confirm"}`.
|
||||
- Include `timeout_seconds: int` and `assignee_user_ids: list[str]`.
|
||||
- Add `goal_default` for `timeout_seconds` and `assignee_user_ids`.
|
||||
- Manual-confirm actions are normally side-effect-light, but existing Peptide `start_experiment` is already a `MANUAL_CONFIRM` action that performs scheduler start after the operator gate, so a reset-after-confirm pattern is compatible with current Peptide style.
|
||||
|
||||
Proposed confirmation text:
|
||||
|
||||
```text
|
||||
请确认G3、CEM、Tecan、撕膜机、封膜机、打标机、旋转堆栈上下料位、3个转台等位置的物料已清理完毕;
|
||||
请开门检查冰箱、IDOT、酶标仪、离心机、LCMS内部没有遗留物料。
|
||||
```
|
||||
|
||||
Decorator/function shape:
|
||||
|
||||
```python
|
||||
RESET_MANUAL_CONFIRM_MESSAGE = (
|
||||
"请确认G3、CEM、Tecan、撕膜机、封膜机、打标机、旋转堆栈上下料位、3个转台等位置的物料已清理完毕;\n"
|
||||
"请开门检查冰箱、IDOT、酶标仪、离心机、LCMS内部没有遗留物料。"
|
||||
)
|
||||
|
||||
@action(
|
||||
always_free=True,
|
||||
node_type=NodeType.MANUAL_CONFIRM,
|
||||
placeholder_keys={"assignee_user_ids": "unilabos_manual_confirm"},
|
||||
goal_default={
|
||||
"reset_scheduler": True,
|
||||
"reset_order_status": True,
|
||||
"reset_location": True,
|
||||
"reset_devices": False,
|
||||
"physical_cleanup_confirmed": False,
|
||||
"timeout_seconds": 3600,
|
||||
"assignee_user_ids": [],
|
||||
},
|
||||
feedback_interval=300,
|
||||
description=RESET_MANUAL_CONFIRM_MESSAGE,
|
||||
)
|
||||
def reset_manual(
|
||||
self,
|
||||
reset_scheduler: bool = True,
|
||||
reset_order_status: bool = True,
|
||||
reset_location: bool = True,
|
||||
reset_devices: bool = False,
|
||||
physical_cleanup_confirmed: bool = False,
|
||||
timeout_seconds: int = 3600,
|
||||
assignee_user_ids: Optional[List[str]] = None,
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""人工确认物理清理后执行复位。
|
||||
|
||||
Args:
|
||||
reset_scheduler[调度器复位]: 调用 /api/lims/scheduler/reset,默认勾选。
|
||||
reset_order_status[订单状态复位]: 调用 /api/lims/order/reset-order-status,默认勾选。
|
||||
reset_location[库位复位]: 调用 /api/lims/storage/reset-location,默认勾选。
|
||||
reset_devices[仪器复位]: 调用 /api/lims/device/reset-devices,默认不勾选。
|
||||
physical_cleanup_confirmed[物理清理确认]: 确认清理提示中的物料检查已经完成,默认不勾选。
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
Execution rule:
|
||||
|
||||
- If `physical_cleanup_confirmed` is false, return a blocked result and do not call any reset API.
|
||||
- If it is true, call the same internal helper as `reset_auto`.
|
||||
- Return `confirmation_message` in the result payload so call logs preserve the exact operator instruction text.
|
||||
|
||||
Renderer caveat:
|
||||
|
||||
- `description` should carry the warning in generated action metadata.
|
||||
- `physical_cleanup_confirmed` must remain a plain `bool` so it renders as a checkbox.
|
||||
- The cleanup warning should be carried by the action `description` and the docstring param description. Do not rely on `Field(description=...)` unless registry `Annotated` support has been implemented and tested.
|
||||
- If the current frontend does not show action descriptions or docstring field descriptions reliably, add a read-only string parameter such as `confirmation_message: str = RESET_MANUAL_CONFIRM_MESSAGE` with `goal_default`, or use a handle-based display only after renderer behavior is verified.
|
||||
|
||||
## Shared Internal Helper
|
||||
|
||||
Both public actions should delegate to one helper, for example:
|
||||
|
||||
```python
|
||||
def _execute_reset_operations(
|
||||
self,
|
||||
*,
|
||||
reset_scheduler: bool,
|
||||
reset_order_status: bool,
|
||||
reset_location: bool,
|
||||
reset_devices: bool,
|
||||
) -> Dict[str, Any]:
|
||||
...
|
||||
```
|
||||
|
||||
Call order:
|
||||
|
||||
1. `scheduler_reset`
|
||||
2. `reset_order_status`
|
||||
3. `reset_location`
|
||||
4. `reset_devices`
|
||||
|
||||
Result shape:
|
||||
|
||||
```python
|
||||
{
|
||||
"selected_operations": [
|
||||
{"key": "reset_scheduler", "label": "调度器复位", "selected": True},
|
||||
{"key": "reset_order_status", "label": "订单状态复位", "selected": True},
|
||||
{"key": "reset_location", "label": "库位复位", "selected": True},
|
||||
{"key": "reset_devices", "label": "仪器复位", "selected": False},
|
||||
],
|
||||
"executed_calls": [
|
||||
{"operation": "scheduler_reset", "endpoint": "/api/lims/scheduler/reset", "result": {"code": 1}},
|
||||
],
|
||||
"skipped_operations": [
|
||||
{"operation": "reset_devices", "reason": "checkbox_disabled"},
|
||||
],
|
||||
"warnings": [],
|
||||
}
|
||||
```
|
||||
|
||||
Failure handling:
|
||||
|
||||
- Execute selected operations sequentially and record each result.
|
||||
- If an operation returns non-`1` code, add a warning and continue unless the caller later requests fail-fast.
|
||||
- If an RPC method raises, catch it, record an error entry, and continue to the next selected operation unless fail-fast is introduced.
|
||||
|
||||
## RPC Wrapper Adjustment
|
||||
|
||||
Adjust the two id-shaped wrappers to no-data calls:
|
||||
|
||||
- `BioyondV1RPC.reset_order_status()` should no longer require `order_id`.
|
||||
- `BioyondV1RPC.reset_location()` should no longer require `location_id`.
|
||||
|
||||
Current no-data wrappers already exist:
|
||||
|
||||
- `scheduler_reset()`
|
||||
- `reset_devices()`
|
||||
|
||||
Suggested RPC signatures:
|
||||
|
||||
```python
|
||||
def scheduler_reset(self) -> int: ...
|
||||
def reset_order_status(self) -> int: ...
|
||||
def reset_location(self) -> int: ...
|
||||
def reset_devices(self) -> int: ...
|
||||
```
|
||||
|
||||
Compatibility option:
|
||||
|
||||
```python
|
||||
def reset_order_status(self, order_id: Optional[str] = None) -> int:
|
||||
del order_id
|
||||
...
|
||||
|
||||
def reset_location(self, location_id: Optional[str] = None) -> int:
|
||||
del location_id
|
||||
...
|
||||
```
|
||||
|
||||
This keeps older code from crashing while making the actual wire request no-data.
|
||||
|
||||
## Adjusted Runtime API Schemas
|
||||
|
||||
These are the schemas Peptide reset code should target at runtime after the live no-data probes. They intentionally omit `data`, even though OpenAPI models nullable `data` for these endpoints.
|
||||
|
||||
All four requests use:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "string",
|
||||
"requestTime": "date-time"
|
||||
}
|
||||
```
|
||||
|
||||
No `data` field should be sent by default.
|
||||
|
||||
All four responses use:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 调度器复位
|
||||
|
||||
Endpoint:
|
||||
|
||||
```text
|
||||
POST /api/lims/scheduler/reset
|
||||
```
|
||||
|
||||
Adjusted request:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "B10B5995",
|
||||
"requestTime": "2026-05-21T08:15:16.494Z"
|
||||
}
|
||||
```
|
||||
|
||||
Live response:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 1779351316072
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- OpenAPI says `data` is nullable int32.
|
||||
- Live Peptide accepted omitted `data`.
|
||||
|
||||
### 订单状态复位
|
||||
|
||||
Endpoint:
|
||||
|
||||
```text
|
||||
POST /api/lims/order/reset-order-status
|
||||
```
|
||||
|
||||
Adjusted request:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "B10B5995",
|
||||
"requestTime": "2026-05-21T08:13:34.750Z"
|
||||
}
|
||||
```
|
||||
|
||||
Live response:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 1779351214422
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- OpenAPI says `data` is nullable string.
|
||||
- Live Peptide accepted omitted `data`.
|
||||
- Do not model this as order-id scoped unless Bioyond confirms backend behavior.
|
||||
|
||||
### 库位复位
|
||||
|
||||
Endpoint:
|
||||
|
||||
```text
|
||||
POST /api/lims/storage/reset-location
|
||||
```
|
||||
|
||||
Adjusted request:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "B10B5995",
|
||||
"requestTime": "2026-05-21T08:15:18.924Z"
|
||||
}
|
||||
```
|
||||
|
||||
Live response:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 1779351318565
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- OpenAPI says `data` is nullable string.
|
||||
- Live Peptide accepted omitted `data`.
|
||||
- Do not model this as location-id scoped unless Bioyond confirms backend behavior.
|
||||
|
||||
### 仪器复位
|
||||
|
||||
Endpoint:
|
||||
|
||||
```text
|
||||
POST /api/lims/device/reset-devices
|
||||
```
|
||||
|
||||
Adjusted request:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "B10B5995",
|
||||
"requestTime": "date-time"
|
||||
}
|
||||
```
|
||||
|
||||
Expected response shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"message": "",
|
||||
"timestamp": 0
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- OpenAPI says `data` is nullable string.
|
||||
- Current `BioyondV1RPC.reset_devices()` already sends no `data`.
|
||||
- This endpoint was not live-probed in the no-data reset session.
|
||||
- Keep checkbox default unchecked.
|
||||
|
||||
## Tests To Add Before Implementation
|
||||
|
||||
1. `reset_auto` is not `NodeType.MANUAL_CONFIRM`.
|
||||
2. `reset_manual` has `node_type=NodeType.MANUAL_CONFIRM`.
|
||||
3. `reset_manual` metadata includes:
|
||||
- `always_free=True`
|
||||
- `placeholder_keys={"assignee_user_ids": "unilabos_manual_confirm"}`
|
||||
- `timeout_seconds=3600`
|
||||
- `assignee_user_ids=[]`
|
||||
- `physical_cleanup_confirmed=False`
|
||||
4. Both reset actions expose four real boolean params:
|
||||
- `reset_scheduler`
|
||||
- `reset_order_status`
|
||||
- `reset_location`
|
||||
- `reset_devices`
|
||||
5. The generated registry schema marks those reset params as JSON Schema `type: boolean`, not `object` or `string`, so the frontend can render checkboxes.
|
||||
6. `reset_auto` replaces the current public `reset` action. Unless a later compatibility request adds an alias, no old id-shaped public `reset` action remains.
|
||||
7. Goal defaults are:
|
||||
- first three reset checkboxes `True`
|
||||
- `reset_devices=False`
|
||||
8. `reset_manual(..., physical_cleanup_confirmed=False)` does not call any RPC reset method.
|
||||
9. `reset_auto()` with defaults calls:
|
||||
- `scheduler_reset()`
|
||||
- `reset_order_status()`
|
||||
- `reset_location()`
|
||||
- not `reset_devices()`
|
||||
10. `reset_auto(reset_devices=True)` also calls `reset_devices()`.
|
||||
11. `reset_order_status()` and `reset_location()` RPC wrappers send no `data` key.
|
||||
12. No reset path calls `take_out`.
|
||||
13. No reset path calls `refresh_material_cache`.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement `take_out` in reset.
|
||||
- Do not refresh `material_cache` from reset.
|
||||
- Do not resolve order ids or location ids for reset.
|
||||
- Do not add Project/cache/browser cleanup routes.
|
||||
- Do not make `reset_devices` default-on.
|
||||
- Do not execute this plan during planning.
|
||||
@@ -799,6 +799,23 @@ class BioyondPeptideStation(BioyondWorkstation):
|
||||
raise ValueError(f"未知 reset operation: {operation}")
|
||||
return result
|
||||
|
||||
@action(always_free=True, description="从 Bioyond 同步库存物料到本地资源树")
|
||||
def sync_from_external(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
del kwargs
|
||||
with self._debug_call_session("sync_from_external"):
|
||||
self._require_hardware_interface()
|
||||
if not getattr(self, "resource_synchronizer", None):
|
||||
raise RuntimeError("BioyondPeptideStation 未初始化 resource_synchronizer")
|
||||
|
||||
success = bool(self.resource_synchronizer.sync_from_external())
|
||||
resource_tree_update_requested = self._publish_resource_tree_update() if success else False
|
||||
return {
|
||||
"success": success,
|
||||
"action": "sync_from_external",
|
||||
"resource_tree_update_requested": resource_tree_update_requested,
|
||||
"message": "Bioyond 资源同步成功" if success else "Bioyond 资源同步失败或无库存数据",
|
||||
}
|
||||
|
||||
@action(always_free=True, description="启动 Bioyond 调度器")
|
||||
def scheduler_start(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
del kwargs
|
||||
@@ -1354,6 +1371,43 @@ class BioyondPeptideStation(BioyondWorkstation):
|
||||
|
||||
# ---------- 基础设施 ----------
|
||||
|
||||
def _publish_resource_tree_update(self) -> bool:
|
||||
"""触发当前 deck 的资源树更新,让前端看到最新同步结果。"""
|
||||
ros_node = getattr(self, "_ros_node", None)
|
||||
if ros_node is None:
|
||||
logger.warning("资源树更新跳过: 未绑定 _ros_node")
|
||||
return False
|
||||
|
||||
deck = getattr(self, "deck", None)
|
||||
if deck is None:
|
||||
logger.warning("资源树更新跳过: 未绑定 deck")
|
||||
return False
|
||||
|
||||
update_resource = getattr(ros_node, "update_resource", None)
|
||||
if update_resource is None:
|
||||
logger.warning("资源树更新跳过: _ros_node 缺少 update_resource")
|
||||
return False
|
||||
|
||||
try:
|
||||
try:
|
||||
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode # type: ignore
|
||||
except Exception: # pragma: no cover
|
||||
ROS2DeviceNode = None # type: ignore[assignment]
|
||||
|
||||
if ROS2DeviceNode is not None and hasattr(ROS2DeviceNode, "run_async_func"):
|
||||
ROS2DeviceNode.run_async_func(update_resource, True, **{"resources": [deck]})
|
||||
else:
|
||||
update_resource(resources=[deck])
|
||||
|
||||
logger.info("已调度多肽 deck '%s' 的资源树更新", getattr(deck, "name", ""))
|
||||
return True
|
||||
except TypeError as exc:
|
||||
logger.error("资源树更新失败: update_resource 调用签名错误: %s", exc)
|
||||
raise
|
||||
except Exception as exc:
|
||||
logger.warning("资源树更新失败: %s", exc)
|
||||
return False
|
||||
|
||||
def _run_scheduler_action(self, method_name: str, label: str) -> Dict[str, Any]:
|
||||
rpc = self._require_hardware_interface()
|
||||
method = getattr(rpc, method_name, None)
|
||||
|
||||
@@ -56,13 +56,17 @@ class ConnectionMonitor:
|
||||
def _monitor_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
# 使用 lightweight API 检查连接
|
||||
# query_matial_type_list 是比较快的查询
|
||||
start_time = time.time()
|
||||
result = self.workstation.hardware_interface.material_type_list()
|
||||
# 使用轻量级调度状态接口检查连接,避免启动时打印完整物料类型列表。
|
||||
result = self.workstation.hardware_interface.scheduler_status()
|
||||
|
||||
status = "online" if result else "offline"
|
||||
msg = "Connection established" if status == "online" else "Failed to get material type list"
|
||||
if status == "online":
|
||||
msg = (
|
||||
f"Scheduler status={result.get('status')}, "
|
||||
f"hasTask={result.get('hasTask')}"
|
||||
)
|
||||
else:
|
||||
msg = "Failed to get scheduler status"
|
||||
|
||||
if status != self._last_status:
|
||||
logger.info(f"Bioyond连接状态变更: {self._last_status} -> {status}")
|
||||
|
||||
Reference in New Issue
Block a user