""" Windows 真实实现层 ================== 在 Windows 环境下,将 Android API 的存根行为替换为真实的系统调用。 使 Android-pyApi 从"存根库"变为"可用的生产力工具"。 覆盖能力: - 网络: ipconfig, netsh, socket, wmi - 蓝牙: PowerShell Bluetooth cmdlets - 传感器: wmi, psutil - 音频: pycaw / PowerShell - 剪贴板: pyperclip / win32clipboard - 电源/电池: psutil, wmi - 位置: IP 地理定位 (requests) - 通知: win10toast / plyer - 显示: pyautogui, screeninfo, win32api - UI自动化: pyautogui - 摄像头: opencv - 进程管理: psutil, subprocess - 文件: 原生 Python os - 媒体: ffmpeg/ffprobe """ from __future__ import annotations import os import sys import re import json import time import socket import shutil import subprocess import platform import hashlib from pathlib import Path from datetime import datetime from typing import Any, Dict, List, Optional, Tuple # ═══════════════════════════════════════════════════════════════ # 能力检测 # ═══════════════════════════════════════════════════════════════ IS_WINDOWS = platform.system() == "Windows" def _which(name: str) -> Optional[str]: return shutil.which(name) HAS_POWERSHELL = _which("powershell") is not None or _which("pwsh") is not None HAS_FFMPEG = _which("ffmpeg") is not None HAS_FFPROBE = _which("ffprobe") is not None try: import psutil HAS_PSUTIL = True except ImportError: HAS_PSUTIL = False try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: import cv2 HAS_CV2 = True except ImportError: HAS_CV2 = False try: import numpy as np HAS_NUMPY = True except ImportError: HAS_NUMPY = False try: from PIL import Image, ImageDraw, ImageGrab HAS_PIL = True except ImportError: HAS_PIL = False try: import pyautogui HAS_PYAUTOGUI = True except ImportError: HAS_PYAUTOGUI = False try: import pyperclip HAS_PYPERCLIP = True except ImportError: HAS_PYPERCLIP = False try: from win10toast import ToastNotifier HAS_WIN10TOAST = True except ImportError: HAS_WIN10TOAST = False try: import wmi HAS_WMI = True except ImportError: HAS_WMI = False try: import win32api import win32con import win32gui HAS_WIN32 = True except ImportError: HAS_WIN32 = False try: from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from comtypes import CLSCTX_ALL HAS_PYCAW = True except ImportError: HAS_PYCAW = False try: from screeninfo import get_monitors HAS_SCREENINFO = True except ImportError: HAS_SCREENINFO = False def _run(cmd: List[str] or str, timeout: int = 10, capture: bool = True) -> Tuple[int, str, str]: """执行命令,返回 (exit_code, stdout, stderr)""" try: if isinstance(cmd, str): result = subprocess.run(cmd, shell=True, capture_output=capture, text=True, timeout=timeout, encoding='utf-8', errors='replace') else: result = subprocess.run(cmd, capture_output=capture, text=True, timeout=timeout, encoding='utf-8', errors='replace') return result.returncode, result.stdout.strip(), result.stderr.strip() except subprocess.TimeoutExpired: return -1, "", "timeout" except Exception as e: return -1, "", str(e) def _run_ps(script: str, timeout: int = 10) -> Tuple[int, str, str]: """执行 PowerShell 脚本""" ps_exe = "pwsh" if _which("pwsh") else "powershell" cmd = [ps_exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script] return _run(cmd, timeout=timeout) def _read_file(path: str) -> Optional[str]: try: with open(path, "r", encoding='utf-8', errors='replace') as f: return f.read().strip() except Exception: return None def _read_file_int(path: str) -> Optional[int]: val = _read_file(path) if val is not None: try: return int(val) except ValueError: return None return None # ═══════════════════════════════════════════════════════════════ # 网络 — 真实实现 # ═══════════════════════════════════════════════════════════════ class WindowsNetwork: """真实网络信息 — 通过 ipconfig, netsh, socket""" @staticmethod def get_interfaces() -> List[dict]: """获取网络接口列表""" interfaces = [] # 使用 PowerShell 获取网络适配器信息 code, out, _ = _run_ps( "Get-NetAdapter | Select-Object Name, Status, MacAddress, InterfaceDescription | ConvertTo-Json" ) if code == 0 and out: try: data = json.loads(out) if isinstance(data, dict): data = [data] for iface in data: interfaces.append({ "ifname": iface.get("Name", ""), "operstate": iface.get("Status", ""), "address": iface.get("MacAddress", ""), "description": iface.get("InterfaceDescription", ""), }) return interfaces except json.JSONDecodeError: pass # 回退到 ipconfig code, out, _ = _run("ipconfig /all") if code == 0: current = {} for line in out.split("\n"): line = line.strip() if "adapter" in line.lower() and ":" in line: if current: interfaces.append(current) current = {"ifname": line.split(":")[0].strip(), "operstate": "up", "address": ""} elif "Physical Address" in line or "物理地址" in line: current["address"] = line.split(":")[-1].strip() if current: interfaces.append(current) return interfaces @staticmethod def get_ip_address(interface: str = None) -> str: """获取IP地址""" try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: pass try: return socket.gethostbyname(socket.gethostname()) except Exception: return "127.0.0.1" @staticmethod def get_wifi_info() -> dict: """获取WiFi信息""" code, out, _ = _run("netsh wlan show interfaces") if code == 0: info = {"ssid": "", "signal": 0, "status": "disconnected"} for line in out.split("\n"): line = line.strip() if "SSID" in line and "BSSID" not in line: parts = line.split(":", 1) if len(parts) >= 2: info["ssid"] = parts[1].strip() elif "Signal" in line: parts = line.split(":", 1) if len(parts) >= 2: signal_str = parts[1].strip().rstrip("%") try: info["signal"] = int(signal_str) except ValueError: pass elif "State" in line: parts = line.split(":", 1) if len(parts) >= 2: info["status"] = parts[1].strip().lower() return info return {"ssid": "", "signal": 0, "status": "unknown"} @staticmethod def get_connectivity() -> bool: """检测网络连通性""" for host, port in [("8.8.8.8", 53), ("1.1.1.1", 53), ("114.114.114.114", 53)]: try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(3) s.connect((host, port)) s.close() return True except Exception: continue if HAS_REQUESTS: try: requests.get("http://www.baidu.com", timeout=3) return True except Exception: pass return False @staticmethod def get_dns_servers() -> List[str]: """获取DNS服务器""" code, out, _ = _run_ps( "Get-DnsClientServerAddress | Select-Object -ExpandProperty ServerAddresses" ) if code == 0 and out: return [s.strip() for s in out.split("\n") if s.strip()] # 回退到 ipconfig code, out, _ = _run("ipconfig /all") if code == 0: dns = [] for line in out.split("\n"): if "DNS" in line and ":" in line: parts = line.split(":", 1) if len(parts) >= 2: addr = parts[1].strip() if addr and not addr.startswith("("): dns.append(addr) return dns return [] @staticmethod def get_gateway() -> str: """获取默认网关""" code, out, _ = _run("ipconfig") if code == 0: for line in out.split("\n"): if ("Default Gateway" in line or "默认网关" in line) and ":" in line: parts = line.split(":", 1) if len(parts) >= 2: gw = parts[1].strip() if gw and gw != "0.0.0.0": return gw return "" @staticmethod def get_mac_address(interface: str = None) -> str: """获取MAC地址""" code, out, _ = _run("getmac /fo csv /v") if code == 0: lines = out.split("\n") for line in lines[1:]: # 跳过标题行 parts = line.split(",") if len(parts) >= 3: mac = parts[2].strip('"').replace("-", ":") if mac and mac != "N/A": return mac return "" @staticmethod def get_rx_tx_bytes(interface: str = None) -> dict: """获取网络收发字节数""" if HAS_PSUTIL: counters = psutil.net_io_counters(pernic=True) if interface and interface in counters: c = counters[interface] return {"rx_bytes": c.bytes_recv, "tx_bytes": c.bytes_sent, "interface": interface} elif counters: # 取第一个非回环接口 for name, c in counters.items(): if name.lower() != "lo": return {"rx_bytes": c.bytes_recv, "tx_bytes": c.bytes_sent, "interface": name} return {"rx_bytes": 0, "tx_bytes": 0, "interface": ""} # ═══════════════════════════════════════════════════════════════ # 蓝牙 — 真实实现 (PowerShell) # ═══════════════════════════════════════════════════════════════ class WindowsBluetooth: """真实蓝牙操作 — 通过 PowerShell Bluetooth cmdlets""" STATE_OFF = 10 STATE_TURNING_ON = 11 STATE_ON = 12 STATE_TURNING_OFF = 13 @staticmethod def get_state() -> int: code, out, _ = _run_ps( "Get-PnpDevice -Class Bluetooth | Where-Object {$_.FriendlyName -like '*Radio*' -or $_.FriendlyName -like '*Adapter*'} | Select-Object Status" ) if code == 0 and "OK" in out: return WindowsBluetooth.STATE_ON return WindowsBluetooth.STATE_OFF @staticmethod def get_address() -> Optional[str]: code, out, _ = _run_ps( "Get-PnpDevice -Class Bluetooth -Status OK | Select-Object InstanceId | ConvertTo-Json" ) if code == 0 and out: try: data = json.loads(out) if isinstance(data, dict): data = [data] for dev in data: instance_id = dev.get("InstanceId", "") # 从 InstanceId 提取 MAC 地址 mac_match = re.search(r'([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}', instance_id) if mac_match: return mac_match.group().replace("-", ":") except json.JSONDecodeError: pass return None @staticmethod def get_name() -> Optional[str]: code, out, _ = _run_ps( "Get-PnpDevice -Class Bluetooth -Status OK | Where-Object {$_.FriendlyName -like '*Radio*' -or $_.FriendlyName -like '*Adapter*'} | Select-Object FriendlyName | ConvertTo-Json" ) if code == 0 and out: try: data = json.loads(out) if isinstance(data, dict): return data.get("FriendlyName") elif isinstance(data, list) and data: return data[0].get("FriendlyName") except json.JSONDecodeError: pass return None @staticmethod def enable() -> bool: code, _, err = _run_ps( "Get-PnpDevice -Class Bluetooth | Enable-PnpDevice -Confirm:$false" ) return code == 0 @staticmethod def disable() -> bool: code, _, err = _run_ps( "Get-PnpDevice -Class Bluetooth | Disable-PnpDevice -Confirm:$false" ) return code == 0 @staticmethod def get_bonded_devices() -> List[dict]: code, out, _ = _run_ps( "Get-PnpDevice -Class Bluetooth -Status OK | Select-Object FriendlyName, InstanceId | ConvertTo-Json" ) devices = [] if code == 0 and out: try: data = json.loads(out) if isinstance(data, dict): data = [data] for dev in data: name = dev.get("FriendlyName", "") if name and "Enumerator" not in name: devices.append({ "address": "", "name": name, "bonded": True, }) except json.JSONDecodeError: pass return devices @staticmethod def scan(timeout: int = 10) -> List[dict]: """扫描蓝牙设备 (Windows 限制,只能列出已配对设备)""" return WindowsBluetooth.get_bonded_devices() @staticmethod def pair(address: str) -> bool: # Windows 需要通过设置界面配对 code, _, _ = _run_ps("Start-Process ms-settings:bluetooth") return code == 0 @staticmethod def connect(address: str) -> bool: return False # Windows 无命令行直接连接方式 @staticmethod def disconnect(address: str) -> bool: return False @staticmethod def unpair(address: str) -> bool: return False # ═══════════════════════════════════════════════════════════════ # 传感器 — 真实实现 (wmi, psutil) # ═══════════════════════════════════════════════════════════════ class WindowsSensors: """真实传感器数据 — 通过 WMI 和 psutil""" TYPE_ACCELEROMETER = 1 TYPE_GYROSCOPE = 4 TYPE_LIGHT = 5 TYPE_PRESSURE = 6 TYPE_PROXIMITY = 8 TYPE_TEMPERATURE = 7 TYPE_RELATIVE_HUMIDITY = 12 TYPE_AMBIENT_TEMPERATURE = 13 @staticmethod def get_temperatures() -> List[dict]: """获取温度传感器读数""" temps = [] # 通过 WMI 获取 CPU 温度 if HAS_WMI: try: w = wmi.WMI(namespace="root\\wmi") for sensor in w.MSAcpi_ThermalZoneTemperature(): temp_celsius = (sensor.CurrentTemperature / 10.0) - 273.15 temps.append({ "name": sensor.InstanceName, "type": "thermal_zone", "value": temp_celsius, "unit": "°C", }) except Exception: pass # 通过 psutil 获取 (如果支持) if HAS_PSUTIL: try: if hasattr(psutil, "sensors_temperatures"): for name, entries in psutil.sensors_temperatures().items(): for entry in entries: temps.append({ "name": f"{name}/{entry.label or 'unknown'}", "type": name, "value": entry.current, "unit": "°C", }) except Exception: pass return temps @staticmethod def get_fan_speeds() -> List[dict]: """获取风扇转速""" fans = [] if HAS_PSUTIL: try: if hasattr(psutil, "sensors_fans"): for name, entries in psutil.sensors_fans().items(): for entry in entries: fans.append({ "name": f"{name}/{entry.label or 'unknown'}", "rpm": entry.current, }) except Exception: pass return fans @staticmethod def get_cpu_frequency() -> List[dict]: """获取CPU频率""" freqs = [] if HAS_PSUTIL: try: cpu_freq = psutil.cpu_freq(percpu=True) if cpu_freq: for i, freq in enumerate(cpu_freq): freqs.append({ "cpu": i, "freq_mhz": freq.current, }) else: # 单个频率值 freq = psutil.cpu_freq() if freq: freqs.append({ "cpu": 0, "freq_mhz": freq.current, }) except Exception: pass return freqs @staticmethod def get_light_level() -> Optional[float]: """获取环境光传感器 (Windows 通常无直接访问)""" return None @staticmethod def get_all_sensors() -> dict: """获取所有传感器数据""" return { "temperatures": WindowsSensors.get_temperatures(), "fans": WindowsSensors.get_fan_speeds(), "cpu_freq": WindowsSensors.get_cpu_frequency(), "light": WindowsSensors.get_light_level(), } # ═══════════════════════════════════════════════════════════════ # 音频 — 真实实现 (pycaw / PowerShell) # ═══════════════════════════════════════════════════════════════ class WindowsAudio: """真实音频控制 — 通过 pycaw 或 PowerShell""" @staticmethod def _get_pycaw_volume(): """获取 pycaw 音量接口""" if not HAS_PYCAW: return None try: devices = AudioUtilities.GetSpeakers() interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) return interface.QueryInterface(IAudioEndpointVolume) except Exception: return None @staticmethod def get_volume() -> int: """获取当前音量 (0-100)""" # 方法1: pycaw vol = WindowsAudio._get_pycaw_volume() if vol: try: return int(vol.GetMasterVolumeLevelScalar() * 100) except Exception: pass # 方法2: PowerShell code, out, _ = _run_ps( "(Get-AudioDevice -PlaybackVolume) 2>$null; if($?) {$out} else {(Get-WmiObject -Class Win32_SoundDevice | Select-Object -First 1).Volume}" ) if code == 0 and out: try: return int(out.strip()) except ValueError: pass return 50 # 默认值 @staticmethod def set_volume(volume: int) -> bool: """设置音量 (0-100)""" volume = max(0, min(100, volume)) # 方法1: pycaw vol = WindowsAudio._get_pycaw_volume() if vol: try: vol.SetMasterVolumeLevelScalar(volume / 100.0, None) return True except Exception: pass # 方法2: PowerShell code, _, _ = _run_ps(f"(Get-AudioDevice -PlaybackVolume {volume}) 2>$null") return code == 0 @staticmethod def get_mute() -> bool: """获取静音状态""" vol = WindowsAudio._get_pycaw_volume() if vol: try: return vol.GetMute() except Exception: pass return False @staticmethod def set_mute(mute: bool) -> bool: """设置静音""" vol = WindowsAudio._get_pycaw_volume() if vol: try: vol.SetMute(mute, None) return True except Exception: pass return False @staticmethod def get_audio_devices() -> List[dict]: """获取音频设备列表""" devices = [] code, out, _ = _run_ps( "Get-AudioDevice -List 2>$null; if(-not $?) { Get-WmiObject Win32_SoundDevice | Select-Object Name, Status }" ) if code == 0 and out: for line in out.split("\n"): if line.strip(): devices.append({"name": line.strip(), "type": "windows"}) return devices # ═══════════════════════════════════════════════════════════════ # 剪贴板 — 真实实现 (pyperclip) # ═══════════════════════════════════════════════════════════════ class WindowsClipboard: """真实剪贴板 — 通过 pyperclip 或 tkinter""" @staticmethod def read() -> str: if HAS_PYPERCLIP: try: return pyperclip.paste() except Exception: pass # 回退到 tkinter try: import tkinter root = tkinter.Tk() root.withdraw() text = root.clipboard_get() root.destroy() return text except Exception: return "" @staticmethod def write(text: str) -> bool: if HAS_PYPERCLIP: try: pyperclip.copy(text) return True except Exception: pass # 回退到 tkinter try: import tkinter root = tkinter.Tk() root.withdraw() root.clipboard_clear() root.clipboard_append(text) root.update() root.destroy() return True except Exception: return False # ═══════════════════════════════════════════════════════════════ # 电源/电池 — 真实实现 (psutil) # ═══════════════════════════════════════════════════════════════ class WindowsPower: """真实电源/电池信息""" BATTERY_STATUS_UNKNOWN = 1 BATTERY_STATUS_CHARGING = 2 BATTERY_STATUS_DISCHARGING = 3 BATTERY_STATUS_NOT_CHARGING = 4 BATTERY_STATUS_FULL = 5 @staticmethod def get_battery_info() -> dict: """获取电池信息""" info = {"present": False, "level": -1, "status": "unknown", "temperature": 0, "voltage": 0, "technology": ""} if HAS_PSUTIL: try: battery = psutil.sensors_battery() if battery: info["present"] = True info["level"] = battery.percent if battery.power_plugged: info["status"] = "charging" if battery.percent < 100 else "full" else: info["status"] = "discharging" if battery.secsleft and battery.secsleft > 0: info["seconds_left"] = battery.secsleft except Exception: pass return info @staticmethod def get_power_status() -> dict: """获取电源状态""" info = WindowsPower.get_battery_info() return { "battery_present": info["present"], "battery_level": info["level"], "battery_status": info["status"], "is_charging": "charging" in info["status"], "temperature": info["temperature"], "voltage": info["voltage"], } @staticmethod def get_cpu_usage() -> float: """获取CPU使用率""" if HAS_PSUTIL: return psutil.cpu_percent(interval=0.5) return 0.0 @staticmethod def get_memory_info() -> dict: """获取内存信息""" if HAS_PSUTIL: mem = psutil.virtual_memory() return { "total": mem.total, "available": mem.available, "used": mem.used, "free": mem.free, "percent": mem.percent, } return {"total": 0, "available": 0, "used": 0, "free": 0, "percent": 0} # ═══════════════════════════════════════════════════════════════ # 位置 — 真实实现 (IP 地理定位) # ═══════════════════════════════════════════════════════════════ class WindowsLocation: """真实位置信息 — 通过 IP 地理定位""" _cached_location = None _cache_time = 0 @staticmethod def get_location() -> dict: """通过 IP 获取地理位置""" if not HAS_REQUESTS: return {"latitude": 0.0, "longitude": 0.0, "accuracy": 0, "source": "none"} now = time.time() if WindowsLocation._cached_location and now - WindowsLocation._cache_time < 300: return WindowsLocation._cached_location services = [ ("http://ip-api.com/json/", "ip-api"), ("https://ipapi.co/json/", "ipapi"), ("https://freeipapi.com/api/json", "freeipapi"), ] for url, source in services: try: resp = requests.get(url, timeout=5) if resp.status_code == 200: data = resp.json() lat = data.get("lat") or data.get("latitude") or 0.0 lon = data.get("lon") or data.get("longitude") or 0.0 location = { "latitude": float(lat), "longitude": float(lon), "accuracy": 50000, "source": source, "city": data.get("city", ""), "region": data.get("regionName", data.get("region", "")), "country": data.get("country", data.get("country_name", "")), "country_code": data.get("countryCode", data.get("country", "")), "timezone": data.get("timezone", ""), "isp": data.get("isp", ""), } WindowsLocation._cached_location = location WindowsLocation._cache_time = now return location except Exception: continue return {"latitude": 0.0, "longitude": 0.0, "accuracy": 0, "source": "failed"} # ═══════════════════════════════════════════════════════════════ # 通知 — 真实实现 (win10toast / PowerShell) # ═══════════════════════════════════════════════════════════════ class WindowsNotify: """真实桌面通知 — 通过 win10toast 或 PowerShell""" @staticmethod def send(title: str, text: str, icon: str = None, urgency: str = "normal", timeout: int = 5) -> bool: # 方法1: win10toast if HAS_WIN10TOAST: try: toaster = ToastNotifier() toaster.show_toast(title, text, duration=timeout, threaded=True) return True except Exception: pass # 方法2: PowerShell BurntToast 模块 script = f''' try {{ Import-Module BurntToast -ErrorAction Stop New-BurntToastNotification -Text "{title}", "{text}" }} catch {{ [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") [System.Windows.Forms.MessageBox]::Show("{text}", "{title}") }} ''' code, _, _ = _run_ps(script) if code == 0: return True # 方法3: PowerShell 原生方式 script = f''' [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null $template = @" {title} {text} "@ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument $xml.LoadXml($template) $toast = [Windows.UI.Notifications.ToastNotification]::new($xml) [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Android-pyApi").Show($toast) ''' code, _, _ = _run_ps(script) return code == 0 # ═══════════════════════════════════════════════════════════════ # 显示/屏幕 — 真实实现 (pyautogui, screeninfo, win32api) # ═══════════════════════════════════════════════════════════════ class WindowsDisplay: """真实显示/屏幕信息""" @staticmethod def get_screen_size() -> Tuple[int, int]: """获取屏幕尺寸""" # 方法1: screeninfo if HAS_SCREENINFO: try: monitors = get_monitors() if monitors: m = monitors[0] return m.width, m.height except Exception: pass # 方法2: win32api if HAS_WIN32: try: width = win32api.GetSystemMetrics(0) height = win32api.GetSystemMetrics(1) return width, height except Exception: pass # 方法3: pyautogui if HAS_PYAUTOGUI: try: size = pyautogui.size() return size.width, size.height except Exception: pass # 方法4: tkinter try: import tkinter root = tkinter.Tk() w = root.winfo_screenwidth() h = root.winfo_screenheight() root.destroy() return w, h except Exception: return 1920, 1080 @staticmethod def screenshot(path: str = None) -> Optional[str]: """截屏""" if HAS_PYAUTOGUI: try: img = pyautogui.screenshot() if path: img.save(path) return path else: buf = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"screenshot_{int(time.time())}.png") img.save(buf) return buf except Exception: pass if HAS_PIL: try: img = ImageGrab.grab() if path: img.save(path) return path else: buf = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"screenshot_{int(time.time())}.png") img.save(buf) return buf except Exception: pass return None @staticmethod def get_mouse_position() -> Tuple[int, int]: """获取鼠标位置""" if HAS_PYAUTOGUI: try: pos = pyautogui.position() return pos.x, pos.y except Exception: pass if HAS_WIN32: try: pos = win32api.GetCursorPos() return pos[0], pos[1] except Exception: pass return 0, 0 # ═══════════════════════════════════════════════════════════════ # UI 自动化 — 真实实现 (pyautogui) # ═══════════════════════════════════════════════════════════════ class WindowsUIAutomation: """真实 UI 自动化 — 通过 pyautogui""" @staticmethod def click(x: int, y: int) -> bool: if not HAS_PYAUTOGUI: return False try: pyautogui.click(x, y) return True except Exception: return False @staticmethod def double_click(x: int, y: int) -> bool: if not HAS_PYAUTOGUI: return False try: pyautogui.doubleClick(x, y) return True except Exception: return False @staticmethod def right_click(x: int, y: int) -> bool: if not HAS_PYAUTOGUI: return False try: pyautogui.rightClick(x, y) return True except Exception: return False @staticmethod def type_text(text: str) -> bool: if not HAS_PYAUTOGUI: return False try: pyautogui.typewrite(text, interval=0.02) return True except Exception: return False @staticmethod def press_key(key: str) -> bool: if not HAS_PYAUTOGUI: return False key_map = { "enter": "enter", "return": "enter", "tab": "tab", "escape": "escape", "esc": "escape", "backspace": "backspace", "delete": "delete", "space": "space", "up": "up", "down": "down", "left": "left", "right": "right", "home": "home", "end": "end", "page_up": "pageup", "page_down": "pagedown", } pyautogui_key = key_map.get(key.lower(), key) try: pyautogui.press(pyautogui_key) return True except Exception: return False @staticmethod def hotkey(*keys) -> bool: if not HAS_PYAUTOGUI: return False key_map = { "ctrl": "ctrl", "control": "ctrl", "alt": "alt", "shift": "shift", "super": "win", "win": "win", "meta": "win", } mapped = [key_map.get(k.lower(), k) for k in keys] try: pyautogui.hotkey(*mapped) return True except Exception: return False @staticmethod def scroll(amount: int) -> bool: if not HAS_PYAUTOGUI: return False try: pyautogui.scroll(amount) return True except Exception: return False @staticmethod def find_window(title: str) -> Optional[int]: """查找窗口""" if HAS_WIN32: try: hwnd = win32gui.FindWindow(None, title) if hwnd: return hwnd except Exception: pass # 模糊搜索 if HAS_WIN32: found = [] def callback(hwnd, _): if win32gui.IsWindowVisible(hwnd): window_title = win32gui.GetWindowText(hwnd) if title.lower() in window_title.lower(): found.append(hwnd) return True try: win32gui.EnumWindows(callback, None) if found: return found[0] except Exception: pass return None @staticmethod def activate_window(window_id: int) -> bool: if HAS_WIN32: try: win32gui.SetForegroundWindow(window_id) return True except Exception: pass return False @staticmethod def close_window(window_id: int) -> bool: if HAS_WIN32: try: win32gui.PostMessage(window_id, win32con.WM_CLOSE, 0, 0) return True except Exception: pass return False # ═══════════════════════════════════════════════════════════════ # 摄像头 — 真实实现 (opencv) # ═══════════════════════════════════════════════════════════════ class WindowsCamera: """真实摄像头 — 通过 OpenCV""" @staticmethod def list_cameras() -> List[int]: """列出可用摄像头""" cameras = [] if HAS_CV2: for i in range(10): cap = cv2.VideoCapture(i) if cap.isOpened(): cameras.append(i) cap.release() else: break return cameras @staticmethod def capture(camera_id: int = 0, output_path: str = None) -> Optional[str]: """拍照""" if not HAS_CV2: return None cap = cv2.VideoCapture(camera_id) if not cap.isOpened(): return None ret, frame = cap.read() cap.release() if not ret: return None if not output_path: output_path = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"camera_{int(time.time())}.jpg") cv2.imwrite(output_path, frame) return output_path @staticmethod def get_camera_info(camera_id: int = 0) -> dict: """获取摄像头信息""" info = {"id": camera_id, "available": False, "width": 0, "height": 0} if HAS_CV2: cap = cv2.VideoCapture(camera_id) if cap.isOpened(): info["available"] = True info["width"] = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) info["height"] = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) info["fps"] = cap.get(cv2.CAP_PROP_FPS) cap.release() return info # ═══════════════════════════════════════════════════════════════ # 进程/应用管理 — 真实实现 (psutil) # ═══════════════════════════════════════════════════════════════ class WindowsProcess: """真实进程/应用管理""" @staticmethod def list_processes() -> List[dict]: """列出所有进程""" if HAS_PSUTIL: procs = [] for p in psutil.process_iter(['pid', 'name', 'username', 'memory_percent', 'cpu_percent', 'status', 'cmdline']): try: info = p.info procs.append({ "pid": info['pid'], "name": info['name'], "user": info['username'], "memory": info.get('memory_percent', 0), "cpu": info.get('cpu_percent', 0), "status": info['status'], "cmdline": " ".join(info.get('cmdline') or []), }) except (psutil.NoSuchProcess, psutil.AccessDenied): pass return procs # 回退到 tasklist code, out, _ = _run("tasklist /fo csv /v") if code == 0: procs = [] lines = out.split("\n") for line in lines[1:]: parts = line.split('","') if len(parts) >= 8: procs.append({ "name": parts[0].strip('"'), "pid": int(parts[1].strip('"')), "user": parts[6].strip('"') if len(parts) > 6 else "", "status": parts[7].strip('"') if len(parts) > 7 else "", "memory": 0, "cpu": 0, "cmdline": "", }) return procs return [] @staticmethod def get_running_apps() -> List[dict]: """获取运行中的应用 (GUI 窗口)""" apps = [] if HAS_WIN32: def callback(hwnd, _): if win32gui.IsWindowVisible(hwnd): title = win32gui.GetWindowText(hwnd) if title and title not in ("Default IME", "MSCTFIME UI"): apps.append({"window_id": hwnd, "name": title}) return True try: win32gui.EnumWindows(callback, None) except Exception: pass return apps @staticmethod def kill_process(pid: int) -> bool: """终止进程""" if HAS_PSUTIL: try: psutil.Process(pid).terminate() return True except Exception: return False code, _, _ = _run(f"taskkill /pid {pid} /f") return code == 0 @staticmethod def start_app(name: str) -> bool: """启动应用""" try: os.startfile(name) return True except Exception: pass try: subprocess.Popen(name, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return True except Exception: return False @staticmethod def get_system_info() -> dict: """获取系统信息""" info = { "platform": platform.system(), "hostname": socket.gethostname(), "kernel": platform.release(), "arch": platform.machine(), "python": platform.python_version(), "windows_edition": platform.win32_edition() if hasattr(platform, 'win32_edition') else "", "windows_version": platform.version(), } if HAS_PSUTIL: info["cpu_count"] = psutil.cpu_count() cpu_freq = psutil.cpu_freq() info["cpu_freq"] = cpu_freq.current if cpu_freq else 0 boot_time = psutil.boot_time() info["uptime"] = time.time() - boot_time mem = psutil.virtual_memory() info["memory_total"] = mem.total info["memory_used"] = mem.used info["memory_percent"] = mem.percent disk = psutil.disk_usage('C:\\') info["disk_total"] = disk.total info["disk_used"] = disk.used info["disk_percent"] = disk.percent return info # ═══════════════════════════════════════════════════════════════ # Shell — 真实实现 # ═══════════════════════════════════════════════════════════════ class WindowsShell: """真实 Shell 执行""" @staticmethod def exec(command: str, timeout: int = 30, root: bool = False) -> dict: """执行 Shell 命令""" if root: # 以管理员权限运行 script = f'Start-Process cmd -ArgumentList "/c {command}" -Verb RunAs -Wait' code, out, err = _run_ps(script, timeout=timeout) else: code, out, err = _run(command, timeout=timeout) return { "success": code == 0, "output": out + ("\n" + err if err else ""), "exit_code": code, "stdout": out, "stderr": err, } @staticmethod def exec_async(command: str) -> int: """异步执行命令,返回 PID""" try: proc = subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return proc.pid except Exception: return -1 # ═══════════════════════════════════════════════════════════════ # 统一入口 # ═══════════════════════════════════════════════════════════════ def get_windows_capabilities() -> dict: """获取当前 Windows 环境的可用能力""" return { "is_windows": IS_WINDOWS, "network": True, "bluetooth": HAS_POWERSHELL, "audio": HAS_PYCAW or HAS_POWERSHELL, "clipboard": HAS_PYPERCLIP, "ui_automation": HAS_PYAUTOGUI, "notifications": HAS_WIN10TOAST or HAS_POWERSHELL, "sensors": HAS_WMI or HAS_PSUTIL, "display": True, "camera": HAS_CV2, "ffmpeg": HAS_FFMPEG, "ffprobe": HAS_FFPROBE, "psutil": HAS_PSUTIL, "requests": HAS_REQUESTS, "PIL": HAS_PIL, "opencv": HAS_CV2, "numpy": HAS_NUMPY, "wmi": HAS_WMI, "pyautogui": HAS_PYAUTOGUI, "pyperclip": HAS_PYPERCLIP, "pycaw": HAS_PYCAW, "win32": HAS_WIN32, "screeninfo": HAS_SCREENINFO, "win10toast": HAS_WIN10TOAST, } # 导出所有实现类 __all__ = [ "IS_WINDOWS", "get_windows_capabilities", "WindowsNetwork", "WindowsBluetooth", "WindowsSensors", "WindowsAudio", "WindowsClipboard", "WindowsPower", "WindowsLocation", "WindowsNotify", "WindowsDisplay", "WindowsUIAutomation", "WindowsCamera", "WindowsProcess", "WindowsShell", # 能力标志 "HAS_POWERSHELL", "HAS_FFMPEG", "HAS_FFPROBE", "HAS_PSUTIL", "HAS_REQUESTS", "HAS_CV2", "HAS_NUMPY", "HAS_PIL", "HAS_PYAUTOGUI", "HAS_PYPERCLIP", "HAS_WIN10TOAST", "HAS_WMI", "HAS_WIN32", "HAS_PYCAW", "HAS_SCREENINFO", ]