mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
支持选择器注册表自动生成
支持转运物料
This commit is contained in:
@@ -345,111 +345,6 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
res.response = ""
|
||||
return res
|
||||
|
||||
async def s2c_resource_tree(req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
"""
|
||||
处理资源树更新请求
|
||||
|
||||
支持三种操作:
|
||||
- add: 添加新资源到资源树
|
||||
- update: 更新现有资源
|
||||
- remove: 从资源树中移除资源
|
||||
"""
|
||||
try:
|
||||
data = json.loads(req.command)
|
||||
results = []
|
||||
|
||||
for i in data:
|
||||
action = i.get("action") # remove, add, update
|
||||
resources_uuid: List[str] = i.get("data") # 资源数据
|
||||
self.lab_logger().info(
|
||||
f"[Resource Tree Update] Processing {action} operation, "
|
||||
f"resources count: {len(resources_uuid)}"
|
||||
)
|
||||
tree_set = None
|
||||
if action in ["add", "update"]:
|
||||
response: SerialCommand.Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"
|
||||
].call_async(
|
||||
SerialCommand.Request(
|
||||
command=json.dumps(
|
||||
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
|
||||
)
|
||||
)
|
||||
) # type: ignore
|
||||
raw_nodes = json.loads(response.response)
|
||||
nodes = [ResourceDictInstance.get_resource_instance_from_dict(n) for n in raw_nodes]
|
||||
uuids_to_nodes = {u["uuid"]: n for u, n in zip(raw_nodes, nodes)}
|
||||
for u, n in zip(raw_nodes, nodes):
|
||||
n.res_content.parent = uuids_to_nodes.get(u["parent_uuid"]).res_content if u["parent_uuid"] in uuids_to_nodes else None
|
||||
print(n.res_content.parent)
|
||||
tree_set = ResourceTreeSet.from_nested_list(nodes)
|
||||
try:
|
||||
if action == "add":
|
||||
# 添加资源到资源跟踪器
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
self.resource_tracker.add_resource(plr_resource)
|
||||
parent_uuid = tree.root_node.res_content.parent_uuid
|
||||
if parent_uuid:
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
self.lab_logger().warning(f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在")
|
||||
else:
|
||||
try:
|
||||
parent_resource.assign_child_resource(plr_resource, location=None)
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}")
|
||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
results.append({"success": True, "action": "add"})
|
||||
elif action == "update":
|
||||
# 更新资源
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
states = plr_resource.serialize_all_state()
|
||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource({"uuid": tree.root_node.res_content.uuid}, try_mode=False)
|
||||
original_instance.load_all_state(states)
|
||||
self.lab_logger().info(f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个")
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_update", None)
|
||||
if callable(func):
|
||||
func(tree_set)
|
||||
results.append({"success": True, "action": "update"})
|
||||
elif action == "remove":
|
||||
# 移除资源
|
||||
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
||||
if callable(func):
|
||||
resources_instances: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
|
||||
i in resources_uuid]
|
||||
self.resource_tracker.add_resource()
|
||||
[r.parent.unassign_child_resource(r) for r in resources_instances if r is not None]
|
||||
func(resources_instances)
|
||||
[r.parent.unassign_child_resource(r) for r in resources_instances if r is not None]
|
||||
results.append({"success": True, "action": "remove"})
|
||||
except Exception as e:
|
||||
error_msg = f"Error processing {action} operation: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
results.append({"success": False, "action": action, "error": error_msg})
|
||||
|
||||
# 返回处理结果
|
||||
result_json = {"results": results, "total": len(data)}
|
||||
res.response = json.dumps(result_json, ensure_ascii=False)
|
||||
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"Invalid JSON format: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
|
||||
return res
|
||||
|
||||
async def append_resource(req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
# 物料传输到对应的node节点
|
||||
@@ -644,7 +539,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
"s2c_resource_tree": self.create_service(
|
||||
SerialCommand,
|
||||
f"/srv{self.namespace}/s2c_resource_tree",
|
||||
s2c_resource_tree, # type: ignore
|
||||
self.s2c_resource_tree, # type: ignore
|
||||
callback_group=self.callback_group,
|
||||
),
|
||||
}
|
||||
@@ -667,6 +562,159 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
self.lab_logger().debug(f"资源更新结果: {response}")
|
||||
|
||||
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||
"""
|
||||
处理资源树更新请求
|
||||
|
||||
支持三种操作:
|
||||
- add: 添加新资源到资源树
|
||||
- update: 更新现有资源
|
||||
- remove: 从资源树中移除资源
|
||||
"""
|
||||
try:
|
||||
data = json.loads(req.command)
|
||||
results = []
|
||||
|
||||
for i in data:
|
||||
action = i.get("action") # remove, add, update
|
||||
resources_uuid: List[str] = i.get("data") # 资源数据
|
||||
self.lab_logger().info(
|
||||
f"[Resource Tree Update] Processing {action} operation, "
|
||||
f"resources count: {len(resources_uuid)}"
|
||||
)
|
||||
tree_set = None
|
||||
if action in ["add", "update"]:
|
||||
response: SerialCommand.Response = await self._resource_clients[
|
||||
"c2s_update_resource_tree"
|
||||
].call_async(
|
||||
SerialCommand.Request(
|
||||
command=json.dumps(
|
||||
{"data": {"data": resources_uuid, "with_children": False}, "action": "get"}
|
||||
)
|
||||
)
|
||||
) # type: ignore
|
||||
raw_nodes = json.loads(response.response)
|
||||
tree_set = ResourceTreeSet.from_raw_list(raw_nodes)
|
||||
try:
|
||||
if action == "add":
|
||||
# 添加资源到资源跟踪器
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
self.resource_tracker.add_resource(plr_resource)
|
||||
parent_uuid = tree.root_node.res_content.parent_uuid
|
||||
if parent_uuid:
|
||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||
if parent_resource is None:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在")
|
||||
else:
|
||||
try:
|
||||
parent_resource.assign_child_resource(plr_resource, location=None)
|
||||
except Exception as e:
|
||||
self.lab_logger().warning(
|
||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}")
|
||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
results.append({"success": True, "action": "add"})
|
||||
elif action == "update":
|
||||
# 更新资源
|
||||
plr_resources = tree_set.to_plr_resources()
|
||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||
states = plr_resource.serialize_all_state()
|
||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
||||
{"uuid": tree.root_node.res_content.uuid}, try_mode=False)
|
||||
original_instance.load_all_state(states)
|
||||
self.lab_logger().info(
|
||||
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个")
|
||||
|
||||
func = getattr(self.driver_instance, "resource_tree_update", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
results.append({"success": True, "action": "update"})
|
||||
elif action == "remove":
|
||||
# 移除资源
|
||||
plr_resources: List[ResourcePLR] = [self.resource_tracker.uuid_to_resources[i] for
|
||||
i in resources_uuid]
|
||||
func = getattr(self.driver_instance, "resource_tree_remove", None)
|
||||
if callable(func):
|
||||
func(plr_resources)
|
||||
for plr_resource in plr_resources:
|
||||
plr_resource.parent.unassign_child_resource(plr_resource)
|
||||
self.resource_tracker.remove_resource(plr_resource)
|
||||
results.append({"success": True, "action": "remove"})
|
||||
except Exception as e:
|
||||
error_msg = f"Error processing {action} operation: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
results.append({"success": False, "action": action, "error": error_msg})
|
||||
|
||||
# 返回处理结果
|
||||
result_json = {"results": results, "total": len(data)}
|
||||
res.response = json.dumps(result_json, ensure_ascii=False)
|
||||
self.lab_logger().info(f"[Resource Tree Update] Completed processing {len(data)} operations")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
error_msg = f"Invalid JSON format: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {str(e)}"
|
||||
self.lab_logger().error(f"[Resource Tree Update] {error_msg}")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
res.response = json.dumps({"success": False, "error": error_msg}, ensure_ascii=False)
|
||||
|
||||
return res
|
||||
|
||||
async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id, target_resource_uuid: str):
|
||||
# 准备工作
|
||||
uids = []
|
||||
for plr_resource in plr_resources:
|
||||
uid = getattr(plr_resource, "unilabos_uuid", None)
|
||||
if uid is None:
|
||||
raise ValueError(f"物料{plr_resource}没有unilabos_uuid属性,无法转运")
|
||||
uids.append(uid)
|
||||
srv_address = f"/srv{target_device_id}/s2c_resource_tree"
|
||||
sclient = self.create_client(SerialCommand, srv_address)
|
||||
# 等待服务可用(设置超时)
|
||||
if not sclient.wait_for_service(timeout_sec=5.0):
|
||||
self.lab_logger().error(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
|
||||
raise ValueError(f"[{self.device_id} Node-Resource] Service {srv_address} not available")
|
||||
|
||||
# 先从当前节点移除资源
|
||||
await self.s2c_resource_tree(SerialCommand_Request(command=json.dumps([{
|
||||
"action": "remove",
|
||||
"data": uids # 只移除父节点
|
||||
}], ensure_ascii=False)), SerialCommand_Response())
|
||||
|
||||
# 通知云端转运资源
|
||||
tree_set = ResourceTreeSet.from_plr_resources(plr_resources)
|
||||
for root_node in tree_set.root_nodes:
|
||||
root_node.res_content.parent = None
|
||||
root_node.res_content.parent_uuid = target_resource_uuid
|
||||
r = SerialCommand.Request()
|
||||
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致
|
||||
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
|
||||
self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}")
|
||||
|
||||
# 创建请求
|
||||
request = SerialCommand.Request()
|
||||
request.command = json.dumps([{
|
||||
"action": "add",
|
||||
"data": tree_set.all_nodes_uuid # 只添加父节点,子节点会自动添加
|
||||
}], ensure_ascii=False)
|
||||
|
||||
future = sclient.call_async(request)
|
||||
timeout = 30.0
|
||||
start_time = time.time()
|
||||
while not future.done():
|
||||
if time.time() - start_time > timeout:
|
||||
self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}")
|
||||
return False
|
||||
time.sleep(0.05)
|
||||
self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}")
|
||||
return None
|
||||
|
||||
def register_device(self):
|
||||
"""向注册表中注册设备信息"""
|
||||
topics_info = self._property_publishers.copy()
|
||||
|
||||
@@ -247,7 +247,6 @@ class HostNode(BaseROS2DeviceNode):
|
||||
else:
|
||||
resource_instance = self.resource_tracker.figure_resource({"name": node.res_content.name})
|
||||
self._resource_tracker.loop_update_uuid(resource_instance, uuid_mapping)
|
||||
|
||||
except Exception as ex:
|
||||
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
|
||||
self.lab_logger().error(traceback.format_exc())
|
||||
@@ -1323,7 +1322,7 @@ class HostNode(BaseROS2DeviceNode):
|
||||
if time.time() - start_time > timeout:
|
||||
self.lab_logger().error(f"[Host Node-Resource] Timeout waiting for response from {device_id}")
|
||||
return False
|
||||
time.sleep(0.01)
|
||||
time.sleep(0.05)
|
||||
|
||||
response = future.result()
|
||||
self.lab_logger().info(
|
||||
|
||||
@@ -454,6 +454,59 @@ class ResourceTreeSet(object):
|
||||
|
||||
return plr_resources
|
||||
|
||||
@classmethod
|
||||
def from_raw_list(cls, raw_list: List[Dict[str, Any]]) -> "ResourceTreeSet":
|
||||
"""
|
||||
从原始字典列表创建 ResourceTreeSet,自动建立 parent-children 关系
|
||||
|
||||
Args:
|
||||
raw_list: 原始字典列表,每个字典代表一个资源节点
|
||||
|
||||
Returns:
|
||||
ResourceTreeSet 实例
|
||||
|
||||
Raises:
|
||||
ValueError: 当建立关系时发现不一致
|
||||
"""
|
||||
# 第一步:将字典列表转换为 ResourceDictInstance 列表
|
||||
instances = [ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in raw_list]
|
||||
|
||||
# 第二步:建立映射关系
|
||||
uuid_to_instance: Dict[str, ResourceDictInstance] = {}
|
||||
id_to_instance: Dict[str, ResourceDictInstance] = {}
|
||||
|
||||
for raw_node, instance in zip(raw_list, instances):
|
||||
# 建立 uuid 映射
|
||||
if instance.res_content.uuid:
|
||||
uuid_to_instance[instance.res_content.uuid] = instance
|
||||
# 建立 id 映射
|
||||
if instance.res_content.id:
|
||||
id_to_instance[instance.res_content.id] = instance
|
||||
|
||||
# 第三步:建立 parent-children 关系
|
||||
for raw_node, instance in zip(raw_list, instances):
|
||||
# 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent (id)
|
||||
parent_uuid = raw_node.get("parent_uuid")
|
||||
parent_id = raw_node.get("parent")
|
||||
parent_instance = None
|
||||
|
||||
# 优先用 parent_uuid 匹配
|
||||
if parent_uuid and parent_uuid in uuid_to_instance:
|
||||
parent_instance = uuid_to_instance[parent_uuid]
|
||||
# 否则用 parent (id) 匹配
|
||||
elif parent_id and parent_id in id_to_instance:
|
||||
parent_instance = id_to_instance[parent_id]
|
||||
|
||||
# 设置 parent 引用并建立 children 关系
|
||||
if parent_instance:
|
||||
instance.res_content.parent = parent_instance.res_content
|
||||
# 将当前节点添加到父节点的 children 列表(避免重复添加)
|
||||
if instance not in parent_instance.children:
|
||||
parent_instance.children.append(instance)
|
||||
|
||||
# 第四步:使用 from_nested_list 创建 ResourceTreeSet
|
||||
return cls.from_nested_list(instances)
|
||||
|
||||
@classmethod
|
||||
def from_nested_list(cls, nested_list: List[ResourceDictInstance]) -> "ResourceTreeSet":
|
||||
"""
|
||||
@@ -497,6 +550,16 @@ class ResourceTreeSet(object):
|
||||
"""
|
||||
return [node for tree in self.trees for node in tree.get_all_nodes()]
|
||||
|
||||
@property
|
||||
def all_nodes_uuid(self) -> List[str]:
|
||||
"""
|
||||
获取所有树中的所有节点
|
||||
|
||||
Returns:
|
||||
所有节点的资源实例列表
|
||||
"""
|
||||
return [node.res_content.uuid for tree in self.trees for node in tree.get_all_nodes()]
|
||||
|
||||
def find_by_uuid(self, target_uuid: str) -> Optional[ResourceDictInstance]:
|
||||
"""
|
||||
在所有树中通过uuid查找节点
|
||||
@@ -513,6 +576,116 @@ class ResourceTreeSet(object):
|
||||
return result
|
||||
return None
|
||||
|
||||
def merge_remote_resources(self, remote_tree_set: "ResourceTreeSet") -> "ResourceTreeSet":
|
||||
"""
|
||||
将远端物料同步到本地物料中(以子树为单位)
|
||||
|
||||
同步规则:
|
||||
1. 一级节点(根节点):如果不存在的物料,引入整个子树
|
||||
2. 一级设备下的二级物料:如果不存在,引入整个子树
|
||||
3. 二级设备下的三级物料:如果不存在,引入整个子树
|
||||
如果存在则跳过并提示
|
||||
|
||||
Args:
|
||||
remote_tree_set: 远端的资源树集合
|
||||
|
||||
Returns:
|
||||
合并后的资源树集合(self)
|
||||
"""
|
||||
# 构建本地映射:一级 device id -> 根节点实例
|
||||
local_device_map: Dict[str, ResourceDictInstance] = {}
|
||||
for root_node in self.root_nodes:
|
||||
if root_node.res_content.type == "device":
|
||||
local_device_map[root_node.res_content.id] = root_node
|
||||
|
||||
# 记录需要添加的新根节点(不属于任何 device 的物料)
|
||||
new_root_nodes: List[ResourceDictInstance] = []
|
||||
|
||||
# 遍历远端根节点
|
||||
for remote_root in remote_tree_set.root_nodes:
|
||||
remote_root_id = remote_root.res_content.id
|
||||
remote_root_type = remote_root.res_content.type
|
||||
|
||||
if remote_root_type == "device":
|
||||
# 情况1: 一级是 device
|
||||
if remote_root_id not in local_device_map:
|
||||
logger.warning(f"Device '{remote_root_id}' 在本地不存在,跳过该 device 下的物料同步")
|
||||
continue
|
||||
|
||||
local_device = local_device_map[remote_root_id]
|
||||
|
||||
# 构建本地一级 device 下的子节点映射
|
||||
local_children_map = {child.res_content.name: child for child in local_device.children}
|
||||
|
||||
# 遍历远端一级 device 的子节点
|
||||
for remote_child in remote_root.children:
|
||||
remote_child_name = remote_child.res_content.name
|
||||
remote_child_type = remote_child.res_content.type
|
||||
|
||||
if remote_child_type == "device":
|
||||
# 情况2: 二级是 device
|
||||
if remote_child_name not in local_children_map:
|
||||
logger.warning(f"Device '{remote_root_id}/{remote_child_name}' 在本地不存在,跳过")
|
||||
continue
|
||||
|
||||
local_sub_device = local_children_map[remote_child_name]
|
||||
|
||||
# 构建本地二级 device 下的子节点映射
|
||||
local_sub_children_map = {child.res_content.name: child for child in local_sub_device.children}
|
||||
|
||||
# 遍历远端二级 device 的子节点(三级物料)
|
||||
added_count = 0
|
||||
for remote_material in remote_child.children:
|
||||
remote_material_name = remote_material.res_content.name
|
||||
|
||||
# 情况3: 三级物料
|
||||
if remote_material_name not in local_sub_children_map:
|
||||
# 引入整个子树
|
||||
remote_material.res_content.parent = local_sub_device.res_content
|
||||
local_sub_device.children.append(remote_material)
|
||||
added_count += 1
|
||||
else:
|
||||
logger.info(
|
||||
f"物料 '{remote_root_id}/{remote_child_name}/{remote_material_name}' "
|
||||
f"已存在,跳过"
|
||||
)
|
||||
|
||||
if added_count > 0:
|
||||
logger.info(
|
||||
f"Device '{remote_root_id}/{remote_child_name}': "
|
||||
f"从远端同步了 {added_count} 个物料子树"
|
||||
)
|
||||
else:
|
||||
# 情况2: 二级是物料(不是 device)
|
||||
if remote_child_name not in local_children_map:
|
||||
# 引入整个子树
|
||||
remote_child.res_content.parent = local_device.res_content
|
||||
local_device.children.append(remote_child)
|
||||
logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'")
|
||||
else:
|
||||
logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过")
|
||||
else:
|
||||
# 情况1: 一级节点是物料(不是 device)
|
||||
# 检查是否已存在
|
||||
existing = False
|
||||
for local_root in self.root_nodes:
|
||||
if local_root.res_content.name == remote_root.res_content.name:
|
||||
existing = True
|
||||
logger.info(f"根节点物料 '{remote_root.res_content.name}' 已存在,跳过")
|
||||
break
|
||||
|
||||
if not existing:
|
||||
# 引入整个子树
|
||||
new_root_nodes.append(remote_root)
|
||||
logger.info(f"添加远端独立物料根节点子树: '{remote_root_id}'")
|
||||
|
||||
# 将新的根节点添加到本地树集合
|
||||
if new_root_nodes:
|
||||
for new_root in new_root_nodes:
|
||||
self.trees.append(ResourceTreeInstance(new_root))
|
||||
|
||||
return self
|
||||
|
||||
def dump(self) -> List[List[Dict[str, Any]]]:
|
||||
"""
|
||||
将 ResourceTreeSet 序列化为嵌套列表格式
|
||||
|
||||
Reference in New Issue
Block a user