Update oss config

(cherry picked from commit d39662f65f)
This commit is contained in:
Xuwznln
2025-11-18 20:17:53 +08:00
parent c68d5246d0
commit 9feadd68c6
8 changed files with 409 additions and 401 deletions

View File

@@ -67,14 +67,6 @@ class WSConfig:
max_reconnect_attempts = 999 # 最大重连次数 max_reconnect_attempts = 999 # 最大重连次数
ping_interval = 30 # ping间隔 ping_interval = 30 # ping间隔
# OSS上传配置
class OSSUploadConfig:
api_host = "" # API主机地址
authorization = "" # 授权信息
init_endpoint = "" # 初始化端点
complete_endpoint = "" # 完成端点
max_retries = 3 # 最大重试次数
# HTTP配置 # HTTP配置
class HTTPConfig: class HTTPConfig:
remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址 remote_addr = "https://uni-lab.bohrium.com/api/v1" # 远程服务器地址
@@ -294,19 +286,7 @@ HTTP 客户端配置用于与云端服务通信:
- UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1` - UAT 环境:`https://uni-lab.uat.bohrium.com/api/v1`
- 本地环境:`http://127.0.0.1:48197/api/v1` - 本地环境:`http://127.0.0.1:48197/api/v1`
### 4. OSSUploadConfig - OSS 上传配置 ### 4. ROSConfig - ROS 配置
对象存储服务配置,用于文件上传功能:
| 参数 | 类型 | 默认值 | 说明 |
| ------------------- | ---- | ------ | -------------------- |
| `api_host` | str | `""` | OSS API 主机地址 |
| `authorization` | str | `""` | 授权认证信息 |
| `init_endpoint` | str | `""` | 上传初始化端点 |
| `complete_endpoint` | str | `""` | 上传完成端点 |
| `max_retries` | int | `3` | 上传失败最大重试次数 |
### 5. ROSConfig - ROS 配置
配置 ROS 消息转换器需要加载的模块: 配置 ROS 消息转换器需要加载的模块:

View File

@@ -5,6 +5,7 @@
## 概述 ## 概述
注册表Registry是 Uni-Lab 的设备配置系统,采用 YAML 格式定义设备的: 注册表Registry是 Uni-Lab 的设备配置系统,采用 YAML 格式定义设备的:
- 可用动作Actions - 可用动作Actions
- 状态类型Status Types - 状态类型Status Types
- 初始化参数Init Parameters - 初始化参数Init Parameters
@@ -33,7 +34,7 @@
### 核心字段说明 ### 核心字段说明
| 字段名 | 类型 | 需要手写 | 说明 | | 字段名 | 类型 | 需要手写 | 说明 |
| ----------------- | ------ | -------- | ----------------------------------- | | ----------------- | ------ | -------- | --------------------------------- |
| 设备标识符 | string | 是 | 设备的唯一名字,如 `mock_chiller` | | 设备标识符 | string | 是 | 设备的唯一名字,如 `mock_chiller` |
| class | object | 部分 | 设备的核心信息,必须配置 | | class | object | 部分 | 设备的核心信息,必须配置 |
| description | string | 否 | 设备描述,系统默认给空字符串 | | description | string | 否 | 设备描述,系统默认给空字符串 |
@@ -74,8 +75,8 @@ my_device:
goal: { ... } goal: { ... }
result: { ... } result: { ... }
description: "设备描述" description: '设备描述'
version: "1.0.0" version: '1.0.0'
category: category:
- device_category - device_category
handles: [] handles: []
@@ -106,6 +107,7 @@ my_device:
适合大多数场景,快速高效。 适合大多数场景,快速高效。
**步骤** **步骤**
1. 启动 Uni-Lab 1. 启动 Uni-Lab
2. 访问 Web 界面的"注册表编辑器" 2. 访问 Web 界面的"注册表编辑器"
3. 上传您的 Python 设备驱动文件 3. 上传您的 Python 设备驱动文件
@@ -125,6 +127,7 @@ unilab -g dev.json --complete_registry --registry_path ./my_registry
``` ```
系统会: 系统会:
1. 扫描 Python 类 1. 扫描 Python 类
2. 分析方法签名和类型 2. 分析方法签名和类型
3. 自动生成缺失的字段 3. 自动生成缺失的字段
@@ -186,6 +189,7 @@ my_device:
| ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema | | ROS 动作类型 | 标准 ROS 动作 | goal_default 和 schema |
**常用的 ROS 动作类型** **常用的 ROS 动作类型**
- `SendCmd`:发送简单命令 - `SendCmd`:发送简单命令
- `NavigateThroughPoses`:导航动作 - `NavigateThroughPoses`:导航动作
- `SingleJointPosition`:单关节位置控制 - `SingleJointPosition`:单关节位置控制
@@ -297,7 +301,7 @@ my_device:
### 识别规则 ### 识别规则
| Python 类型 | placeholder_keys 值 | 前端效果 | | Python 类型 | placeholder_keys 值 | 前端效果 |
|-----------|-------------------|---------| | -------------------- | -------------------- | -------------- |
| `ResourceSlot` | `unilabos_resources` | 单选资源下拉框 | | `ResourceSlot` | `unilabos_resources` | 单选资源下拉框 |
| `List[ResourceSlot]` | `unilabos_resources` | 多选资源下拉框 | | `List[ResourceSlot]` | `unilabos_resources` | 多选资源下拉框 |
| `DeviceSlot` | `unilabos_devices` | 单选设备下拉框 | | `DeviceSlot` | `unilabos_devices` | 单选设备下拉框 |
@@ -313,6 +317,7 @@ placeholder_keys:
``` ```
**前端渲染**: **前端渲染**:
``` ```
Source: [下拉选择框 ▼] Source: [下拉选择框 ▼]
├── plate_1 (96孔板) ├── plate_1 (96孔板)
@@ -329,6 +334,7 @@ placeholder_keys:
``` ```
**前端渲染**: **前端渲染**:
``` ```
Targets: [多选下拉框 ▼] Targets: [多选下拉框 ▼]
☑ plate_1 (96孔板) ☑ plate_1 (96孔板)
@@ -345,6 +351,7 @@ placeholder_keys:
``` ```
**前端渲染**: **前端渲染**:
``` ```
Pump: [下拉选择框 ▼] Pump: [下拉选择框 ▼]
├── pump_1 (注射泵A) ├── pump_1 (注射泵A)
@@ -360,6 +367,7 @@ placeholder_keys:
``` ```
**前端渲染**: **前端渲染**:
``` ```
Sync Devices: [多选下拉框 ▼] Sync Devices: [多选下拉框 ▼]
☑ heater_1 (加热器A) ☑ heater_1 (加热器A)
@@ -414,7 +422,7 @@ placeholder_keys:
### status_types ### status_types
系统会扫描你的 Python 类从状态方法propertyget_方法自动生成这部分 系统会扫描你的 Python 类从状态方法propertyget\_方法自动生成这部分
```yaml ```yaml
status_types: status_types:
@@ -424,6 +432,7 @@ status_types:
``` ```
**注意事项** **注意事项**
- 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性 - 系统会查找所有 `get_` 开头的方法和 `@property` 装饰的属性
- 类型会自动转成相应的类型(如 `str``float``bool` - 类型会自动转成相应的类型(如 `str``float``bool`
- 如果类型是 `Any``None` 或未知的,默认使用 `String` - 如果类型是 `Any``None` 或未知的,默认使用 `String`
@@ -459,6 +468,7 @@ init_param_schema:
``` ```
**生成规则** **生成规则**
- `config` 部分:分析 `__init__` 方法的参数、类型和默认值 - `config` 部分:分析 `__init__` 方法的参数、类型和默认值
- `data` 部分:根据 `status_types` 生成前端显示用的类型定义 - `data` 部分:根据 `status_types` 生成前端显示用的类型定义
@@ -619,22 +629,22 @@ advanced_liquid_handler:
result: result:
success: success success: success
schema: schema:
description: "转移液体" description: '转移液体'
properties: properties:
goal: goal:
properties: properties:
source: source:
type: object type: object
description: "源容器" description: '源容器'
target: target:
type: object type: object
description: "目标容器" description: '目标容器'
volume: volume:
type: number type: number
description: "体积(μL)" description: '体积(μL)'
tip: tip:
type: object type: object
description: "枪头(可选)" description: '枪头(可选)'
required: required:
- source - source
- target - target
@@ -668,12 +678,12 @@ advanced_liquid_handler:
result: result:
success: success success: success
description: "高级液体处理工作站,支持多目标转移和设备协同" description: '高级液体处理工作站,支持多目标转移和设备协同'
version: "1.0.0" version: '1.0.0'
category: category:
- liquid_handling - liquid_handling
handles: [] handles: []
icon: "" icon: ''
``` ```
### 另一个完整示例:温度控制器 ### 另一个完整示例:温度控制器
@@ -895,6 +905,7 @@ cat unilabos/registry/devices/my_device.yaml
### 2. 验证 placeholder_keys ### 2. 验证 placeholder_keys
确认: 确认:
- ResourceSlot 参数有 `unilabos_resources` - ResourceSlot 参数有 `unilabos_resources`
- DeviceSlot 参数有 `unilabos_devices` - DeviceSlot 参数有 `unilabos_devices`
- List 类型被正确识别 - List 类型被正确识别
@@ -919,8 +930,10 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
### Q1: placeholder_keys 没有自动生成 ### Q1: placeholder_keys 没有自动生成
**检查**: **检查**:
1. 是否使用了`--complete_registry`参数? 1. 是否使用了`--complete_registry`参数?
2. 类型注解是否正确? 2. 类型注解是否正确?
```python ```python
# ✓ 正确 # ✓ 正确
def method(self, resource: ResourceSlot): def method(self, resource: ResourceSlot):
@@ -928,6 +941,7 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
# ✗ 错误(缺少类型注解) # ✗ 错误(缺少类型注解)
def method(self, resource): def method(self, resource):
``` ```
3. 是否正确导入? 3. 是否正确导入?
```python ```python
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
@@ -938,6 +952,7 @@ python -c "from unilabos.devices.my_module.my_device import MyDevice"
**原因**: placeholder_keys 未正确配置 **原因**: placeholder_keys 未正确配置
**解决**: **解决**:
```yaml ```yaml
# 检查YAML中是否有 # 检查YAML中是否有
placeholder_keys: placeholder_keys:
@@ -947,6 +962,7 @@ placeholder_keys:
### Q3: 多选不工作 ### Q3: 多选不工作
**检查类型注解**: **检查类型注解**:
```python ```python
# ✓ 正确 - 会生成多选 # ✓ 正确 - 会生成多选
def method(self, resources: List[ResourceSlot]): def method(self, resources: List[ResourceSlot]):
@@ -960,6 +976,7 @@ def method(self, resources: ResourceSlot):
**说明**: 运行时会自动转换 **说明**: 运行时会自动转换
前端传递: 前端传递:
```json ```json
{ {
"resource": "plate_1" // 字符串ID "resource": "plate_1" // 字符串ID
@@ -967,6 +984,7 @@ def method(self, resources: ResourceSlot):
``` ```
运行时收到: 运行时收到:
```python ```python
resource.id # "plate_1" resource.id # "plate_1"
resource.name # "96孔板" resource.name # "96孔板"
@@ -977,6 +995,7 @@ resource.type # "resource"
### Q5: 设备加载不了 ### Q5: 设备加载不了
**检查**: **检查**:
1. 确认 `class.module` 路径是否正确 1. 确认 `class.module` 路径是否正确
2. 确认 Python 驱动类能否正常导入 2. 确认 Python 驱动类能否正常导入
3. 使用 yaml 验证器检查文件格式 3. 使用 yaml 验证器检查文件格式
@@ -985,6 +1004,7 @@ resource.type # "resource"
### Q6: 自动生成失败 ### Q6: 自动生成失败
**检查**: **检查**:
1. 确认类继承了正确的基类 1. 确认类继承了正确的基类
2. 确保状态方法的返回类型注解清晰 2. 确保状态方法的返回类型注解清晰
3. 检查类能否被动态导入 3. 检查类能否被动态导入
@@ -993,6 +1013,7 @@ resource.type # "resource"
### Q7: 前端显示问题 ### Q7: 前端显示问题
**解决步骤**: **解决步骤**:
1. 删除旧的 yaml 文件,用编辑器重新生成 1. 删除旧的 yaml 文件,用编辑器重新生成
2. 清除浏览器缓存,重新加载页面 2. 清除浏览器缓存,重新加载页面
3. 确认必需字段(如 `schema`)都存在 3. 确认必需字段(如 `schema`)都存在
@@ -1001,6 +1022,7 @@ resource.type # "resource"
### Q8: 动作执行出错 ### Q8: 动作执行出错
**检查**: **检查**:
1. 确认动作方法名符合规范(如 `execute_<action_name>` 1. 确认动作方法名符合规范(如 `execute_<action_name>`
2. 检查 `goal` 字段的参数映射是否正确 2. 检查 `goal` 字段的参数映射是否正确
3. 确认方法返回值格式符合 `result` 映射 3. 确认方法返回值格式符合 `result` 映射
@@ -1075,6 +1097,7 @@ def method(
``` ```
5. **方法命名规范** 5. **方法命名规范**
- 状态方法使用 `@property` 装饰器或 `get_` 前缀 - 状态方法使用 `@property` 装饰器或 `get_` 前缀
- 动作方法使用动词开头 - 动作方法使用动词开头
- 保持命名清晰、一致 - 保持命名清晰、一致
@@ -1114,5 +1137,3 @@ def method(
- Python [typing 模块](https://docs.python.org/3/library/typing.html) - Python [typing 模块](https://docs.python.org/3/library/typing.html)
- [YAML 语法](https://yaml.org/) - [YAML 语法](https://yaml.org/)
- [JSON Schema](https://json-schema.org/) - [JSON Schema](https://json-schema.org/)

View File

@@ -50,8 +50,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.client = tcp.register_node_list(self.nodes) self.client = tcp.register_node_list(self.nodes)
``` ```
## 2. 编写驱动与寄存器读写 ## 2. 编写驱动与寄存器读写
### 2.1 寄存器示例 ### 2.1 寄存器示例
@@ -95,8 +93,8 @@ def start_and_read_metrics(self):
完成工站类与驱动后,需要生成(或更新)工站注册表供系统识别。 完成工站类与驱动后,需要生成(或更新)工站注册表供系统识别。
### 3.1 新增工站设备(或资源)首次生成注册表 ### 3.1 新增工站设备(或资源)首次生成注册表
首先通过以下命令启动 unilab。进入 unilab 系统状态检查页面 首先通过以下命令启动 unilab。进入 unilab 系统状态检查页面
```bash ```bash
@@ -112,6 +110,7 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
![注册表生成流程](image_battery_plc/unilab_registry_process.png) ![注册表生成流程](image_battery_plc/unilab_registry_process.png)
步骤说明: 步骤说明:
1. 选择新增的工站`coin_cell_assembly.py`文件 1. 选择新增的工站`coin_cell_assembly.py`文件
2. 点击分析按钮,分析`coin_cell_assembly.py`文件 2. 点击分析按钮,分析`coin_cell_assembly.py`文件
3. 选择`coin_cell_assembly.py`文件中继承`WorkstationBase` 3. 选择`coin_cell_assembly.py`文件中继承`WorkstationBase`
@@ -124,20 +123,16 @@ python unilabos\app\main.py -g celljson.json --ak <user的AK> --sk <user的SK>
![生成的YAML文件](image_battery_plc/unilab_new_yaml.png) ![生成的YAML文件](image_battery_plc/unilab_new_yaml.png)
### 3.2 添加新生成注册表 ### 3.2 添加新生成注册表
`unilabos\registry\devices`目录下新建一个 yaml 文件,此处新建文件命名为`coincellassemblyworkstation_device.yaml`,将上面生成的新的注册表信息粘贴到`coincellassemblyworkstation_device.yaml`文件中。 `unilabos\registry\devices`目录下新建一个 yaml 文件,此处新建文件命名为`coincellassemblyworkstation_device.yaml`,将上面生成的新的注册表信息粘贴到`coincellassemblyworkstation_device.yaml`文件中。
在终端输入以下命令进行注册表补全操作。 在终端输入以下命令进行注册表补全操作。
```bash ```bash
python unilabos\app\register.py --complete_registry python unilabos\app\register.py --complete_registry
``` ```
### 3.3 启动并上传注册表 ### 3.3 启动并上传注册表
新增设备之后,启动 unilab 需要增加`--upload_registry`参数,来上传注册表信息。 新增设备之后,启动 unilab 需要增加`--upload_registry`参数,来上传注册表信息。
@@ -159,6 +154,7 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
### 4.2 首次接入流程 ### 4.2 首次接入流程
首次新增设备(或资源)需要完整流程: 首次新增设备(或资源)需要完整流程:
1. ✅ 在网页端生成注册表信息 1. ✅ 在网页端生成注册表信息
2. ✅ 使用 `--complete_registry` 补全注册表 2. ✅ 使用 `--complete_registry` 补全注册表
3. ✅ 使用 `--upload_registry` 上传注册表信息 3. ✅ 使用 `--upload_registry` 上传注册表信息
@@ -166,6 +162,7 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
### 4.3 驱动更新流程 ### 4.3 驱动更新流程
如果不是新增设备,仅修改了工站驱动的 `.py` 文件: 如果不是新增设备,仅修改了工站驱动的 `.py` 文件:
1. ✅ 运行 `--complete_registry` 补全注册表 1. ✅ 运行 `--complete_registry` 补全注册表
2. ✅ 运行 `--upload_registry` 上传注册表 2. ✅ 运行 `--upload_registry` 上传注册表
3. ❌ 不需要在网页端重新生成注册表 3. ❌ 不需要在网页端重新生成注册表
@@ -203,5 +200,3 @@ module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinC
5. ✅ 新增设备与更新驱动的区别 5. ✅ 新增设备与更新驱动的区别
这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。 这个案例展示了完整的 PLC 设备接入流程,可以作为其他类似设备接入的参考模板。

View File

@@ -592,7 +592,7 @@ class PLCWorkstation(WorkstationBase):
### 8.1 WorkstationBase 核心属性 ### 8.1 WorkstationBase 核心属性
| 属性 | 类型 | 说明 | | 属性 | 类型 | 说明 |
| --------------------------- | ----------------------- | ----------------------------- | | ------------------------- | ----------------------- | ------------------------------- |
| `_ros_node` | ROS2WorkstationNode | ROS 节点引用,由 post_init 设置 | | `_ros_node` | ROS2WorkstationNode | ROS 节点引用,由 post_init 设置 |
| `deck` | Deck | PyLabRobot Deck本地物料系统 | | `deck` | Deck | PyLabRobot Deck本地物料系统 |
| `plr_resources` | Dict[str, PLRResource] | 物料资源映射 | | `plr_resources` | Dict[str, PLRResource] | 物料资源映射 |

View File

@@ -592,4 +592,3 @@ ros2 topic list
- [ROS2 网络配置](https://docs.ros.org/en/humble/Tutorials/Advanced/Networking.html) - [ROS2 网络配置](https://docs.ros.org/en/humble/Tutorials/Advanced/Networking.html)
- [DDS 配置](https://fast-dds.docs.eprosima.com/) - [DDS 配置](https://fast-dds.docs.eprosima.com/)
- Uni-Lab 云平台文档 - Uni-Lab 云平台文档

View File

@@ -1,161 +1,156 @@
import argparse import argparse
import os import os
import time import time
from datetime import datetime
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple
import requests import requests
from unilabos.config.config import OSSUploadConfig from unilabos.app.web.client import http_client, HTTPClient
from unilabos.utils import logger
def _init_upload(file_path: str, oss_path: str, filename: Optional[str] = None, def _get_oss_token(
process_key: str = "file-upload", device_id: str = "default", filename: str,
expires_hours: int = 1) -> Tuple[bool, Dict]: driver_name: str = "default",
exp_type: str = "default",
client: Optional[HTTPClient] = None,
) -> Tuple[bool, Dict]:
""" """
初始化上传过程 获取OSS上传Token
Args: Args:
file_path: 本地文件路径 filename: 文件名
oss_path: OSS目标路径 driver_name: 驱动名称
filename: 文件名如果为None则使用file_path的文件名 exp_type: 实验类型
process_key: 处理键 client: HTTPClient实例如果不提供则使用默认的http_client
device_id: 设备ID
expires_hours: 链接过期小时数
Returns: Returns:
(成功标志, 响应数据) (成功标志, Token数据字典包含token/path/host/expires)
""" """
if filename is None: # 使用提供的client或默认的http_client
filename = os.path.basename(file_path) if client is None:
client = http_client
# 构造初始化请求 # 构造scene参数: driver_name-exp_type
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.init_endpoint}" scene = f"{driver_name}-{exp_type}"
headers = {
"Authorization": OSSUploadConfig.authorization,
"Content-Type": "application/json"
}
payload = { # 构造请求URL使用client的remote_addr已包含/api/v1/
"device_id": device_id, url = f"{client.remote_addr}/applications/token"
"process_key": process_key, params = {"scene": scene, "filename": filename}
"filename": filename,
"path": oss_path,
"expires_hours": expires_hours
}
try: try:
response = requests.post(url, headers=headers, json=payload) logger.info(f"[OSS] 请求预签名URL: scene={scene}, filename={filename}")
if response.status_code == 201: response = requests.get(url, params=params, headers={"Authorization": f"Lab {client.auth}"}, timeout=10)
result = response.json()
if result.get("code") == "10000":
return True, result.get("data", {})
print(f"初始化上传失败: {response.status_code}, {response.text}") if response.status_code == 200:
result = response.json()
if result.get("code") == 0:
data = result.get("data", {})
# 转换expires时间戳为可读格式
expires_timestamp = data.get("expires", 0)
expires_datetime = datetime.fromtimestamp(expires_timestamp)
expires_str = expires_datetime.strftime("%Y-%m-%d %H:%M:%S")
logger.info(f"[OSS] 获取预签名URL成功")
logger.info(f"[OSS] - URL: {data.get('url', 'N/A')}")
logger.info(f"[OSS] - Expires: {expires_str} (timestamp: {expires_timestamp})")
return True, data
logger.error(f"[OSS] 获取预签名URL失败: {response.status_code}, {response.text}")
return False, {} return False, {}
except Exception as e: except Exception as e:
print(f"初始化上传异常: {str(e)}") logger.error(f"[OSS] 获取预签名URL异常: {str(e)}")
return False, {} return False, {}
def _put_upload(file_path: str, upload_url: str) -> bool: def _put_upload(file_path: str, upload_url: str) -> bool:
""" """
执行PUT上传 使用预签名URL上传文件到OSS
Args: Args:
file_path: 本地文件路径 file_path: 本地文件路径
upload_url: 上传URL upload_url: 完整的预签名上传URL
Returns: Returns:
是否成功 是否成功
""" """
try: try:
logger.info(f"[OSS] 开始上传文件: {file_path}")
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
response = requests.put(upload_url, data=f) # 使用预签名URL上传不需要额外的认证header
response = requests.put(upload_url, data=f, timeout=300)
if response.status_code == 200: if response.status_code == 200:
logger.info(f"[OSS] 文件上传成功")
return True return True
print(f"PUT上传失败: {response.status_code}, {response.text}") logger.error(f"[OSS] 上传失败: {response.status_code}")
logger.error(f"[OSS] 响应内容: {response.text[:500] if response.text else '无响应内容'}")
return False return False
except Exception as e: except Exception as e:
print(f"PUT上传异常: {str(e)}") logger.error(f"[OSS] 上传异常: {str(e)}")
return False return False
def _complete_upload(uuid: str) -> bool: def oss_upload(
""" file_path: str,
完成上传过程 filename: Optional[str] = None,
driver_name: str = "default",
Args: exp_type: str = "default",
uuid: 上传的UUID max_retries: int = 3,
client: Optional[HTTPClient] = None,
Returns: ) -> Dict:
是否成功
"""
url = f"{OSSUploadConfig.api_host}{OSSUploadConfig.complete_endpoint}"
headers = {
"Authorization": OSSUploadConfig.authorization,
"Content-Type": "application/json"
}
payload = {
"uuid": uuid
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
result = response.json()
if result.get("code") == "10000":
return True
print(f"完成上传失败: {response.status_code}, {response.text}")
return False
except Exception as e:
print(f"完成上传异常: {str(e)}")
return False
def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
process_key: str = "file-upload", device_id: str = "default") -> bool:
""" """
文件上传主函数,包含重试机制 文件上传主函数,包含重试机制
Args: Args:
file_path: 本地文件路径 file_path: 本地文件路径
oss_path: OSS目标路径
filename: 文件名如果为None则使用file_path的文件名 filename: 文件名如果为None则使用file_path的文件名
process_key: 处理键 driver_name: 驱动名称用于构造scene
device_id: 设备ID exp_type: 实验类型用于构造scene
max_retries: 最大重试次数
client: HTTPClient实例如果不提供则使用默认的http_client
Returns: Returns:
是否成功上传 Dict: {
"success": bool, # 是否上传成功
"original_path": str, # 原始文件路径
"oss_path": str # OSS路径成功时或空字符串失败时
}
""" """
max_retries = OSSUploadConfig.max_retries if filename is None:
filename = os.path.basename(file_path)
if not os.path.exists(file_path):
logger.error(f"[OSS] 文件不存在: {file_path}")
return {"success": False, "original_path": file_path, "oss_path": ""}
retry_count = 0 retry_count = 0
oss_path = ""
while retry_count < max_retries: while retry_count < max_retries:
try: try:
# 步骤1初始化上传 # 步骤1获取预签名URL
init_success, init_data = _init_upload( token_success, token_data = _get_oss_token(
file_path=file_path, filename=filename, driver_name=driver_name, exp_type=exp_type, client=client
oss_path=oss_path,
filename=filename,
process_key=process_key,
device_id=device_id
) )
if not init_success: if not token_success:
print(f"初始化上传失败,重试 {retry_count + 1}/{max_retries}") logger.warning(f"[OSS] 获取预签名URL失败,重试 {retry_count + 1}/{max_retries}")
retry_count += 1 retry_count += 1
time.sleep(1) # 等待1秒后重试 time.sleep(1)
continue continue
# 获取UUID和上传URL # 获取预签名URL和OSS路径
uuid = init_data.get("uuid") upload_url = token_data.get("url")
upload_url = init_data.get("upload_url") oss_path = token_data.get("path", "")
if not uuid or not upload_url: if not upload_url:
print(f"初始化上传返回数据不完整,重试 {retry_count + 1}/{max_retries}") logger.warning(f"[OSS] 无法获取上传URLAPI未返回url字段")
retry_count += 1 retry_count += 1
time.sleep(1) time.sleep(1)
continue continue
@@ -163,69 +158,82 @@ def oss_upload(file_path: str, oss_path: str, filename: Optional[str] = None,
# 步骤2PUT上传文件 # 步骤2PUT上传文件
put_success = _put_upload(file_path, upload_url) put_success = _put_upload(file_path, upload_url)
if not put_success: if not put_success:
print(f"PUT上传失败重试 {retry_count + 1}/{max_retries}") logger.warning(f"[OSS] PUT上传失败重试 {retry_count + 1}/{max_retries}")
retry_count += 1
time.sleep(1)
continue
# 步骤3完成上传
complete_success = _complete_upload(uuid)
if not complete_success:
print(f"完成上传失败,重试 {retry_count + 1}/{max_retries}")
retry_count += 1 retry_count += 1
time.sleep(1) time.sleep(1)
continue continue
# 所有步骤都成功 # 所有步骤都成功
print(f"文件 {file_path} 上传成功") logger.info(f"[OSS] 文件 {file_path} 上传成功")
return True return {"success": True, "original_path": file_path, "oss_path": oss_path}
except Exception as e: except Exception as e:
print(f"上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}") logger.error(f"[OSS] 上传过程异常: {str(e)},重试 {retry_count + 1}/{max_retries}")
retry_count += 1 retry_count += 1
time.sleep(1) time.sleep(1)
print(f"文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}") logger.error(f"[OSS] 文件 {file_path} 上传失败,已达到最大重试次数 {max_retries}")
return False return {"success": False, "original_path": file_path, "oss_path": oss_path}
if __name__ == "__main__": if __name__ == "__main__":
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt # python -m unilabos.app.oss_upload -f /path/to/your/file.txt --driver HPLC --type test
# python -m unilabos.app.oss_upload -f /path/to/your/file.txt --driver HPLC --type test \
# --ak xxx --sk yyy --remote-addr http://xxx/api/v1
# 命令行参数解析 # 命令行参数解析
parser = argparse.ArgumentParser(description='文件上传测试工具') parser = argparse.ArgumentParser(description="文件上传测试工具")
parser.add_argument('--file', '-f', type=str, required=True, help='要上传的本地文件路径') parser.add_argument("--file", "-f", type=str, required=True, help="要上传的本地文件路径")
parser.add_argument('--path', '-p', type=str, default='/HPLC1/Any', help='OSS目标路径') parser.add_argument("--driver", "-d", type=str, default="default", help="驱动名称")
parser.add_argument('--device', '-d', type=str, default='test-device', help='设备ID') parser.add_argument("--type", "-t", type=str, default="default", help="实验类型")
parser.add_argument('--process', '-k', type=str, default='HPLC-txt-result', help='处理键') parser.add_argument("--ak", type=str, help="Access Key如果提供则覆盖配置")
parser.add_argument("--sk", type=str, help="Secret Key如果提供则覆盖配置")
parser.add_argument("--remote-addr", type=str, help="远程服务器地址(包含/api/v1如果提供则覆盖配置")
args = parser.parse_args() args = parser.parse_args()
# 检查文件是否存在 # 检查文件是否存在
if not os.path.exists(args.file): if not os.path.exists(args.file):
print(f"错误:文件 {args.file} 不存在") logger.error(f"错误:文件 {args.file} 不存在")
exit(1) exit(1)
print("=" * 50) # 如果提供了ak/sk/remote_addr创建临时HTTPClient
print(f"开始上传文件: {args.file}") temp_client = None
print(f"目标路径: {args.path}") if args.ak and args.sk:
print(f"设备ID: {args.device}") import base64
print(f"处理键: {args.process}")
print("=" * 50) auth = base64.b64encode(f"{args.ak}:{args.sk}".encode("utf-8")).decode("utf-8")
remote_addr = args.remote_addr if args.remote_addr else http_client.remote_addr
temp_client = HTTPClient(remote_addr=remote_addr, auth=auth)
logger.info(f"[配置] 使用自定义配置: remote_addr={remote_addr}")
elif args.remote_addr:
temp_client = HTTPClient(remote_addr=args.remote_addr, auth=http_client.auth)
logger.info(f"[配置] 使用自定义remote_addr: {args.remote_addr}")
else:
logger.info(f"[配置] 使用默认配置: remote_addr={http_client.remote_addr}")
logger.info("=" * 50)
logger.info(f"开始上传文件: {args.file}")
logger.info(f"驱动名称: {args.driver}")
logger.info(f"实验类型: {args.type}")
logger.info(f"Scene: {args.driver}-{args.type}")
logger.info("=" * 50)
# 执行上传 # 执行上传
success = oss_upload( result = oss_upload(
file_path=args.file, file_path=args.file,
oss_path=args.path,
filename=None, # 使用默认文件名 filename=None, # 使用默认文件名
process_key=args.process, driver_name=args.driver,
device_id=args.device exp_type=args.type,
client=temp_client,
) )
# 输出结果 # 输出结果
if success: if result["success"]:
print("\n√ 文件上传成功!") logger.info(f"\n√ 文件上传成功!")
logger.info(f"原始路径: {result['original_path']}")
logger.info(f"OSS路径: {result['oss_path']}")
exit(0) exit(0)
else: else:
print("\n× 文件上传失败!") logger.error(f"\n× 文件上传失败!")
logger.error(f"原始路径: {result['original_path']}")
exit(1) exit(1)

View File

@@ -36,15 +36,6 @@ class WSConfig:
ping_interval = 30 # ping间隔 ping_interval = 30 # ping间隔
# OSS上传配置
class OSSUploadConfig:
api_host = ""
authorization = ""
init_endpoint = ""
complete_endpoint = ""
max_retries = 3
# HTTP配置 # HTTP配置
class HTTPConfig: class HTTPConfig:
remote_addr = "http://127.0.0.1:48197/api/v1" remote_addr = "http://127.0.0.1:48197/api/v1"

View File

@@ -405,9 +405,19 @@ class RunningResultChecker(DriverChecker):
for i in range(self.driver._finished, temp): for i in range(self.driver._finished, temp):
sample_id = self.driver._get_resource_sample_id(self.driver._wf_name, i) # 从0开始计数 sample_id = self.driver._get_resource_sample_id(self.driver._wf_name, i) # 从0开始计数
pdf, txt = self.driver.get_data_file(i + 1) pdf, txt = self.driver.get_data_file(i + 1)
device_id = self.driver.device_id if hasattr(self.driver, "device_id") else "default" # 使用新的OSS上传接口传入driver_name和exp_type
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example", device_id=device_id) pdf_result = oss_upload(pdf, filename=os.path.basename(pdf), driver_name="HPLC", exp_type="analysis")
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result", device_id=device_id) txt_result = oss_upload(txt, filename=os.path.basename(txt), driver_name="HPLC", exp_type="result")
if pdf_result["success"]:
print(f"PDF上传成功: {pdf_result['oss_path']}")
else:
print(f"PDF上传失败: {pdf_result['original_path']}")
if txt_result["success"]:
print(f"TXT上传成功: {txt_result['oss_path']}")
else:
print(f"TXT上传失败: {txt_result['original_path']}")
# self.driver.extract_data_from_txt() # self.driver.extract_data_from_txt()
except Exception as ex: except Exception as ex:
self.driver._finished = 0 self.driver._finished = 0
@@ -456,8 +466,12 @@ if __name__ == "__main__":
} }
sample_id = obj._get_resource_sample_id("test", 0) sample_id = obj._get_resource_sample_id("test", 0)
pdf, txt = obj.get_data_file("1", after_time=datetime(2024, 11, 6, 19, 3, 6)) pdf, txt = obj.get_data_file("1", after_time=datetime(2024, 11, 6, 19, 3, 6))
oss_upload(pdf, f"hplc/{sample_id}/{os.path.basename(pdf)}", process_key="example") # 使用新的OSS上传接口传入driver_name和exp_type
oss_upload(txt, f"hplc/{sample_id}/{os.path.basename(txt)}", process_key="HPLC-txt-result") pdf_result = oss_upload(pdf, filename=os.path.basename(pdf), driver_name="HPLC", exp_type="analysis")
txt_result = oss_upload(txt, filename=os.path.basename(txt), driver_name="HPLC", exp_type="result")
print(f"PDF上传结果: {pdf_result}")
print(f"TXT上传结果: {txt_result}")
# driver = HPLCDriver() # driver = HPLCDriver()
# for i in range(10000): # for i in range(10000):
# print({k: v for k, v in driver._device_status.items() if isinstance(v, str)}) # print({k: v for k, v in driver._device_status.items() if isinstance(v, str)})