feat: update coin cell assembly, bioyond cell workstation, and resource configs

This commit is contained in:
Andy6M
2026-03-25 23:31:06 +08:00
parent 91928a87ac
commit 467f0b1115
13 changed files with 290 additions and 49 deletions

168
CHANGES_2026_03_24.md Normal file
View File

@@ -0,0 +1,168 @@
# 变更说明 2026-03-24
## 问题背景
`BioyondElectrolyteDeck`(原 `BIOYOND_YB_Deck`)迁移后,前端物料未能正常上传/同步。
---
## 修复内容
### 1. `unilabos/resources/bioyond/decks.py`
- 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑,与旧版 `BIOYOND_YB_Deck` 保持一致
- 工厂函数 `bioyond_electrolyte_deck` 保留显式调用 `deck.setup()`,避免重复初始化
```python
# 修复前(缺少 setup 参数,无法通过 setup=True 触发初始化)
def __init__(self, name, size_x, size_y, size_z, category):
super().__init__(...)
# 修复后
def __init__(self, name, size_x, size_y, size_z, category, setup: bool = False):
super().__init__(...)
if setup:
self.setup()
```
---
### 2. `unilabos/resources/graphio.py`
- 修复 `resource_bioyond_to_plr` 中两处 `bottle.tracker.liquids` 直接赋值导致的崩溃
- `ResourceHolder`(如枪头盒的 TipSpot 槽位)没有 `tracker` 属性,直接访问会抛出 `AttributeError`,阻断整个 Bioyond 同步流程
```python
# 修复前
bottle.tracker.liquids = [...]
# 修复后
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [...]
```
---
### 3. `unilabos/app/main.py`
- 保留 `file_path is not None` 条件不变(已还原),并补充注释说明原因
- 该逻辑只在**本地文件模式**下有意义:本地 graph 文件只含设备结构远端有已保存物料merge 才能将两者合并
- 远端模式(`file_path=None`)下,`resource_tree_set``request_startup_json` 来自同一份数据merge 为空操作,条件是否加 `file_path is not None` 对结果没有影响
---
### 4. `unilabos/devices/workstation/bioyond_studio/station.py` ⭐ 核心修复
- 当 deck 通过反序列化创建时,不会自动调用 `setup()`,导致 `deck.children` 为空,`warehouses` 始终是 `{}`
- 增加兜底逻辑:仓库扫描后仍为空,则主动调用 `deck.setup()` 初始化仓库
- 这是导致所有物料放置失败(`warehouse '...' 在deck中不存在。可用warehouses: []`)的根本原因
```python
# 新增兜底
if not self.deck.warehouses and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
self.deck.setup()
```
---
---
## 补充修复 2026-03-25依华扣电组装工站子物料未上传
### 问题
`CoinCellAssemblyWorkstation.post_init` 直接上传空 deck未调用 `deck.setup()`,导致:
- 前端子物料(成品弹夹、料盘、瓶架等)不显示
- 运行时 `self.deck.get_resource("成品弹夹")` 抛出 `ResourceNotFoundError`
### 修复文件
**`unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`**
- `YihuaCoinCellDeck.__init__` 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑
**`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`**
- `post_init` 中增加与 Bioyond 工站相同的兜底逻辑deck 无子节点时调用 `deck.setup()` 初始化
```python
# post_init 中新增
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
self.deck.setup()
```
### 联动 Bug`MaterialPlate.create_with_holes` 构造顺序错误
**现象**`deck.setup()` 被调用后,启动时抛出:
```
设备后初始化失败: Must specify either `ordered_items` or `ordering`.
```
**根因**`create_with_holes` 原来的逻辑是先构造空的 `MaterialPlate` 实例,再 assign 洞位:
```python
# 旧错误cls(...) 时 ordered_items=None → ItemizedResource.__init__ 立即报错
plate = cls(name=name, ...) # ← 这里就崩了
holes = create_ordered_items_2d(...) # ← 根本没走到这里
for hole_name, hole in holes.items():
plate.assign_child_resource(...)
```
pylabrobot 的 `ItemizedResource.__init__` 强制要求 `ordered_items``ordering` 必须有一个不为 `None`,空构造直接失败。
**修复**:先建洞位,再作为 `ordered_items` 传给构造函数:
```python
# 新(正确):先建洞位,再一次性传入构造函数
holes = create_ordered_items_2d(klass=MaterialHole, num_items_x=4, ...)
return cls(name=name, ..., ordered_items=holes)
```
> 此 bug 此前未被触发,是因为 `deck.setup()` 从未被调用到——正是上面 `post_init` 兜底修复引出的联动问题。
---
## 补充修复 2026-03-253→2→1 转运资源同步失败
### 问题
配液工站Bioyond完成分液后调用 `transfer_3_to_2_to_1_auto` 将分液瓶板转运到扣电工站BatteryStation。物理 LIMS 转运成功,但数字孪生资源树同步始终失败:
```
[资源同步] ❌ 失败: 目标设备 'BatteryStation' 中未找到资源 'bottle_rack_6x2'
```
### 根因
`_get_resource_from_device` 方法负责跨设备查找资源对象,有两个问题:
1. **原始路径完全失效**:尝试 `from unilabos.app.ros2_app import get_device_plr_resource_by_name`,但该模块不存在,`ImportError``except Exception: pass` 静默吞掉
2. **降级路径搜错地方**:遍历 `self._plr_resources`Bioyond 自己的资源),不可能找到 BatteryStation 的 `bottle_rack_6x2`
### 修复文件
**`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`**
改用全局设备注册表 `registered_devices` 跨设备访问目标 deck
```python
# 修复前(失效)
from unilabos.app.ros2_app import get_device_plr_resource_by_name # 模块不存在
return get_device_plr_resource_by_name(device_id, resource_name)
# 修复后
from unilabos.ros.nodes.base_device_node import registered_devices
device_info = registered_devices.get(device_id)
if device_info is not None:
driver = device_info.get("driver_instance") # TypedDict 是 dict必须用 .get()
if driver is not None:
deck = getattr(driver, "deck", None)
if deck is not None:
res = deck.get_resource(resource_name)
```
关键细节:`DeviceInfoType``TypedDict`(即普通 `dict`),必须用 `device_info.get("driver_instance")` 而非 `getattr(device_info, "driver_instance", None)`——后者对字典永远返回 `None`
---
## 根本原因分析
旧版以**本地文件模式**启动(有 `graph` 文件deck 在启动前已通过 `merge_remote_resources` 获得仓库子节点,反序列化时能正确恢复 warehouses。
新版以**远端模式**启动(`file_path=None`deck 反序列化时没有仓库子节点,`station.py` 扫描为空,所有物料的 warehouse 匹配失败Bioyond 同步的 16 个资源全部无法放置到对应仓库位,前端不显示。

View File

@@ -621,6 +621,8 @@ def main():
continue
# 如果从远端获取了物料信息,则与本地物料进行同步
# 仅在本地文件模式下有意义:本地文件只含设备结构,远端有已保存的物料,需要 merge
# 远端模式下 resource_tree_set 与 request_startup_json 来自同一份数据merge 为空操作
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
print_status("开始同步远端物料到本地...", "info")
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])

View File

@@ -979,10 +979,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
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,
mix_time: List[int] = [],
coin_cell_volume: float = 0.0,
pouch_cell_volume: float = 0.0,
conductivity_volume: float = 0.0,
conductivity_bottle_count: int = 0,
) -> Dict[str, Any]:
"""
@@ -1003,10 +1003,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
]
batch_id: 批次ID若为空则用当前时间戳
bottle_type: 配液瓶类型,默认 "配液小瓶"
mix_time: 混匀时间(秒)
load_shedding_info: 扣电组装分液体积
pouch_cell_info: 软包组装分液体积
conductivity_info: 电导测试分液体积
mix_time: 混匀时间列表(秒),与 formulation 一一对应,不足则补 0
coin_cell_volume: 扣电组装分液体积
pouch_cell_volume: 软包电池注液组装分液体积
conductivity_volume: 电导测试分液体积
conductivity_bottle_count: 电导测试分液瓶数
Returns:
@@ -1039,9 +1039,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过")
continue
item_mix_time = mix_time[idx] if idx < len(mix_time) else 0
logger.info(f"[create_orders_formulation] 第 {idx + 1} 个配方: orderName={order_name}, "
f"loadShedding={load_shedding_info}, pouchCell={pouch_cell_info}, "
f"conductivity={conductivity_info}, totalMass={total_mass}, "
f"coinCellVolume={coin_cell_volume}, pouchCellVolume={pouch_cell_volume}, "
f"conductivityVolume={conductivity_volume}, totalMass={total_mass}, "
f"material_count={len(mats)}")
orders.append({
@@ -1049,10 +1050,10 @@ class BioyondCellWorkstation(BioyondWorkstation):
"orderName": order_name,
"createTime": create_time,
"bottleType": bottle_type,
"mixTime": mix_time,
"loadSheddingInfo": load_shedding_info,
"pouchCellInfo": pouch_cell_info,
"conductivityInfo": conductivity_info,
"mixTime": item_mix_time,
"loadSheddingInfo": coin_cell_volume,
"pouchCellInfo": pouch_cell_volume,
"conductivityInfo": conductivity_volume,
"conductivityBottleCount": conductivity_bottle_count,
"materialInfos": mats,
"totalMass": round(total_mass, 4),
@@ -1650,18 +1651,31 @@ class BioyondCellWorkstation(BioyondWorkstation):
Args:
device_id: 目标设备 ID"BatteryStation"
resource_name: 资源名称(如 "electrolyte_buffer"
resource_name: 资源名称(如 "bottle_rack_6x2"
Returns:
找到的 PLR Resource 对象,未找到则返回 None
"""
# 优先:通过全局设备注册表直接访问目标设备的 deck
# DeviceInfoType 是 TypedDict即普通 dict必须用 dict.get() 而非 getattr()
try:
from unilabos.app.ros2_app import get_device_plr_resource_by_name
return get_device_plr_resource_by_name(device_id, resource_name)
from unilabos.ros.nodes.base_device_node import registered_devices
device_info = registered_devices.get(device_id)
if device_info is not None:
driver = device_info.get("driver_instance")
if driver is not None:
deck = getattr(driver, "deck", None)
if deck is not None and hasattr(deck, "get_resource"):
try:
res = deck.get_resource(resource_name)
if res is not None:
return res
except Exception:
pass
except Exception:
pass
# 降级:遍历 workstation 已注册的 plr_resources 列表
# 降级:遍历 workstation 已注册的 plr_resources 列表(仅当前设备)
try:
for res in getattr(self, "_plr_resources", []):
if res.name == resource_name:

View File

@@ -760,10 +760,9 @@ class BioyondWorkstation(WorkstationBase):
except:
pass
# 创建通信模块
# 创建通信模块;同步器将在 post_init 中初始化并执行首次同步
self._create_communication_module(bioyond_config)
self.resource_synchronizer = BioyondResourceSynchronizer(self)
self.resource_synchronizer.sync_from_external()
self.resource_synchronizer = None
# TODO: self._ros_node里面拿属性
@@ -802,6 +801,15 @@ class BioyondWorkstation(WorkstationBase):
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化仓库
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
self.deck.setup()
# 初始化同步器并执行首次同步(需在仓库初始化之后)
self.resource_synchronizer = BioyondResourceSynchronizer(self)
self.resource_synchronizer.sync_from_external()
# 启动连接监控
try:
self.connection_monitor = ConnectionMonitor(self)

View File

@@ -169,23 +169,28 @@ class MaterialPlate(ItemizedResource[MaterialHole]):
model: Optional[str] = None,
) -> "MaterialPlate":
"""工厂方法:创建带 4x4 洞位的料板(仅用于初始 setup不在反序列化路径调用"""
plate = cls(name=name, size_x=size_x, size_y=size_y, size_z=size_z, category=category, model=model)
# 默认洞位间距(与 _unilabos_state 默认值保持一致)
hole_spacing_x = 24.0
hole_spacing_y = 24.0
# 先建洞位,再作为 ordered_items 传入构造函数
# ItemizedResource.__init__ 要求 ordered_items 或 ordering 二选一必须有值)
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(size_x - 4 * plate._unilabos_state["hole_spacing_x"]) / 2,
dy=(size_y - 4 * plate._unilabos_state["hole_spacing_y"]) / 2,
dx=(size_x - 4 * hole_spacing_x) / 2,
dy=(size_y - 4 * hole_spacing_y) / 2,
dz=size_z,
item_dx=plate._unilabos_state["hole_spacing_x"],
item_dy=plate._unilabos_state["hole_spacing_y"],
item_dx=hole_spacing_x,
item_dy=hole_spacing_y,
size_x=16,
size_y=16,
size_z=16,
)
for hole_name, hole in holes.items():
plate.assign_child_resource(hole, location=hole.location)
return plate
return cls(
name=name, size_x=size_x, size_y=size_y, size_z=size_z,
ordered_items=holes, category=category, model=model,
)
def update_locations(self):
# TODO:调多次相加
@@ -542,6 +547,7 @@ class YihuaCoinCellDeck(Deck):
size_z: float = 100.0,
origin: Coordinate = Coordinate(-2200, 0, 0),
category: str = "coin_cell_deck",
setup: bool = False,
):
super().__init__(
name=name,
@@ -550,6 +556,8 @@ class YihuaCoinCellDeck(Deck):
size_z=100.0,
origin=origin,
)
if setup:
self.setup()
def setup(self) -> None:
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置"""

View File

@@ -193,7 +193,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node
#self.deck = create_a_coin_cell_deck()
# Deck 为空时(反序列化未恢复子节点),主动调用 setup() 初始化子物料
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
self.deck.setup()
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})

View File

@@ -0,0 +1,29 @@
Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio
20260325_132011,0.0,12.119999885559082,405.0,3189,20,7,test0008,13163721,,
20260325_132301,0.0,12.079999923706055,153.0,3172,20,7,test0008,13200631,,
20260325_132516,0.0,12.119999885559082,153.0,3205,20,7,test0008,13224031,,
20260325_132758,0.0,12.309999465942383,161.0,3221,20,7,test0008,13251351,,
20260325_133215,0.0,12.520000457763672,257.0,3318,20,7,NoRead88,13293861,,
20260325_133820,0.0,12.15999984741211,363.0,3269,20,7,NoRead88,13321291,,
20260325_134049,0.0,12.100000381469727,149.0,3383,20,7,NoRead88,13381641,,
20260325_134327,0.0,12.369999885559082,157.0,3237,20,7,NoRead88,13404651,,
20260325_160512,0.0,12.299999237060547,238.0,3577,20,7,NoRead88,16022161,,
20260325_160734,0.0,12.40000057220459,155.0,3464,20,7,NoRead88,16045481,,
20260325_161010,0.0,12.269999504089355,155.0,3609,20,7,NoRead88,60731181,,
20260325_161252,0.0,12.579999923706055,162.0,3496,20,7,NoRead88,16100671,,
20260325_161636,0.0,12.619999885559082,223.0,3399,20,7,NoRead88,16135951,,
20260325_161909,0.0,12.039999961853027,153.0,3302,20,7,NoRead88,16163351,,
20260325_162145,0.0,12.00999927520752,155.0,3350,20,7,NoRead88,16190731,,
20260325_162429,0.0,12.329998970031738,163.0,3561,20,7,NoRead88,16214361,,
20260325_162841,0.0,12.579999923706055,251.0,3593,20,7,NoRead88,16260311,,
20260325_163118,0.0,12.25999927520752,156.0,3545,20,7,NoRead88,16283921,,
20260325_163356,0.0,12.220000267028809,157.0,3464,20,7,NoRead88,16311611,,
20260325_163641,0.0,12.199999809265137,165.0,3674,20,7,NoRead88,16335401,,
20260325_164046,0.0,12.25,244.0,3512,20,7,NoRead88,16380881,,
20260325_164321,0.0,12.079999923706055,154.0,3609,20,7,NoRead88,16404401,,
20260325_164556,0.0,12.029999732971191,155.0,3593,20,7,NoRead88,16431851,,
20260325_164840,0.0,12.100000381469727,163.0,3496,20,7,NoRead88,16455451,,
20260325_172206,0.0,12.00999927520752,245.0,3011,20,7,NoRead88,17193041,BSO2026032500006,"{""EMC"": 0.949, ""LiFSI"": 0.051}"
20260325_172608,0.0,12.0,242.0,3253,20,7,NoRead88,17233491,BSO2026032500007,"{""EMC"": 0.9582, ""LiFSI"": 0.0418}"
20260325_183415,0.0,12.690000534057617,1226.0,3528,20,7,NoRead88,18150131,BSO2026032500011,"{""EMC"": 0.949, ""LiFSI"": 0.051}"
20260325_190044,0.0,12.130000114440918,1586.0,3528,20,7,NoRead88,18355771,BSO2026032500012,"{""EMC"": 0.9582, ""LiFSI"": 0.0418}"
1 Time open_circuit_voltage pole_weight assembly_time assembly_pressure electrolyte_volume coin_num electrolyte_code coin_cell_code formulation_order_code formulation_ratio
2 20260325_132011 0.0 12.119999885559082 405.0 3189 20 7 test0008 13163721
3 20260325_132301 0.0 12.079999923706055 153.0 3172 20 7 test0008 13200631
4 20260325_132516 0.0 12.119999885559082 153.0 3205 20 7 test0008 13224031
5 20260325_132758 0.0 12.309999465942383 161.0 3221 20 7 test0008 13251351
6 20260325_133215 0.0 12.520000457763672 257.0 3318 20 7 NoRead88 13293861
7 20260325_133820 0.0 12.15999984741211 363.0 3269 20 7 NoRead88 13321291
8 20260325_134049 0.0 12.100000381469727 149.0 3383 20 7 NoRead88 13381641
9 20260325_134327 0.0 12.369999885559082 157.0 3237 20 7 NoRead88 13404651
10 20260325_160512 0.0 12.299999237060547 238.0 3577 20 7 NoRead88 16022161
11 20260325_160734 0.0 12.40000057220459 155.0 3464 20 7 NoRead88 16045481
12 20260325_161010 0.0 12.269999504089355 155.0 3609 20 7 NoRead88 60731181
13 20260325_161252 0.0 12.579999923706055 162.0 3496 20 7 NoRead88 16100671
14 20260325_161636 0.0 12.619999885559082 223.0 3399 20 7 NoRead88 16135951
15 20260325_161909 0.0 12.039999961853027 153.0 3302 20 7 NoRead88 16163351
16 20260325_162145 0.0 12.00999927520752 155.0 3350 20 7 NoRead88 16190731
17 20260325_162429 0.0 12.329998970031738 163.0 3561 20 7 NoRead88 16214361
18 20260325_162841 0.0 12.579999923706055 251.0 3593 20 7 NoRead88 16260311
19 20260325_163118 0.0 12.25999927520752 156.0 3545 20 7 NoRead88 16283921
20 20260325_163356 0.0 12.220000267028809 157.0 3464 20 7 NoRead88 16311611
21 20260325_163641 0.0 12.199999809265137 165.0 3674 20 7 NoRead88 16335401
22 20260325_164046 0.0 12.25 244.0 3512 20 7 NoRead88 16380881
23 20260325_164321 0.0 12.079999923706055 154.0 3609 20 7 NoRead88 16404401
24 20260325_164556 0.0 12.029999732971191 155.0 3593 20 7 NoRead88 16431851
25 20260325_164840 0.0 12.100000381469727 163.0 3496 20 7 NoRead88 16455451
26 20260325_172206 0.0 12.00999927520752 245.0 3011 20 7 NoRead88 17193041 BSO2026032500006 {"EMC": 0.949, "LiFSI": 0.051}
27 20260325_172608 0.0 12.0 242.0 3253 20 7 NoRead88 17233491 BSO2026032500007 {"EMC": 0.9582, "LiFSI": 0.0418}
28 20260325_183415 0.0 12.690000534057617 1226.0 3528 20 7 NoRead88 18150131 BSO2026032500011 {"EMC": 0.949, "LiFSI": 0.051}
29 20260325_190044 0.0 12.130000114440918 1586.0 3528 20 7 NoRead88 18355771 BSO2026032500012 {"EMC": 0.9582, "LiFSI": 0.0418}

View File

@@ -196,11 +196,11 @@ bioyond_cell:
batch_id: ''
bottle_type: 配液小瓶
conductivity_bottle_count: 0
conductivity_info: 0.0
conductivity_volume: 0.0
formulation: null
load_shedding_info: 0.0
mix_time: 0
pouch_cell_info: 0.0
coin_cell_volume: 0.0
mix_time: []
pouch_cell_volume: 0.0
handles:
output:
- data_key: total_orders
@@ -239,9 +239,9 @@ bioyond_cell:
default: 0
description: 电导测试分液瓶数
type: integer
conductivity_info:
conductivity_volume:
default: 0.0
description: 电导测试分液体积
description: 电导测试分液体积
type: number
formulation:
description: 配方列表,每个元素代表一个订单(一瓶)
@@ -269,17 +269,19 @@ bioyond_cell:
- materials
type: object
type: array
load_shedding_info:
coin_cell_volume:
default: 0.0
description: 扣电组装分液体积
description: 扣电组装分液体积
type: number
mix_time:
default: 0
description: 混匀时间(秒)
type: integer
pouch_cell_info:
default: []
description: 混匀时间列表(秒),与 formulation 一一对应
items:
type: integer
type: array
pouch_cell_volume:
default: 0.0
description: 软包组装分液体积
description: 软包电池注液组装分液体积
type: number
required:
- formulation

View File

@@ -104,8 +104,11 @@ class BioyondElectrolyteDeck(Deck):
size_y: float = 1400.0,
size_z: float = 2670.0,
category: str = "deck",
setup: bool = False,
) -> None:
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
if setup:
self.setup()
def setup(self) -> None:
# 添加仓库

View File

@@ -797,9 +797,10 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
bottle = plr_material[number] = initialize_resource(
{"name": f'{detail["name"]}_{number}', "class": reverse_type_mapping[typeName][0]}, resource_type=ResourcePLR
)
bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
]
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
]
bottle.code = detail.get("code", "")
logger.debug(f" └─ [子物料] {detail['name']}{plr_material.name}[{number}] (类型:{typeName})")
else:
@@ -808,9 +809,10 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
# 只对有 capacity 属性的容器(液体容器)处理液体追踪
if hasattr(plr_material, 'capacity'):
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
bottle.tracker.liquids = [
(material["name"], float(material.get("quantity", 0)) if material.get("quantity") else 0)
]
plr_materials.append(plr_material)