diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py index e80a1ef6..6d512720 100644 --- a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py @@ -2,6 +2,7 @@ from datetime import datetime import json import time from typing import Optional, Dict, Any, List +from typing_extensions import TypedDict import requests from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG @@ -13,6 +14,14 @@ import sys from pathlib import Path import importlib +class ComputeExperimentDesignReturn(TypedDict): + solutions: list + titration: dict + solvents: dict + feeding_order: list + return_info: str + + class BioyondDispensingStation(BioyondWorkstation): def __init__( self, @@ -102,7 +111,7 @@ class BioyondDispensingStation(BioyondWorkstation): wt_percent: str = "0.25", m_tot: str = "70", titration_percent: str = "0.03", - ) -> dict: + ) -> ComputeExperimentDesignReturn: try: if isinstance(ratio, str): try: diff --git a/unilabos/registry/devices/bioyond.yaml b/unilabos/registry/devices/bioyond.yaml index 37dd0fae..3325a260 100644 --- a/unilabos/registry/devices/bioyond.yaml +++ b/unilabos/registry/devices/bioyond.yaml @@ -83,6 +83,96 @@ workstation.bioyond_dispensing_station: title: batch_create_diamine_solution_tasks参数 type: object type: UniLabJsonCommand + auto-brief_step_parameters: + feedback: {} + goal: {} + goal_default: + data: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + data: + type: object + required: + - data + type: object + result: {} + required: + - goal + title: brief_step_parameters参数 + type: object + type: UniLabJsonCommand + auto-compute_experiment_design: + feedback: {} + goal: {} + goal_default: + m_tot: '70' + ratio: null + titration_percent: '0.03' + wt_percent: '0.25' + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + m_tot: + default: '70' + type: string + ratio: + type: object + titration_percent: + default: '0.03' + type: string + wt_percent: + default: '0.25' + type: string + required: + - ratio + type: object + result: + properties: + feeding_order: + items: {} + title: Feeding Order + type: array + return_info: + title: Return Info + type: string + solutions: + items: {} + title: Solutions + type: array + solvents: + additionalProperties: true + title: Solvents + type: object + titration: + additionalProperties: true + title: Titration + type: object + required: + - solutions + - titration + - solvents + - feeding_order + - return_info + title: ComputeExperimentDesignReturn + type: object + required: + - goal + title: compute_experiment_design参数 + type: object + type: UniLabJsonCommand auto-process_order_finish_report: feedback: {} goal: {} @@ -112,6 +202,85 @@ workstation.bioyond_dispensing_station: title: process_order_finish_report参数 type: object type: UniLabJsonCommand + auto-project_order_report: + feedback: {} + goal: {} + goal_default: + order_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + order_id: + type: string + required: + - order_id + type: object + result: {} + required: + - goal + title: project_order_report参数 + type: object + type: UniLabJsonCommand + auto-query_resource_by_name: + feedback: {} + goal: {} + goal_default: + material_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + material_name: + type: string + required: + - material_name + type: object + result: {} + required: + - goal + title: query_resource_by_name参数 + type: object + type: UniLabJsonCommand + auto-transfer_materials_to_reaction_station: + feedback: {} + goal: {} + goal_default: + target_device_id: null + transfer_groups: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + target_device_id: + type: string + transfer_groups: + type: array + required: + - target_device_id + - transfer_groups + type: object + result: {} + required: + - goal + title: transfer_materials_to_reaction_station参数 + type: object + type: UniLabJsonCommand auto-wait_for_multiple_orders_and_get_reports: feedback: {} goal: {} @@ -144,6 +313,31 @@ workstation.bioyond_dispensing_station: title: wait_for_multiple_orders_and_get_reports参数 type: object type: UniLabJsonCommand + auto-workflow_sample_locations: + feedback: {} + goal: {} + goal_default: + workflow_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_id: + type: string + required: + - workflow_id + type: object + result: {} + required: + - goal + title: workflow_sample_locations参数 + type: object + type: UniLabJsonCommand create_90_10_vial_feeding_task: feedback: {} goal: diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml index c98983c5..9ae76b7f 100644 --- a/unilabos/registry/devices/bioyond_dispensing_station.yaml +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -5,6 +5,96 @@ bioyond_dispensing_station: - bioyond_dispensing_station class: action_value_mappings: + auto-brief_step_parameters: + feedback: {} + goal: {} + goal_default: + data: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + data: + type: object + required: + - data + type: object + result: {} + required: + - goal + title: brief_step_parameters参数 + type: object + type: UniLabJsonCommand + auto-compute_experiment_design: + feedback: {} + goal: {} + goal_default: + m_tot: '70' + ratio: null + titration_percent: '0.03' + wt_percent: '0.25' + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + m_tot: + default: '70' + type: string + ratio: + type: object + titration_percent: + default: '0.03' + type: string + wt_percent: + default: '0.25' + type: string + required: + - ratio + type: object + result: + properties: + feeding_order: + items: {} + title: Feeding Order + type: array + return_info: + title: Return Info + type: string + solutions: + items: {} + title: Solutions + type: array + solvents: + additionalProperties: true + title: Solvents + type: object + titration: + additionalProperties: true + title: Titration + type: object + required: + - solutions + - titration + - solvents + - feeding_order + - return_info + title: ComputeExperimentDesignReturn + type: object + required: + - goal + title: compute_experiment_design参数 + type: object + type: UniLabJsonCommand auto-process_order_finish_report: feedback: {} goal: {} @@ -34,6 +124,110 @@ bioyond_dispensing_station: title: process_order_finish_report参数 type: object type: UniLabJsonCommand + auto-project_order_report: + feedback: {} + goal: {} + goal_default: + order_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + order_id: + type: string + required: + - order_id + type: object + result: {} + required: + - goal + title: project_order_report参数 + type: object + type: UniLabJsonCommand + auto-query_resource_by_name: + feedback: {} + goal: {} + goal_default: + material_name: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + material_name: + type: string + required: + - material_name + type: object + result: {} + required: + - goal + title: query_resource_by_name参数 + type: object + type: UniLabJsonCommand + auto-transfer_materials_to_reaction_station: + feedback: {} + goal: {} + goal_default: + target_device_id: null + transfer_groups: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + target_device_id: + type: string + transfer_groups: + type: array + required: + - target_device_id + - transfer_groups + type: object + result: {} + required: + - goal + title: transfer_materials_to_reaction_station参数 + type: object + type: UniLabJsonCommand + auto-workflow_sample_locations: + feedback: {} + goal: {} + goal_default: + workflow_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_id: + type: string + required: + - workflow_id + type: object + result: {} + required: + - goal + title: workflow_sample_locations参数 + type: object + type: UniLabJsonCommand batch_create_90_10_vial_feeding_tasks: feedback: {} goal: diff --git a/unilabos/registry/devices/camera.yaml b/unilabos/registry/devices/camera.yaml index b64b342c..fe1aef28 100644 --- a/unilabos/registry/devices/camera.yaml +++ b/unilabos/registry/devices/camera.yaml @@ -61,6 +61,9 @@ camera: device_id: default: video_publisher type: string + device_uuid: + default: '' + type: string period: default: 0.1 type: number diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b9dd94a2..d38c43a3 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -4497,9 +4497,6 @@ liquid_handler: simulator: default: false type: boolean - total_height: - default: 310 - type: number required: - backend - deck diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 84bcf4a9..b7d10a60 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -4,6 +4,215 @@ reaction_station.bioyond: - reaction_station_bioyond class: action_value_mappings: + auto-create_order: + feedback: {} + goal: {} + goal_default: + json_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + json_str: + type: string + required: + - json_str + type: object + result: {} + required: + - goal + title: create_order参数 + type: object + type: UniLabJsonCommand + auto-hard_delete_merged_workflows: + feedback: {} + goal: {} + goal_default: + workflow_ids: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_ids: + items: + type: string + type: array + required: + - workflow_ids + type: object + result: {} + required: + - goal + title: hard_delete_merged_workflows参数 + type: object + type: UniLabJsonCommand + auto-merge_workflow_with_parameters: + feedback: {} + goal: {} + goal_default: + json_str: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + json_str: + type: string + required: + - json_str + type: object + result: {} + required: + - goal + title: merge_workflow_with_parameters参数 + type: object + type: UniLabJsonCommand + auto-process_temperature_cutoff_report: + feedback: {} + goal: {} + goal_default: + report_request: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + report_request: + type: string + required: + - report_request + type: object + result: {} + required: + - goal + title: process_temperature_cutoff_report参数 + type: object + type: UniLabJsonCommand + auto-process_web_workflows: + feedback: {} + goal: {} + goal_default: + web_workflow_json: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + web_workflow_json: + type: string + required: + - web_workflow_json + type: object + result: {} + required: + - goal + title: process_web_workflows参数 + type: object + type: UniLabJsonCommand + auto-skip_titration_steps: + feedback: {} + goal: {} + goal_default: + preintake_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + preintake_id: + type: string + required: + - preintake_id + type: object + result: {} + required: + - goal + title: skip_titration_steps参数 + type: object + type: UniLabJsonCommand + auto-wait_for_multiple_orders_and_get_reports: + feedback: {} + goal: {} + goal_default: + batch_create_result: null + check_interval: 10 + timeout: 7200 + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + batch_create_result: + type: string + check_interval: + default: 10 + type: integer + timeout: + default: 7200 + type: integer + required: [] + type: object + result: {} + required: + - goal + title: wait_for_multiple_orders_and_get_reports参数 + type: object + type: UniLabJsonCommand + auto-workflow_step_query: + feedback: {} + goal: {} + goal_default: + workflow_id: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + workflow_id: + type: string + required: + - workflow_id + type: object + result: {} + required: + - goal + title: workflow_step_query参数 + type: object + type: UniLabJsonCommand drip_back: feedback: {} goal: @@ -524,19 +733,7 @@ reaction_station.bioyond: module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation protocol_type: [] status_types: - all_workflows: dict - average_viscosity: float - bioyond_status: dict - force: float - in_temperature: float - out_temperature: float - pt100_temperature: float - sensor_average_temperature: float - setting_temperature: float - speed: float - target_temperature: float - viscosity: float - workstation_status: dict + workflow_sequence: String type: python config_info: [] description: Bioyond反应站 @@ -548,21 +745,19 @@ reaction_station.bioyond: config: type: object deck: - type: object + type: string + protocol_type: + type: string required: [] type: object data: properties: - all_workflows: - type: object - bioyond_status: - type: object - workstation_status: - type: object + workflow_sequence: + items: + type: string + type: array required: - - bioyond_status - - all_workflows - - workstation_status + - workflow_sequence type: object version: 1.0.0 reaction_station.reactor: @@ -570,19 +765,34 @@ reaction_station.reactor: - reactor - reaction_station_bioyond class: - action_value_mappings: {} + action_value_mappings: + auto-update_metrics: + feedback: {} + goal: {} + goal_default: + payload: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + payload: + type: object + required: + - payload + type: object + result: {} + required: + - goal + title: update_metrics参数 + type: object + type: UniLabJsonCommand module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor - status_types: - average_viscosity: float - force: float - in_temperature: float - out_temperature: float - pt100_temperature: float - sensor_average_temperature: float - setting_temperature: float - speed: float - target_temperature: float - viscosity: float + status_types: {} type: python config_info: [] description: 反应站子设备-反应器 @@ -593,30 +803,14 @@ reaction_station.reactor: properties: config: type: object + deck: + type: string + protocol_type: + type: string required: [] type: object data: - properties: - average_viscosity: - type: number - force: - type: number - in_temperature: - type: number - out_temperature: - type: number - pt100_temperature: - type: number - sensor_average_temperature: - type: number - setting_temperature: - type: number - speed: - type: number - target_temperature: - type: number - viscosity: - type: number + properties: {} required: [] type: object version: 1.0.0 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 4fee8042..98ea7d70 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -453,7 +453,7 @@ class Registry: return status_schema def _generate_unilab_json_command_schema( - self, method_args: List[Dict[str, Any]], method_name: str + self, method_args: List[Dict[str, Any]], method_name: str, return_annotation: Any = None ) -> Dict[str, Any]: """ 根据UniLabJsonCommand方法信息生成JSON Schema,暂不支持嵌套类型 @@ -461,6 +461,7 @@ class Registry: Args: method_args: 方法信息字典,包含args等 method_name: 方法名称 + return_annotation: 返回类型注解,用于生成result schema(仅支持TypedDict) Returns: JSON Schema格式的参数schema @@ -489,14 +490,68 @@ class Registry: if param_required: schema["required"].append(param_name) + # 生成result schema(仅当return_annotation是TypedDict时) + result_schema = {} + if return_annotation is not None and self._is_typed_dict(return_annotation): + result_schema = self._generate_typed_dict_result_schema(return_annotation) + return { "title": f"{method_name}参数", "description": f"", "type": "object", - "properties": {"goal": schema, "feedback": {}, "result": {}}, + "properties": {"goal": schema, "feedback": {}, "result": result_schema}, "required": ["goal"], } + def _is_typed_dict(self, annotation: Any) -> bool: + """ + 检查类型注解是否是TypedDict + + Args: + annotation: 类型注解对象 + + Returns: + 是否为TypedDict + """ + if annotation is None or annotation == inspect.Parameter.empty: + return False + + # 使用 typing_extensions.is_typeddict 进行检查(Python < 3.12 兼容) + try: + from typing_extensions import is_typeddict + + return is_typeddict(annotation) + except ImportError: + # 回退方案:检查 TypedDict 特有的属性 + if isinstance(annotation, type): + return hasattr(annotation, "__required_keys__") and hasattr(annotation, "__optional_keys__") + return False + + def _generate_typed_dict_result_schema(self, return_annotation: Any) -> Dict[str, Any]: + """ + 根据TypedDict类型生成result的JSON Schema + + Args: + return_annotation: TypedDict类型注解 + + Returns: + JSON Schema格式的result schema + """ + if not self._is_typed_dict(return_annotation): + return {} + + try: + from msgcenterpy.instances.typed_dict_instance import TypedDictMessageInstance + + result_schema = TypedDictMessageInstance.get_json_schema_from_typed_dict(return_annotation) + return result_schema + except ImportError: + logger.warning("[UniLab Registry] msgcenterpy未安装,无法生成TypedDict的result schema") + return {} + except Exception as e: + logger.warning(f"[UniLab Registry] 生成TypedDict result schema失败: {e}") + return {} + def _add_builtin_actions(self, device_config: Dict[str, Any], device_id: str): """ 为设备配置添加内置的执行驱动命令动作 @@ -577,9 +632,15 @@ class Registry: if "init_param_schema" not in device_config: device_config["init_param_schema"] = {} if "class" in device_config: - if "status_types" not in device_config["class"] or device_config["class"]["status_types"] is None: + if ( + "status_types" not in device_config["class"] + or device_config["class"]["status_types"] is None + ): device_config["class"]["status_types"] = {} - if "action_value_mappings" not in device_config["class"] or device_config["class"]["action_value_mappings"] is None: + if ( + "action_value_mappings" not in device_config["class"] + or device_config["class"]["action_value_mappings"] is None + ): device_config["class"]["action_value_mappings"] = {} enhanced_info = {} if complete_registry: @@ -631,7 +692,9 @@ class Registry: "goal": {}, "feedback": {}, "result": {}, - "schema": self._generate_unilab_json_command_schema(v["args"], k), + "schema": self._generate_unilab_json_command_schema( + v["args"], k, v.get("return_annotation") + ), "goal_default": {i["name"]: i["default"] for i in v["args"]}, "handles": [], "placeholder_keys": { diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 71d8c377..ac9930f6 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -5,7 +5,7 @@ import threading import time import traceback import uuid -from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, Union +from typing import TYPE_CHECKING, Optional, Dict, Any, List, ClassVar, Set, TypedDict, Union from action_msgs.msg import GoalStatus from geometry_msgs.msg import Point @@ -38,6 +38,7 @@ from unilabos.ros.msgs.message_converter import ( from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.presets.controller_node import ControllerNode from unilabos.ros.nodes.resource_tracker import ( + ResourceDict, ResourceDictInstance, ResourceTreeSet, ResourceTreeInstance, @@ -48,7 +49,7 @@ from unilabos.utils.type_check import serialize_result_info from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot if TYPE_CHECKING: - from unilabos.app.ws_client import QueueItem, WSResourceChatData + from unilabos.app.ws_client import QueueItem @dataclass @@ -56,6 +57,11 @@ class DeviceActionStatus: job_ids: Dict[str, float] = field(default_factory=dict) +class TestResourceReturn(TypedDict): + resources: List[List[ResourceDict]] + devices: List[DeviceSlot] + + class HostNode(BaseROS2DeviceNode): """ 主机节点类,负责管理设备、资源和控制器 @@ -1346,7 +1352,7 @@ class HostNode(BaseROS2DeviceNode): def test_resource( self, resource: ResourceSlot, resources: List[ResourceSlot], device: DeviceSlot, devices: List[DeviceSlot] - ): + ) -> TestResourceReturn: return { "resources": ResourceTreeSet.from_plr_resources([resource, *resources]).dump(), "devices": [device, *devices], diff --git a/unilabos/utils/environment_check.py b/unilabos/utils/environment_check.py index 66293e0e..3963b9ef 100644 --- a/unilabos/utils/environment_check.py +++ b/unilabos/utils/environment_check.py @@ -5,6 +5,7 @@ import argparse import importlib +import locale import subprocess import sys @@ -22,13 +23,33 @@ class EnvironmentChecker: "websockets": "websockets", "msgcenterpy": "msgcenterpy", "opentrons_shared_data": "opentrons_shared_data", + "typing_extensions": "typing_extensions", } # 特殊安装包(需要特殊处理的包) self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"} + # 包版本要求(包名: 最低版本) + self.version_requirements = { + "msgcenterpy": "0.1.5", # msgcenterpy 最低版本要求 + } + self.missing_packages = [] self.failed_installs = [] + self.packages_need_upgrade = [] + + # 检测系统语言 + self.is_chinese = self._is_chinese_locale() + + def _is_chinese_locale(self) -> bool: + """检测系统是否为中文环境""" + try: + lang = locale.getdefaultlocale()[0] + if lang and ("zh" in lang.lower() or "chinese" in lang.lower()): + return True + except Exception: + pass + return False def check_package_installed(self, package_name: str) -> bool: """检查包是否已安装""" @@ -38,31 +59,74 @@ class EnvironmentChecker: except ImportError: return False - def install_package(self, package_name: str, pip_name: str) -> bool: + def get_package_version(self, package_name: str) -> str | None: + """获取已安装包的版本""" + try: + module = importlib.import_module(package_name) + return getattr(module, "__version__", None) + except (ImportError, AttributeError): + return None + + def compare_version(self, current: str, required: str) -> bool: + """ + 比较版本号 + Returns: + True: current >= required + False: current < required + """ + try: + current_parts = [int(x) for x in current.split(".")] + required_parts = [int(x) for x in required.split(".")] + + # 补齐长度 + max_len = max(len(current_parts), len(required_parts)) + current_parts.extend([0] * (max_len - len(current_parts))) + required_parts.extend([0] * (max_len - len(required_parts))) + + return current_parts >= required_parts + except Exception: + return True # 如果无法比较,假设版本满足要求 + + def install_package(self, package_name: str, pip_name: str, upgrade: bool = False) -> bool: """安装包""" try: - print_status(f"正在安装 {package_name} ({pip_name})...", "info") + action = "升级" if upgrade else "安装" + print_status(f"正在{action} {package_name} ({pip_name})...", "info") # 构建安装命令 - cmd = [sys.executable, "-m", "pip", "install", pip_name] + cmd = [sys.executable, "-m", "pip", "install"] + + # 如果是升级操作,添加 --upgrade 参数 + if upgrade: + cmd.append("--upgrade") + + cmd.append(pip_name) + + # 如果是中文环境,使用清华镜像源 + if self.is_chinese: + cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"]) # 执行安装 result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) # 5分钟超时 if result.returncode == 0: - print_status(f"✓ {package_name} 安装成功", "success") + print_status(f"✓ {package_name} {action}成功", "success") return True else: - print_status(f"× {package_name} 安装失败: {result.stderr}", "error") + print_status(f"× {package_name} {action}失败: {result.stderr}", "error") return False except subprocess.TimeoutExpired: - print_status(f"× {package_name} 安装超时", "error") + print_status(f"× {package_name} {action}超时", "error") return False except Exception as e: - print_status(f"× {package_name} 安装异常: {str(e)}", "error") + print_status(f"× {package_name} {action}异常: {str(e)}", "error") return False + def upgrade_package(self, package_name: str, pip_name: str) -> bool: + """升级包""" + return self.install_package(package_name, pip_name, upgrade=True) + def check_all_packages(self) -> bool: """检查所有必需的包""" print_status("开始检查环境依赖...", "info") @@ -71,60 +135,116 @@ class EnvironmentChecker: for import_name, pip_name in self.required_packages.items(): if not self.check_package_installed(import_name): self.missing_packages.append((import_name, pip_name)) + else: + # 检查版本要求 + if import_name in self.version_requirements: + current_version = self.get_package_version(import_name) + required_version = self.version_requirements[import_name] + + if current_version: + if not self.compare_version(current_version, required_version): + print_status( + f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})", + "warning", + ) + self.packages_need_upgrade.append((import_name, pip_name)) # 检查特殊包 for package_name, install_url in self.special_packages.items(): if not self.check_package_installed(package_name): self.missing_packages.append((package_name, install_url)) - if not self.missing_packages: + all_ok = not self.missing_packages and not self.packages_need_upgrade + + if all_ok: print_status("✓ 所有依赖包检查完成,环境正常", "success") return True - print_status(f"发现 {len(self.missing_packages)} 个缺失的包", "warning") + if self.missing_packages: + print_status(f"发现 {len(self.missing_packages)} 个缺失的包", "warning") + if self.packages_need_upgrade: + print_status(f"发现 {len(self.packages_need_upgrade)} 个需要升级的包", "warning") + return False def install_missing_packages(self, auto_install: bool = True) -> bool: """安装缺失的包""" - if not self.missing_packages: + if not self.missing_packages and not self.packages_need_upgrade: return True if not auto_install: - print_status("缺失以下包:", "warning") - for import_name, pip_name in self.missing_packages: - print_status(f" - {import_name} (pip install {pip_name})", "warning") + if self.missing_packages: + print_status("缺失以下包:", "warning") + for import_name, pip_name in self.missing_packages: + print_status(f" - {import_name} (pip install {pip_name})", "warning") + if self.packages_need_upgrade: + print_status("需要升级以下包:", "warning") + for import_name, pip_name in self.packages_need_upgrade: + print_status(f" - {import_name} (pip install --upgrade {pip_name})", "warning") return False - print_status(f"开始自动安装 {len(self.missing_packages)} 个缺失的包...", "info") + # 安装缺失的包 + if self.missing_packages: + print_status(f"开始自动安装 {len(self.missing_packages)} 个缺失的包...", "info") - success_count = 0 - for import_name, pip_name in self.missing_packages: - if self.install_package(import_name, pip_name): - success_count += 1 - else: - self.failed_installs.append((import_name, pip_name)) + success_count = 0 + for import_name, pip_name in self.missing_packages: + if self.install_package(import_name, pip_name): + success_count += 1 + else: + self.failed_installs.append((import_name, pip_name)) + + print_status(f"✓ 成功安装 {success_count}/{len(self.missing_packages)} 个包", "success") + + # 升级需要更新的包 + if self.packages_need_upgrade: + print_status(f"开始自动升级 {len(self.packages_need_upgrade)} 个包...", "info") + + upgrade_success_count = 0 + for import_name, pip_name in self.packages_need_upgrade: + if self.upgrade_package(import_name, pip_name): + upgrade_success_count += 1 + else: + self.failed_installs.append((import_name, pip_name)) + + print_status(f"✓ 成功升级 {upgrade_success_count}/{len(self.packages_need_upgrade)} 个包", "success") if self.failed_installs: - print_status(f"有 {len(self.failed_installs)} 个包安装失败:", "error") + print_status(f"有 {len(self.failed_installs)} 个包操作失败:", "error") for import_name, pip_name in self.failed_installs: - print_status(f" - {import_name} (pip install {pip_name})", "error") + print_status(f" - {import_name} ({pip_name})", "error") return False - print_status(f"✓ 成功安装 {success_count} 个包", "success") return True def verify_installation(self) -> bool: """验证安装结果""" - if not self.missing_packages: + if not self.missing_packages and not self.packages_need_upgrade: return True print_status("验证安装结果...", "info") failed_verification = [] + + # 验证新安装的包 for import_name, pip_name in self.missing_packages: if not self.check_package_installed(import_name): failed_verification.append((import_name, pip_name)) + # 验证升级的包 + for import_name, pip_name in self.packages_need_upgrade: + if not self.check_package_installed(import_name): + failed_verification.append((import_name, pip_name)) + elif import_name in self.version_requirements: + current_version = self.get_package_version(import_name) + required_version = self.version_requirements[import_name] + if current_version and not self.compare_version(current_version, required_version): + failed_verification.append((import_name, pip_name)) + print_status( + f" {import_name} 版本仍然过低 (当前: {current_version}, 需要: >={required_version})", + "error", + ) + if failed_verification: print_status(f"有 {len(failed_verification)} 个包验证失败:", "error") for import_name, pip_name in failed_verification: diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index 4b873386..00fcd06b 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -239,8 +239,12 @@ class ImportManager: cls = get_class(class_path) class_name = cls.__name__ - result = {"class_name": class_name, "init_params": self._analyze_method_signature(cls.__init__)["args"], - "status_methods": {}, "action_methods": {}} + result = { + "class_name": class_name, + "init_params": self._analyze_method_signature(cls.__init__)["args"], + "status_methods": {}, + "action_methods": {}, + } # 分析类的所有成员 for name, method in cls.__dict__.items(): if name.startswith("_"): @@ -374,6 +378,7 @@ class ImportManager: "name": method.__name__, "args": args, "return_type": self._get_type_string(signature.return_annotation), + "return_annotation": signature.return_annotation, # 保留原始类型注解,用于TypedDict等特殊处理 "is_async": inspect.iscoroutinefunction(method), }