diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index 10c3b66a..2dfa8a83 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -1000,6 +1000,183 @@ class BioyondCellWorkstation(BioyondWorkstation): return final_result + def create_orders_formulation( + self, + formulation: List[Dict[str, Any]], + batch_id: str = "", + bottle_type: str = "配液小瓶", + mix_time: int = 0, + load_shedding_info: float = 0.0, + pouch_cell_info: float = 0.0, + conductivity_info: float = 0.0, + conductivity_bottle_count: int = 0, + ) -> Dict[str, Any]: + """ + 配方批量输入版本的 create_orders —— 等价于 create_orders, + 但参数来源于前端 FormulationBatchWidget,而非 Excel 文件。 + + Args: + formulation: 配方列表,每个元素代表一个订单(一瓶),格式: + [ + { + "order_name": "配方A", # 可选,配方名称 + "materials": [ # 物料列表 + {"name": "LiPF6", "mass": 12.5}, + {"name": "EC", "mass": 50.0}, + ] + }, + ... + ] + batch_id: 批次ID,若为空则用当前时间戳 + bottle_type: 配液瓶类型,默认 "配液小瓶" + mix_time: 混匀时间(秒) + load_shedding_info: 扣电组装分液体积 + pouch_cell_info: 软包组装分液体积 + conductivity_info: 电导测试分液体积 + conductivity_bottle_count: 电导测试分液瓶数 + + Returns: + 与 create_orders 返回格式一致的结果字典 + """ + if not formulation: + raise ValueError("formulation 参数不能为空") + + if not batch_id: + batch_id = f"formulation_{datetime.now().strftime('%Y%m%d%H%M%S')}" + + create_time = f"{datetime.now().year}/{datetime.now().month}/{datetime.now().day}" + + # 将 formulation 转换为 LIMS orders 格式(与 create_orders 中的格式一致) + orders: List[Dict[str, Any]] = [] + for idx, item in enumerate(formulation): + materials = item.get("materials", []) + order_name = item.get("order_name", f"{batch_id}_order_{idx + 1}") + + mats: List[Dict[str, Any]] = [] + total_mass = 0.0 + for mat in materials: + name = mat.get("name", "") + mass = float(mat.get("mass", 0.0)) + if name and mass > 0: + mats.append({"name": name, "mass": mass}) + total_mass += mass + + if not mats: + logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过") + continue + + orders.append({ + "batchId": batch_id, + "orderName": order_name, + "createTime": create_time, + "bottleType": bottle_type, + "mixTime": mix_time, + "loadSheddingInfo": load_shedding_info, + "pouchCellInfo": pouch_cell_info, + "conductivityInfo": conductivity_info, + "conductivityBottleCount": conductivity_bottle_count, + "materialInfos": mats, + "totalMass": round(total_mass, 4), + }) + + if not orders: + logger.error("[create_orders_formulation] 没有有效的订单可提交") + return {"status": "error", "message": "没有有效配方数据"} + + logger.info(f"[create_orders_formulation] 即将提交 {len(orders)} 个订单 (batchId={batch_id})") + + # ========== 提交订单到 LIMS ========== + response = self._post_lims("/api/lims/order/orders", orders) + logger.info(f"[create_orders_formulation] 接口返回: {response}") + + data_list = response.get("data", []) + if not data_list: + logger.error("创建订单未返回有效数据!") + return response + + order_codes = [item.get("orderCode") for item in data_list if item.get("orderCode")] + if not order_codes: + logger.error("未找到任何有效的 orderCode!") + return response + + logger.info(f"[create_orders_formulation] 等待 {len(order_codes)} 个订单完成: {order_codes}") + + # ========== 等待所有订单完成 ========== + all_reports = [] + for idx, order_code in enumerate(order_codes, 1): + logger.info(f"[create_orders_formulation] 等待第 {idx}/{len(order_codes)} 个订单: {order_code}") + result = self.wait_for_order_finish(order_code) + if result.get("status") == "success": + all_reports.append(result.get("report", {})) + logger.info(f"[create_orders_formulation] ✓ 订单 {order_code} 完成") + else: + logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}") + all_reports.append({ + "orderCode": order_code, + "status": result.get("status"), + "error": result.get("message", "未知错误"), + }) + + # ========== 计算质量比 ========== + all_mass_ratios = [] + for idx, report in enumerate(all_reports, 1): + order_code = report.get("orderCode", "N/A") + if "error" not in report: + try: + mass_ratios = self._process_order_reagents(report) + all_mass_ratios.append({ + "orderCode": order_code, + "orderName": report.get("orderName", "N/A"), + "real_mass_ratio": mass_ratios.get("real_mass_ratio", {}), + "target_mass_ratio": mass_ratios.get("target_mass_ratio", {}), + }) + except Exception as e: + logger.error(f"计算订单 {order_code} 质量比失败: {e}") + all_mass_ratios.append({ + "orderCode": order_code, + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": str(e), + }) + else: + all_mass_ratios.append({ + "orderCode": order_code, + "real_mass_ratio": {}, + "target_mass_ratio": {}, + "error": "订单未成功完成", + }) + + # ========== 提取分液瓶板 + 创建资源 ========== + all_vial_plates = [] + processed_material_ids = set() + for report in all_reports: + vial_plate_info = self._extract_vial_plate_from_report(report) + if vial_plate_info: + material_id = vial_plate_info.get("materialId") + all_vial_plates.append(vial_plate_info) + if material_id in processed_material_ids: + continue + try: + self._create_vial_plate_resource(vial_plate_info) + processed_material_ids.add(material_id) + except Exception as e: + logger.error(f"[资源树] 创建失败: {e}") + + logger.info( + f"[create_orders_formulation] 完成: " + f"{len(all_reports)} 个订单, {len(all_vial_plates)} 个分液瓶板" + ) + + return { + "status": "all_completed", + "total_orders": len(order_codes), + "bottle_count": len(order_codes), + "reports": all_reports, + "mass_ratios": all_mass_ratios, + "vial_plates": all_vial_plates, + "original_response": response, + } + def _extract_vial_plate_from_report(self, report: Dict) -> Optional[Dict]: """ 从 order_finish 报文中提取分液瓶板信息 diff --git a/unilabos/registry/devices/bioyond_cell.yaml b/unilabos/registry/devices/bioyond_cell.yaml index 1196868e..aa6abd96 100644 --- a/unilabos/registry/devices/bioyond_cell.yaml +++ b/unilabos/registry/devices/bioyond_cell.yaml @@ -188,6 +188,108 @@ bioyond_cell: title: create_orders参数 type: object type: UniLabJsonCommand + auto-create_orders_formulation: + always_free: true + feedback: {} + goal: {} + goal_default: + batch_id: '' + bottle_type: 配液小瓶 + conductivity_bottle_count: 0 + conductivity_info: 0.0 + formulation: null + load_shedding_info: 0.0 + mix_time: 0 + pouch_cell_info: 0.0 + handles: + output: + - data_key: total_orders + data_source: executor + data_type: integer + handler_key: bottle_count + label: 配液瓶数 + - data_key: vial_plates + data_source: executor + data_type: array + handler_key: vial_plates_output + label: 分液瓶板列表 + - data_key: mass_ratios + data_source: executor + data_type: array + handler_key: mass_ratios_output + label: 配方信息列表 + placeholder_keys: + formulation: unilabos_formulation + result: {} + schema: + description: 配方批量输入版本的创建实验——通过前端配方组件输入物料配比,替代Excel导入 + properties: + feedback: {} + goal: + properties: + batch_id: + default: '' + description: 批次ID,为空则自动生成时间戳 + type: string + bottle_type: + default: 配液小瓶 + description: 配液瓶类型 + type: string + conductivity_bottle_count: + default: 0 + description: 电导测试分液瓶数 + type: integer + conductivity_info: + default: 0.0 + description: 电导测试分液体积 + type: number + formulation: + description: 配方列表,每个元素代表一个订单(一瓶) + items: + properties: + materials: + description: 物料列表 + items: + properties: + mass: + description: 质量(g) + type: number + name: + description: 物料名称 + type: string + required: + - name + - mass + type: object + type: array + order_name: + description: 配方名称(可选) + type: string + required: + - materials + type: object + type: array + load_shedding_info: + default: 0.0 + description: 扣电组装分液体积 + type: number + mix_time: + default: 0 + description: 混匀时间(秒) + type: integer + pouch_cell_info: + default: 0.0 + description: 软包组装分液体积 + type: number + required: + - formulation + type: object + result: {} + required: + - goal + title: create_orders_formulation参数 + type: object + type: UniLabJsonCommand auto-create_sample: feedback: {} goal: {} diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 2a277664..d6b9ea07 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -632,6 +632,11 @@ class Registry: # 保留字段的 title(用户自定义的中文名) if "title" in prev_field and prev_field["title"]: field_schema["title"] = prev_field["title"] + # 保留旧 schema 中手动定义的复杂嵌套结构(如 items、properties、required) + # 当旧 schema 比自动生成的更丰富时,使用旧 schema 的结构 + for rich_key in ("items", "properties", "required"): + if rich_key in prev_field and rich_key not in field_schema: + field_schema[rich_key] = prev_field[rich_key] def _is_typed_dict(self, annotation: Any) -> bool: """ @@ -818,20 +823,26 @@ class Registry: "goal_default": {i["name"]: i["default"] for i in v["args"]}, "handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []), "placeholder_keys": { - i["name"]: ( - "unilabos_resources" - if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" - or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") - else "unilabos_devices" - ) - for i in v["args"] - if i.get("type", "") - in [ - "unilabos.registry.placeholder_type:ResourceSlot", - "unilabos.registry.placeholder_type:DeviceSlot", - ("list", "unilabos.registry.placeholder_type:ResourceSlot"), - ("list", "unilabos.registry.placeholder_type:DeviceSlot"), - ] + # 先用旧配置中手动定义的 placeholder_keys 作为基础 + **old_action_configs.get(f"auto-{k}", {}).get("placeholder_keys", {}), + # 再用自动推断的覆盖(ResourceSlot/DeviceSlot 类型) + **{ + i["name"]: ( + "unilabos_resources" + if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" + or i["type"] + == ("list", "unilabos.registry.placeholder_type:ResourceSlot") + else "unilabos_devices" + ) + for i in v["args"] + if i.get("type", "") + in [ + "unilabos.registry.placeholder_type:ResourceSlot", + "unilabos.registry.placeholder_type:DeviceSlot", + ("list", "unilabos.registry.placeholder_type:ResourceSlot"), + ("list", "unilabos.registry.placeholder_type:DeviceSlot"), + ] + }, }, **({"always_free": True} if v.get("always_free") else {}), } diff --git a/unilabos/test/experiments/yibin_electrolyte_config_example.json b/unilabos/test/experiments/yibin_electrolyte_config_example.json index d5efc357..ba25c0ac 100644 --- a/unilabos/test/experiments/yibin_electrolyte_config_example.json +++ b/unilabos/test/experiments/yibin_electrolyte_config_example.json @@ -13,7 +13,7 @@ "deck": { "data": { "_resource_child_name": "YB_Bioyond_Deck", - "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck" + "_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck" } }, "protocol_type": [], @@ -103,15 +103,14 @@ "children": [], "parent": "bioyond_cell_workstation", "type": "deck", - "class": "BIOYOND_YB_Deck", + "class": "BioyondElectrolyteDeck", "position": { "x": 0, "y": 0, "z": 0 }, "config": { - "type": "BIOYOND_YB_Deck", - "setup": true, + "type": "BioyondElectrolyteDeck", "rotation": { "x": 0, "y": 0,