Files
Android-pyApi/android_pyapi/_windows_impl.py
T
shuiyu1123 0f9b6fcc5d feat: 添加 Windows 真实实现层
- 新增 _windows_impl.py,包含 14 个 Windows 实现类
- 支持网络、蓝牙、传感器、音频、剪贴板、电源、位置、通知、显示、UI自动化、摄像头、进程管理、Shell 等功能
- 更新 README.md 添加 Windows 支持说明
- 更新 setup.py 添加 Windows 专用依赖
2026-06-26 21:25:12 +08:00

1381 lines
48 KiB
Python

"""
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 = @"
<toast>
<visual>
<binding template="ToastGeneric">
<text>{title}</text>
<text>{text}</text>
</binding>
</visual>
</toast>
"@
$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",
]