# -*- encoding=utf8 -*- """ Task dispatcher configuration. """ import json import os import re from typing import Any, Dict, List, Optional, Tuple try: import yaml except ImportError: # pragma: no cover - exercised only when dependency is absent. yaml = None BASE_DIR = os.path.dirname(os.path.abspath(__file__)) CONFIG_DIR = os.path.join(BASE_DIR, "config") CONFIG_PATH_ENV = os.environ.get("AUTOOL_DISPATCHER_CONFIG") CONFIG_PATH = CONFIG_PATH_ENV or os.path.join(CONFIG_DIR, "config.json") CONFIG_YAML_PATH = os.path.join(BASE_DIR, "config.yaml") CONFIG_EXAMPLE_PATH = os.path.join(CONFIG_DIR, "config.example.json") CONFIG_EXAMPLE_YAML_PATH = os.path.join(BASE_DIR, "config.example.yaml") CURRENT_ENV_PATH = os.path.join(CONFIG_DIR, "current_env.txt") LOCAL_CONFIG_PATH = os.path.join(CONFIG_DIR, "local.json") ENV_CONFIG_PATHS = { "prod": os.path.join(CONFIG_DIR, "prod.json"), "test": os.path.join(CONFIG_DIR, "test.json"), } ENV_ALIASES = { "": "prod", "prod": "prod", "production": "prod", "main": "prod", "master": "prod", "test": "test", "testing": "test", } def _load_config_payload(path: str, *, required: bool) -> Any: if not os.path.exists(path): if required: raise FileNotFoundError(f"config file not found: {path}") return {} with open(path, "r", encoding="utf-8") as handle: if path.endswith((".yaml", ".yml")): if yaml is None: raise ImportError("PyYAML is required to read YAML config files. Install: pip install PyYAML") payload = yaml.safe_load(handle) or {} else: payload = json.load(handle) return payload def _load_config_dict(path: str, *, required: bool) -> Dict[str, Any]: payload = _load_config_payload(path, required=required) if not isinstance(payload, dict): raise ValueError(f"config payload must be an object: {path}") return payload def _load_json_dict(path: str, *, required: bool) -> Dict[str, Any]: return _load_config_dict(path, required=required) def _merge_dict(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]: merged = dict(base) for key, value in override.items(): current = merged.get(key) if isinstance(current, dict) and isinstance(value, dict): merged[key] = _merge_dict(current, value) continue merged[key] = value return merged def _read_current_env() -> str: if not os.path.exists(CURRENT_ENV_PATH): return "prod" with open(CURRENT_ENV_PATH, "r", encoding="utf-8") as handle: return handle.read().strip().lower() def normalize_env_name(env_name: Optional[str] = None) -> str: raw = str("prod" if env_name is None else env_name).strip().lower() normalized = ENV_ALIASES.get(raw) if normalized: return normalized raise ValueError(f"unsupported config env: {env_name or raw}") def get_runtime_env_name() -> str: return CONFIG_ENV def _load_legacy_layered_config(env_name: Optional[str] = None) -> Dict[str, Any]: normalized_env = normalize_env_name(env_name) config = _load_config_dict(ENV_CONFIG_PATHS[normalized_env], required=True) config = _merge_dict(config, _load_config_dict(LOCAL_CONFIG_PATH, required=False)) return config # 嵌套小写 → 扁平大写键名映射 _NESTED_TO_FLAT_MAP = { ("environment", "name"): "CONFIG_ENV", ("instance_name",): "INSTANCE_NAME", ("redis", "host"): "REDIS_HOST", ("redis", "port"): "REDIS_PORT", ("redis", "db"): "REDIS_DB", ("redis", "channel_namespace"): "CHANNEL_NAMESPACE", ("redis", "max_connections"): "REDIS_MAX_CONNECTIONS_DISPATCHER", ("tasks", "max_retry_count"): "MAX_RETRY_COUNT", ("tasks", "csv_file"): "TASK_CSV_FILE", ("worker", "stale_timeout"): "WORKER_STALE_TIMEOUT", ("worker", "reinit_timeout"): "WORKER_REINIT_TIMEOUT", ("worker", "init_monitor_window"): "WORKER_INIT_MONITOR_WINDOW", ("worker", "action_max_parallel"): "WORKER_ACTION_MAX_PARALLEL", ("worker", "managed_only_can_dispatch"): "ONLY_MANAGED_WORKERS_CAN_DISPATCH", ("worker", "allowed_ip_patterns"): "WORKER_ALLOWED_IP_PATTERNS", ("ssh", "connect_timeout"): "SSH_CONNECT_TIMEOUT_SECONDS", ("ssh", "action_timeout"): "SSH_ACTION_TIMEOUT_SECONDS", ("ssh", "default_user"): "SSH_DEFAULT_USER", ("ssh", "default_password"): "SSH_DEFAULT_PASSWORD", ("ssh", "default_port"): "SSH_DEFAULT_PORT", ("ssh", "psexec_session_id"): "PSEXEC_DEFAULT_SESSION_ID", ("git", "repo_url"): "AUTOOL_GIT_REPO_URL", ("mumu", "manager_path"): "MUMU_MANAGER_PATH", ("mumu", "vm_index"): "MUMU_VM_INDEX", ("mumu", "max_vm_index"): "MUMU_MAX_VM_INDEX", ("mumu", "restart_settle_seconds"): "MUMU_RESTART_SETTLE_SECONDS", ("mumu", "recover_script_path"): "MUMU_RECOVER_SCRIPT_PATH", ("mumu", "recover_ahk_exe"): "MUMU_RECOVER_AHK_EXE", ("mumu", "clean_backup_path"): "CLEAN_BACKUP_PATH", ("mumu", "local_import_dir"): "LOCAL_IMPORT_DIR", ("mumu", "network_bridge_card"): "NET_BRIDGE_CARD", ("mumu", "adb_ip_offset"): "EMULATOR_ADB_IP_OFFSET", ("mumu", "bridge_ip_offset"): "MUMU_BRIDGE_IP_OFFSET", ("mumu", "network_script_path"): "MUMU_NETWORK_SCRIPT_PATH", ("mumu", "network_gateways"): "MUMU_NETWORK_GATEWAYS", ("share", "smb_user"): "SHARE_SMB_USER", ("share", "smb_password"): "SHARE_SMB_PASSWORD", ("logging", "file"): "LOG_FILE", ("logging", "max_bytes"): "LOG_MAX_BYTES", ("logging", "backup_count"): "LOG_BACKUP_COUNT", ("logging", "date_format"): "LOG_DATE_FORMAT", ("notifications", "wechat_tokens"): "WECHAT_TOKENS", ("notifications", "wecom_tokens"): "WECOM_TOKENS", ("alert", "window_seconds"): "ALERT_WINDOW_SECONDS", ("alert", "threshold"): "ALERT_THRESHOLD", ("alert", "cooldown_seconds"): "ALERT_COOLDOWN_SECONDS", ("monitoring", "status_push_interval"): "WORKER_STATUS_PUSH_INTERVAL", ("monitoring", "timezone"): "MONITORING_TIMEZONE", ("monitoring", "timeline_bucket_minutes"): "MONITORING_TIMELINE_BUCKET_MINUTES", ("pipeline", "run_option_keys"): "RUN_PIPELINE_OPTION_KEYS", ("pipeline", "default_options"): "RUN_PIPELINE_DEFAULT_OPTIONS", ("pipeline", "auto_reboot_recovery_delay"): "AUTO_REBOOT_RECOVERY_DELAY_SECONDS", ("pipeline", "legacy_end_worker_ips"): "RUNBATCH_LEGACY_END_WORKER_IPS", ("pipeline", "force_kill_images"): "RUNBATCH_FORCE_KILL_IMAGES", ("dashboard", "host"): "DASHBOARD_HOST", ("dashboard", "port"): "DASHBOARD_PORT", ("analytics", "traffic_root"): "ANALYTICS_TRAFFIC_ROOT", ("analytics", "traffic_root_block"): "ANALYTICS_TRAFFIC_ROOT_BLOCK", ("analytics", "traversal_root"): "ANALYTICS_TRAVERSAL_ROOT", ("analytics", "tpdpi_app_list_file"): "ANALYTICS_TPDPI_APP_LIST_FILE", ("analytics", "tpdpi_url_lib_file"): "ANALYTICS_TPDPI_URL_LIB_FILE", ("analytics", "artifact_wait_seconds"): "ANALYTICS_ARTIFACT_WAIT_SECONDS", ("analytics", "artifact_file_wait_seconds"): "ANALYTICS_ARTIFACT_FILE_WAIT_SECONDS", ("analytics", "job_poll_seconds"): "ANALYTICS_JOB_POLL_SECONDS", ("analytics", "model_traffic_threshold"): "MODEL_TRAFFIC_THRESHOLD", ("minio", "enabled"): "MINIO_ENABLED", ("minio", "endpoint"): "MINIO_ENDPOINT", ("minio", "access_key"): "MINIO_ACCESS_KEY", ("minio", "secret_key"): "MINIO_SECRET_KEY", ("minio", "bucket"): "MINIO_BUCKET", ("minio", "secure"): "MINIO_SECURE", ("apk", "download_mode"): "APK_DOWNLOAD_MODE", ("apk", "local_storage_dir"): "APK_LOCAL_STORAGE_DIR", ("apk", "smb_dir"): "APK_SMB_DIR", ("apk", "us_export_dir"): "APK_US_EXPORT_DIR", ("apk", "us_device_a_serial"): "APK_US_DEVICE_A_SERIAL", ("apk", "us_device_b_serial"): "APK_US_DEVICE_B_SERIAL", ("apk", "us_device_a_vm_index"): "APK_US_DEVICE_A_VM_INDEX", ("apk", "us_device_b_vm_index"): "APK_US_DEVICE_B_VM_INDEX", ("apk", "prefetch_poll_interval"): "APK_PREFETCH_POLL_INTERVAL", ("apk", "download_queue_interval"): "APK_DOWNLOAD_QUEUE_INTERVAL", ("apk", "minio_max_bytes"): "APK_MINIO_MAX_BYTES", ("apk", "emulator_cleanup_enabled"): "APK_EMULATOR_CLEANUP_ENABLED", ("task_routing_rules",): "TASK_ROUTING_RULES", ("worker_inventory",): "WORKER_INVENTORY", } def _is_nested_format(config: Dict[str, Any]) -> bool: """检测是否为嵌套小写格式:顶层有 environment 键且无 CONFIG_ENV""" return "environment" in config and "CONFIG_ENV" not in config def _dig(config: Dict[str, Any], *keys: str) -> Any: """从嵌套字典中按路径取值""" value = config for key in keys: if not isinstance(value, dict): return None value = value.get(key) return value def _normalize_nested_config(config: Dict[str, Any]) -> Dict[str, Any]: """将嵌套小写格式转为扁平大写格式,已是扁平格式则直接返回""" if not _is_nested_format(config): return config flat = {} for nested_keys, flat_key in _NESTED_TO_FLAT_MAP.items(): value = _dig(config, *nested_keys) if value is not None: flat[flat_key] = value return flat def load_dispatcher_config( env_name: Optional[str] = None, config_path: Optional[str] = None, ) -> Dict[str, Any]: if config_path: return _normalize_nested_config(_load_config_dict(os.path.abspath(config_path), required=True)) if env_name is not None: return _load_legacy_layered_config(env_name) if CONFIG_PATH_ENV: return _normalize_nested_config(_load_config_dict(os.path.abspath(CONFIG_PATH_ENV), required=True)) if os.path.exists(CONFIG_YAML_PATH): return _normalize_nested_config(_load_config_dict(CONFIG_YAML_PATH, required=True)) if os.path.exists(CONFIG_PATH): return _normalize_nested_config(_load_config_dict(CONFIG_PATH, required=True)) if os.path.exists(CONFIG_EXAMPLE_YAML_PATH): return _normalize_nested_config(_load_config_dict(CONFIG_EXAMPLE_YAML_PATH, required=True)) if os.path.exists(CONFIG_EXAMPLE_PATH): return _normalize_nested_config(_load_config_dict(CONFIG_EXAMPLE_PATH, required=True)) legacy_env = _read_current_env() return _load_legacy_layered_config(legacy_env) CONFIG = load_dispatcher_config() CONFIG_ENV = normalize_env_name(CONFIG.get("CONFIG_ENV") or CONFIG.get("INSTANCE_NAME") or "prod") # ==================== Instance ==================== INSTANCE_NAME = str(CONFIG["INSTANCE_NAME"]).strip() INSTANCE_DIR = os.path.join(BASE_DIR, "runtime", INSTANCE_NAME) REPORT_DIR = os.path.join(INSTANCE_DIR, "reports") # ==================== Redis ==================== REDIS_HOST = str(CONFIG["REDIS_HOST"]).strip() REDIS_PORT = int(CONFIG["REDIS_PORT"]) REDIS_DB = int(CONFIG["REDIS_DB"]) CHANNEL_NAMESPACE = str(CONFIG["CHANNEL_NAMESPACE"]).strip() REDIS_MAX_CONNECTIONS_DISPATCHER = int(CONFIG["REDIS_MAX_CONNECTIONS_DISPATCHER"]) # ==================== Tasks ==================== MAX_RETRY_COUNT = int(CONFIG["MAX_RETRY_COUNT"]) TASK_CSV_PATH = os.path.join(BASE_DIR, str(CONFIG["TASK_CSV_FILE"]).strip()) # ==================== Worker ==================== WORKER_STALE_TIMEOUT = int(CONFIG["WORKER_STALE_TIMEOUT"]) WORKER_REINIT_TIMEOUT = int(CONFIG["WORKER_REINIT_TIMEOUT"]) WORKER_INIT_MONITOR_WINDOW = int(CONFIG["WORKER_INIT_MONITOR_WINDOW"]) WORKER_ACTION_MAX_PARALLEL = int(CONFIG["WORKER_ACTION_MAX_PARALLEL"]) SSH_CONNECT_TIMEOUT_SECONDS = int(CONFIG["SSH_CONNECT_TIMEOUT_SECONDS"]) SSH_ACTION_TIMEOUT_SECONDS = int(CONFIG["SSH_ACTION_TIMEOUT_SECONDS"]) AUTOOL_GIT_REPO_URL = str(CONFIG["AUTOOL_GIT_REPO_URL"]).strip() MUMU_MANAGER_PATH = str(CONFIG["MUMU_MANAGER_PATH"]).strip() MUMU_VM_INDEX = int(CONFIG["MUMU_VM_INDEX"]) MUMU_MAX_VM_INDEX = int(CONFIG["MUMU_MAX_VM_INDEX"]) CLEAN_BACKUP_PATH = str(CONFIG["CLEAN_BACKUP_PATH"]).strip() LOCAL_IMPORT_DIR = str(CONFIG["LOCAL_IMPORT_DIR"]).strip() NET_BRIDGE_CARD = str(CONFIG["NET_BRIDGE_CARD"]).strip() EMULATOR_ADB_IP_OFFSET = int(CONFIG.get("EMULATOR_ADB_IP_OFFSET", 100)) MUMU_BRIDGE_IP_OFFSET = int(CONFIG.get("MUMU_BRIDGE_IP_OFFSET", 100)) MUMU_NETWORK_GATEWAYS = dict( CONFIG.get("MUMU_NETWORK_GATEWAYS") or {"192.168.1": "192.168.1.1", "192.168.2": "192.168.2.1"} ) WORKER_ALLOWED_IP_PATTERNS = tuple(str(item).strip() for item in CONFIG.get("WORKER_ALLOWED_IP_PATTERNS", []) if str(item).strip()) SSH_DEFAULT_USER = str(CONFIG["SSH_DEFAULT_USER"]).strip() SSH_DEFAULT_PASSWORD = str(CONFIG["SSH_DEFAULT_PASSWORD"]).strip() SSH_DEFAULT_PORT = int(CONFIG["SSH_DEFAULT_PORT"]) PSEXEC_DEFAULT_SESSION_ID = int(CONFIG["PSEXEC_DEFAULT_SESSION_ID"]) MUMU_RESTART_SETTLE_SECONDS = int(CONFIG.get("MUMU_RESTART_SETTLE_SECONDS", 15)) RUNBATCH_LEGACY_END_WORKER_IPS = frozenset( str(item).strip() for item in CONFIG.get("RUNBATCH_LEGACY_END_WORKER_IPS", ["192.168.1.51", "192.168.1.61", "192.168.2.101"]) if str(item).strip() ) RUNBATCH_FORCE_KILL_IMAGES = tuple( str(item).strip() for item in CONFIG.get("RUNBATCH_FORCE_KILL_IMAGES", ["OpenConsole.exe", "WindowsTerminal.exe", "conhost.exe", "powershell.exe"]) if str(item).strip() ) MUMU_RECOVER_SCRIPT_PATH = str(CONFIG.get("MUMU_RECOVER_SCRIPT_PATH", r"\\your-file-server\share\recover_mumu_image.cmd")).strip() MUMU_NETWORK_SCRIPT_PATH = str(CONFIG.get("MUMU_NETWORK_SCRIPT_PATH", r"\\your-file-server\share\recover_mumu_network.cmd")).strip() MUMU_RECOVER_AHK_EXE = str(CONFIG.get("MUMU_RECOVER_AHK_EXE", r"\\your-file-server\share\mumu_recover.exe")).strip() # ==================== Backward compatibility ==================== SHARE_SMB_USER = str(CONFIG.get("SHARE_SMB_USER") or "").strip() SHARE_SMB_PASSWORD = str(CONFIG.get("SHARE_SMB_PASSWORD") or "").strip() def get_repo_url(worker_ip: str) -> str: del worker_ip return AUTOOL_GIT_REPO_URL def get_clean_backup_path(worker_ip: str) -> str: del worker_ip return CLEAN_BACKUP_PATH def get_share_smb_target(worker_ip: str) -> str: del worker_ip path = CLEAN_BACKUP_PATH if not path.startswith("\\\\"): return "" parts = [part for part in path.split("\\") if part] if not parts: return "" return f"\\\\{parts[0]}" # ==================== Output files ==================== FAILED_TASKS_CSV = os.path.join(REPORT_DIR, "failed_tasks.csv") RETRY_TASKS_CSV = os.path.join(REPORT_DIR, "retry_tasks.csv") SUCCESS_TASKS_CSV = os.path.join(REPORT_DIR, "success_tasks.csv") FINAL_REPORT_CSV = os.path.join(REPORT_DIR, "final_report.csv") WORKER_REPORT_CSV = os.path.join(REPORT_DIR, "worker_report.csv") # ==================== Logging ==================== LOG_DIR = os.path.join(INSTANCE_DIR, "logs") LOG_FILE = str(CONFIG["LOG_FILE"]).strip() LOG_MAX_BYTES = int(CONFIG["LOG_MAX_BYTES"]) LOG_BACKUP_COUNT = int(CONFIG["LOG_BACKUP_COUNT"]) LOG_FORMAT = f"%(asctime)s [{INSTANCE_NAME}] [%(levelname)s] %(message)s" LOG_DATE_FORMAT = str(CONFIG["LOG_DATE_FORMAT"]).strip() # ==================== Notifications ==================== WECHAT_TOKENS = dict(CONFIG.get("WECHAT_TOKENS") or {}) WECOM_TOKENS = dict(CONFIG.get("WECOM_TOKENS") or {}) # ==================== Alert strategy ==================== ALERT_WINDOW_SECONDS = int(CONFIG["ALERT_WINDOW_SECONDS"]) ALERT_THRESHOLD = int(CONFIG["ALERT_THRESHOLD"]) ALERT_COOLDOWN_SECONDS = int(CONFIG["ALERT_COOLDOWN_SECONDS"]) # ==================== Status push ==================== WORKER_STATUS_PUSH_INTERVAL = int(CONFIG["WORKER_STATUS_PUSH_INTERVAL"]) RUN_PIPELINE_OPTION_KEYS = tuple(CONFIG.get("RUN_PIPELINE_OPTION_KEYS") or ()) RUN_PIPELINE_DEFAULT_OPTIONS = dict(CONFIG.get("RUN_PIPELINE_DEFAULT_OPTIONS") or {}) AUTO_REBOOT_RECOVERY_DELAY_SECONDS = int(CONFIG["AUTO_REBOOT_RECOVERY_DELAY_SECONDS"]) # ==================== Worker registry ==================== WORKER_INVENTORY_CONFIG_KEY = "WORKER_INVENTORY" WORKER_INVENTORY_PATH = CONFIG_PATH def _resolve_ssh_endpoint(ssh_target: str, ssh_port: int) -> Tuple[str, int]: """直连模式:解析 SSH 目标地址,验证合法性后直接返回""" host = str(ssh_target).split("@")[-1].strip() ip = extract_worker_ip(host) if ip != host: return host, ssh_port if WORKER_ALLOWED_IP_PATTERNS and not any(re.fullmatch(pattern, ip) for pattern in WORKER_ALLOWED_IP_PATTERNS): raise ValueError(f"worker ip is not allowed by WORKER_ALLOWED_IP_PATTERNS: {ssh_target}") return host, ssh_port def extract_worker_ip(value: Any) -> str: text = str(value or "").strip() match = re.search(r"(? str: return extract_worker_ip(ip_address) or extract_worker_ip(worker_id) or str(worker_id or "").strip() def _normalize_worker_entry(raw: Dict[str, Any]) -> Dict[str, Any]: ssh_target = str(raw["ssh_target"]).strip() worker_id = normalize_worker_id(raw.get("worker_id", ""), ssh_target) if not worker_id: raise ValueError("worker inventory entry missing worker_id") if not ssh_target: raise ValueError(f"worker inventory entry missing ssh_target: {worker_id}") ssh_user = str(raw.get("ssh_user") or SSH_DEFAULT_USER).strip() ssh_password = str(raw.get("ssh_password") or "").strip() ssh_port = int(raw.get("ssh_port") or SSH_DEFAULT_PORT) ssh_host, ssh_port = _resolve_ssh_endpoint(ssh_target, ssh_port) repo_dir = str(raw["repo_dir"]).strip() if not repo_dir: raise ValueError(f"worker inventory entry missing repo_dir: {worker_id}") python_exe = str(raw["python_exe"]).strip() if not python_exe: raise ValueError(f"worker inventory entry missing python_exe: {worker_id}") psexec_session_id = int(raw.get("psexec_session_id") or PSEXEC_DEFAULT_SESSION_ID) tags = [] seen = set() for tag in raw.get("tags", []) or []: candidate = str(tag).strip() if candidate and candidate not in seen: seen.add(candidate) tags.append(candidate) item = { "worker_id": worker_id, "ssh_target": ssh_target, "ssh_host": ssh_host, "ssh_port": ssh_port, "ssh_user": ssh_user, "repo_dir": repo_dir, "python_exe": python_exe, "psexec_session_id": psexec_session_id, "tags": tags, } if ssh_password: item["ssh_password"] = ssh_password return item def _worker_matches_env(worker: Dict[str, Any], env_name: str) -> bool: if env_name == "test": return "test" in worker.get("tags", []) return True def load_worker_inventory(path: Optional[str] = None, env_name: Optional[str] = None) -> List[Dict[str, Any]]: normalized_env = normalize_env_name(CONFIG_ENV if env_name is None else env_name) target_path = path if target_path: if not os.path.exists(target_path): return [] payload = _load_config_payload(target_path, required=True) if isinstance(payload, dict): payload = payload.get(WORKER_INVENTORY_CONFIG_KEY, []) else: payload = CONFIG.get(WORKER_INVENTORY_CONFIG_KEY, []) if not isinstance(payload, list): raise ValueError(f"worker inventory must be a list: {target_path}") normalized: List[Dict[str, Any]] = [] seen = set() for item in payload: if not isinstance(item, dict): raise ValueError(f"worker inventory entry must be an object: {item!r}") normalized_item = _normalize_worker_entry(item) worker_id = normalized_item["worker_id"] if worker_id in seen: raise ValueError(f"duplicate worker_id in worker inventory: {worker_id}") seen.add(worker_id) if not _worker_matches_env(normalized_item, normalized_env): continue normalized.append(normalized_item) return normalized WORKER_INVENTORY = load_worker_inventory() MANAGED_WORKER_IDS = [item["worker_id"] for item in WORKER_INVENTORY] ONLY_MANAGED_WORKERS_CAN_DISPATCH = bool(CONFIG.get("ONLY_MANAGED_WORKERS_CAN_DISPATCH", False)) # Exact-match routing rules for targeted dispatch. # Values must be worker_id strings; worker IDs are normalized to the worker IP. TASK_ROUTING_RULES = dict(CONFIG.get("TASK_ROUTING_RULES") or {"package_name": {}, "task_key": {}}) # ==================== Dashboard ==================== DASHBOARD_HOST = str(CONFIG["DASHBOARD_HOST"]).strip() DASHBOARD_PORT = int(CONFIG["DASHBOARD_PORT"]) WORKER_ONLINE_TIMEOUT_SECONDS = WORKER_STALE_TIMEOUT # ==================== Monitoring ==================== MONITORING_DB_PATH = os.path.join(INSTANCE_DIR, "monitoring.sqlite3") MONITORING_TIMEZONE = str(CONFIG["MONITORING_TIMEZONE"]).strip() MONITORING_TIMELINE_BUCKET_MINUTES = int(CONFIG["MONITORING_TIMELINE_BUCKET_MINUTES"]) # ==================== Analytics ==================== ANALYTICS_TRAFFIC_ROOT = str(CONFIG["ANALYTICS_TRAFFIC_ROOT"]).strip() ANALYTICS_TRAFFIC_ROOT_BLOCK = str(CONFIG["ANALYTICS_TRAFFIC_ROOT_BLOCK"]).strip() ANALYTICS_TRAVERSAL_ROOT = str(CONFIG["ANALYTICS_TRAVERSAL_ROOT"]).strip() ANALYTICS_TPDPI_APP_LIST = os.path.join( BASE_DIR, "calculate", str(CONFIG["ANALYTICS_TPDPI_APP_LIST_FILE"]).strip(), ) ANALYTICS_TPDPI_URL_LIB = os.path.join( BASE_DIR, "calculate", str(CONFIG["ANALYTICS_TPDPI_URL_LIB_FILE"]).strip(), ) ANALYTICS_ARTIFACT_WAIT_SECONDS = int(CONFIG["ANALYTICS_ARTIFACT_WAIT_SECONDS"]) ANALYTICS_ARTIFACT_FILE_WAIT_SECONDS = int(CONFIG["ANALYTICS_ARTIFACT_FILE_WAIT_SECONDS"]) ANALYTICS_JOB_POLL_SECONDS = int(CONFIG["ANALYTICS_JOB_POLL_SECONDS"]) MODEL_TRAFFIC_THRESHOLD = int(CONFIG["MODEL_TRAFFIC_THRESHOLD"]) def channel_name(name: str) -> str: return f"{CHANNEL_NAMESPACE}:{name}" if CHANNEL_NAMESPACE else name # ==================== MinIO / APK Cloud Storage ==================== # 功能开关:是否启用 Minio 上传下载功能 # - True: 使用 Minio 进行 APK 上传和分发(原团队内网模式) # - False: 禁用 Minio,使用直接下载模式(跨团队部署推荐) MINIO_ENABLED = bool(CONFIG.get("MINIO_ENABLED", True)) # APK 下载模式(当 MINIO_ENABLED=False 时生效) # - "direct": Worker 直接从下载链接获取 APK(跨团队部署推荐) # - "smb": 从 SMB 共享目录读取 APK(需要内网访问权限) APK_DOWNLOAD_MODE = str(CONFIG.get("APK_DOWNLOAD_MODE", "direct")).strip().lower() # Minio 连接配置(仅当 MINIO_ENABLED=True 时必需) MINIO_ENDPOINT = str(CONFIG.get("MINIO_ENDPOINT", "")).strip() MINIO_ACCESS_KEY = str(CONFIG.get("MINIO_ACCESS_KEY", "")).strip() MINIO_SECRET_KEY = str(CONFIG.get("MINIO_SECRET_KEY", "")).strip() MINIO_BUCKET = str(CONFIG.get("MINIO_BUCKET", "autool-apk")).strip() MINIO_SECURE = bool(CONFIG.get("MINIO_SECURE", False)) # APK 存储路径配置 APK_LOCAL_STORAGE_DIR = str(CONFIG.get("APK_LOCAL_STORAGE_DIR", "")).strip() APK_SMB_DIR = str(CONFIG.get("APK_SMB_DIR", "")).strip() APK_US_EXPORT_DIR = str(CONFIG.get("APK_US_EXPORT_DIR", "D:\\mumu_apks")).strip() # US 拓扑设备配置(仅当使用 US 拓扑模式时需要) APK_US_DEVICE_A_SERIAL = str(CONFIG.get("APK_US_DEVICE_A_SERIAL", "127.0.0.1:7555")).strip() APK_US_DEVICE_B_SERIAL = str(CONFIG.get("APK_US_DEVICE_B_SERIAL", "127.0.0.1:7556")).strip() APK_US_DEVICE_A_VM_INDEX = int(CONFIG.get("APK_US_DEVICE_A_VM_INDEX", 2)) APK_US_DEVICE_B_VM_INDEX = int(CONFIG.get("APK_US_DEVICE_B_VM_INDEX", 1)) # APK 管理配置 APK_PREFETCH_POLL_INTERVAL = int(CONFIG.get("APK_PREFETCH_POLL_INTERVAL", 120)) APK_DOWNLOAD_QUEUE_INTERVAL = int(CONFIG.get("APK_DOWNLOAD_QUEUE_INTERVAL", 60)) APK_MINIO_MAX_BYTES = int(CONFIG.get("APK_MINIO_MAX_BYTES", 107374182400)) # 100GB APK_EMULATOR_CLEANUP_ENABLED = bool(CONFIG.get("APK_EMULATOR_CLEANUP_ENABLED", True))