Files
Uni-Lab-OS/unilabos/app/web/client.py
Xuwznln 7c440d10ab Fix/resource UUID and doc fix (#109)
* Fix ResourceTreeSet load error

* Raise error when using unsupported type to create ResourceTreeSet

* Fix children key error

* Fix children key error

* Fix workstation resource not tracking

* Fix workstation deck & children resource dupe

* Fix workstation deck & children resource dupe

* Fix multiple resource error

* Fix resource tree update

* Fix resource tree update

* Force confirm uuid

* Tip more error log

* Refactor Bioyond workstation and experiment workflow (#105)

Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods.

* Fix resource get.
Fix resource parent not found.
Mapping uuid for all resources.

* mount parent uuid

* Add logging configuration based on BasicConfig in main function

* fix workstation node error

* fix workstation node error

* Update boot example

* temp fix for resource get

* temp fix for resource get

* provide error info when cant find plr type

* pack repo info

* fix to plr type error

* fix to plr type error

* Update regular container method

* support no size init

* fix comprehensive_station.json

* fix comprehensive_station.json

* fix type conversion

* fix state loading for regular container

* Update deploy-docs.yml

* Update deploy-docs.yml

---------

Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
2025-10-16 17:26:07 +08:00

327 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用
"""
import json
import os
from typing import List, Dict, Any, Optional
import requests
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.utils.log import info
from unilabos.config.config import HTTPConfig, BasicConfig
from unilabos.utils import logger
class HTTPClient:
"""HTTP客户端用于与远程服务器通信"""
def __init__(self, remote_addr: Optional[str] = None, auth: Optional[str] = None) -> None:
"""
初始化HTTP客户端
Args:
remote_addr: 远程服务器地址,如果不提供则从配置中获取
auth: 授权信息
"""
self.initialized = False
self.remote_addr = remote_addr or HTTPConfig.remote_addr
if auth is not None:
self.auth = auth
else:
auth_secret = BasicConfig.auth_secret()
self.auth = auth_secret
info(f"正在使用ak sk作为授权信息[{auth_secret}]")
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
def resource_edge_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
"""
添加资源
Args:
resources: 要添加的资源列表
database_process_later: 后台处理资源
Returns:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/edge/material/edge",
json={
"edges": resources,
},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料关系失败: {response.text}")
if response.status_code != 200 and response.status_code != 201:
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
return response
def resource_tree_add(self, resources: ResourceTreeSet, mount_uuid: str, first_add: bool) -> Dict[str, str]:
"""
添加资源
Args:
resources: 要添加的资源树集合ResourceTreeSet
mount_uuid: 要挂载的资源的uuid
first_add: 是否为首次添加资源可以是host也可以是slave来的
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4))
# 从序列化数据中提取所有节点的UUID保存旧UUID
old_uuids = {n.res_content.uuid: n for n in resources.all_nodes}
if not self.initialized or first_add:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
# 处理响应构建UUID映射
uuid_mapping = {}
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料失败: {response.text}")
else:
data = res["data"]
for i in data:
uuid_mapping[i["uuid"]] = i["cloud_uuid"]
else:
logger.error(f"添加物料失败: {response.text}")
for u, n in old_uuids.items():
if u in uuid_mapping:
n.res_content.uuid = uuid_mapping[u]
for c in n.children:
c.res_content.parent_uuid = n.res_content.uuid
else:
logger.warning(f"资源UUID未更新: {u}")
return uuid_mapping
def resource_tree_get(self, uuid_list: List[str], with_children: bool) -> List[Dict[str, Any]]:
"""
添加资源
Args:
uuid_list: List[str]
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
response = requests.post(
f"{self.remote_addr}/edge/material/query",
json={"uuids": uuid_list, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"查询物料失败: {response.text}")
else:
data = res["data"]["nodes"]
return data
else:
logger.error(f"查询物料失败: {response.text}")
return []
def resource_add(self, resources: List[Dict[str, Any]]) -> requests.Response:
"""
添加资源
Args:
resources: 要添加的资源列表
Returns:
Response: API响应对象
"""
if not self.initialized:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料失败: {response.text}")
if response.status_code != 200:
logger.error(f"添加物料失败: {response.text}")
return response
def resource_get(self, id: str, with_children: bool = False) -> Dict[str, Any]:
"""
获取资源
Args:
id: 资源ID
with_children: 是否包含子资源
Returns:
Dict: 返回的资源数据
"""
response = requests.get(
f"{self.remote_addr}/lab/material",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
return response.json()
def resource_del(self, id: str) -> requests.Response:
"""
删除资源
Args:
id: 要删除的资源ID
Returns:
Response: API响应对象
"""
response = requests.delete(
f"{self.remote_addr}/lab/resource/batch_delete/",
params={"id": id},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
return response
def resource_update(self, resources: List[Dict[str, Any]]) -> requests.Response:
"""
更新资源
Args:
resources: 要更新的资源列表
Returns:
Response: API响应对象
"""
if not self.initialized:
self.initialized = True
info(f"首次添加资源,当前远程地址: {self.remote_addr}")
response = requests.post(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
else:
response = requests.put(
f"{self.remote_addr}/lab/material",
json={"nodes": resources},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
logger.error(f"添加物料失败: {response.text}")
if response.status_code != 200:
logger.error(f"添加物料失败: {response.text}")
return response.json()
def upload_file(self, file_path: str, scene: str = "models") -> requests.Response:
"""
上传文件到服务器
使用multipart/form-data格式上传文件类似curl -F "files=@filepath"
Args:
file_path: 要上传的文件路径
scene: 上传场景,可选值为"user""models",默认为"models"
Returns:
Response: API响应对象
"""
with open(file_path, "rb") as file:
files = {"files": file}
logger.info(f"上传文件: {file_path}{scene}")
response = requests.post(
f"{self.remote_addr}/api/account/file_upload/{scene}",
files=files,
headers={"Authorization": f"Lab {self.auth}"},
timeout=30, # 上传文件可能需要更长的超时时间
)
return response
def resource_registry(self, registry_data: Dict[str, Any] | List[Dict[str, Any]]) -> requests.Response:
"""
注册资源到服务器
Args:
registry_data: 注册表数据,格式为 {resource_id: resource_info} / [{resource_info}]
Returns:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/lab/resource",
json=registry_data,
headers={"Authorization": f"Lab {self.auth}"},
timeout=30,
)
if response.status_code not in [200, 201]:
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
return response
def request_startup_json(self) -> Optional[Dict[str, Any]]:
"""
请求启动配置
Args:
startup_json: 启动配置JSON数据
Returns:
Response: API响应对象
"""
response = requests.get(
f"{self.remote_addr}/edge/material/download",
headers={"Authorization": f"Lab {self.auth}"},
timeout=(3, 30),
)
if response.status_code != 200:
logger.error(f"请求启动配置失败: {response.status_code}, {response.text}")
else:
try:
with open(os.path.join(BasicConfig.working_dir, "startup_config.json"), "w", encoding="utf-8") as f:
f.write(response.text)
target_dict = json.loads(response.text)
if "data" in target_dict:
target_dict = target_dict["data"]
return target_dict
except json.JSONDecodeError as e:
logger.error(f"解析启动配置JSON失败: {str(e.args)}\n响应内容: {response.text}")
logger.error(f"响应内容: {response.text}")
return None
# 创建默认客户端实例
http_client = HTTPClient()