autoglm-gui 1.0.2__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/__init__.py +7 -0
  2. autoglm_gui-1.1.0/AutoGLM_GUI/adb_plus/mdns.py +192 -0
  3. autoglm_gui-1.1.0/AutoGLM_GUI/adb_plus/pair.py +60 -0
  4. autoglm_gui-1.1.0/AutoGLM_GUI/adb_plus/qr_pair.py +372 -0
  5. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/api/__init__.py +8 -0
  6. autoglm_gui-1.1.0/AutoGLM_GUI/api/devices.py +391 -0
  7. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/schemas.py +70 -0
  8. autoglm_gui-1.0.2/AutoGLM_GUI/static/assets/about-BOnRPlKQ.js → autoglm_gui-1.1.0/AutoGLM_GUI/static/assets/about-Crpy4Xue.js +1 -1
  9. autoglm_gui-1.1.0/AutoGLM_GUI/static/assets/chat-DGFuSj6_.js +149 -0
  10. autoglm_gui-1.1.0/AutoGLM_GUI/static/assets/index-C1k5Ch1V.js +10 -0
  11. autoglm_gui-1.0.2/AutoGLM_GUI/static/assets/index-CRFVU0eu.js → autoglm_gui-1.1.0/AutoGLM_GUI/static/assets/index-COYnSjzf.js +1 -1
  12. autoglm_gui-1.1.0/AutoGLM_GUI/static/assets/index-QX6oy21q.css +1 -0
  13. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/static/index.html +2 -2
  14. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/PKG-INFO +40 -40
  15. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/README.md +38 -39
  16. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/pyproject.toml +2 -1
  17. autoglm_gui-1.0.2/AutoGLM_GUI/api/devices.py +0 -168
  18. autoglm_gui-1.0.2/AutoGLM_GUI/static/assets/chat-CGW6uMKB.js +0 -149
  19. autoglm_gui-1.0.2/AutoGLM_GUI/static/assets/index-DH-Dl4tK.js +0 -10
  20. autoglm_gui-1.0.2/AutoGLM_GUI/static/assets/index-DzUQ89YC.css +0 -1
  21. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/.gitignore +0 -0
  22. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/__init__.py +0 -0
  23. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/__main__.py +0 -0
  24. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/device.py +0 -0
  25. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/ip.py +0 -0
  26. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/keyboard_installer.py +0 -0
  27. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/screenshot.py +0 -0
  28. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/serial.py +0 -0
  29. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/adb_plus/touch.py +0 -0
  30. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/api/agents.py +0 -0
  31. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/api/control.py +0 -0
  32. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/api/media.py +0 -0
  33. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/api/version.py +0 -0
  34. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/config.py +0 -0
  35. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/config_manager.py +0 -0
  36. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/exceptions.py +0 -0
  37. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/logger.py +0 -0
  38. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/platform_utils.py +0 -0
  39. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/scrcpy_protocol.py +0 -0
  40. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/scrcpy_stream.py +0 -0
  41. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/server.py +0 -0
  42. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/socketio_server.py +0 -0
  43. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/state.py +0 -0
  44. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/static/assets/worker-D6BRitjy.js +0 -0
  45. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/AutoGLM_GUI/version.py +0 -0
  46. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/LICENSE +0 -0
  47. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/__init__.py +0 -0
  48. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/actions/__init__.py +0 -0
  49. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/actions/handler.py +0 -0
  50. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/actions/handler_ios.py +0 -0
  51. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/adb/__init__.py +0 -0
  52. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/adb/connection.py +0 -0
  53. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/adb/device.py +0 -0
  54. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/adb/input.py +0 -0
  55. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/adb/screenshot.py +0 -0
  56. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/agent.py +0 -0
  57. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/agent_ios.py +0 -0
  58. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/__init__.py +0 -0
  59. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/apps.py +0 -0
  60. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/apps_harmonyos.py +0 -0
  61. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/apps_ios.py +0 -0
  62. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/i18n.py +0 -0
  63. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/prompts.py +0 -0
  64. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/prompts_en.py +0 -0
  65. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/prompts_zh.py +0 -0
  66. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/config/timing.py +0 -0
  67. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/device_factory.py +0 -0
  68. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/hdc/__init__.py +0 -0
  69. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/hdc/connection.py +0 -0
  70. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/hdc/device.py +0 -0
  71. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/hdc/input.py +0 -0
  72. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/hdc/screenshot.py +0 -0
  73. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/model/__init__.py +0 -0
  74. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/model/client.py +0 -0
  75. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/xctest/__init__.py +0 -0
  76. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/xctest/connection.py +0 -0
  77. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/xctest/device.py +0 -0
  78. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/xctest/input.py +0 -0
  79. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/phone_agent/xctest/screenshot.py +0 -0
  80. {autoglm_gui-1.0.2 → autoglm_gui-1.1.0}/scrcpy-server-v3.3.3 +0 -0
@@ -6,6 +6,9 @@ from .touch import touch_down, touch_move, touch_up
6
6
  from .ip import get_wifi_ip
7
7
  from .serial import get_device_serial
8
8
  from .device import check_device_available
9
+ from .pair import pair_device
10
+ from .mdns import discover_mdns_devices, MdnsDevice
11
+ from .qr_pair import qr_pairing_manager
9
12
 
10
13
  __all__ = [
11
14
  "ADBKeyboardInstaller",
@@ -17,4 +20,8 @@ __all__ = [
17
20
  "get_wifi_ip",
18
21
  "get_device_serial",
19
22
  "check_device_available",
23
+ "pair_device",
24
+ "discover_mdns_devices",
25
+ "MdnsDevice",
26
+ "qr_pairing_manager",
20
27
  ]
@@ -0,0 +1,192 @@
1
+ """mDNS-based ADB device discovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Optional
7
+ from dataclasses import dataclass
8
+
9
+ from AutoGLM_GUI.platform_utils import run_cmd_silently_sync
10
+
11
+ __all__ = ["MdnsDevice", "discover_mdns_devices"]
12
+
13
+
14
+ @dataclass
15
+ class MdnsDevice:
16
+ """Represents an mDNS-discovered ADB device."""
17
+
18
+ name: str # e.g., "adb-243a09b7-cbCO6P"
19
+ ip: str # e.g., "192.168.130.187"
20
+ port: int # e.g., 34553
21
+ has_pairing: bool # True if device also advertises pairing service
22
+ service_type: str # "_adb-tls-connect._tcp" or "_adb-tls-pairing._tcp"
23
+ pairing_port: Optional[int] = None # Pairing port if has_pairing is True
24
+
25
+
26
+ def _parse_mdns_line(line: str) -> tuple[str, str, str] | None:
27
+ """
28
+ Parse a single mDNS service line.
29
+
30
+ Format: "name \t service_type \t ip:port"
31
+ Example: "adb-243a09b7-cbCO6P\t_adb-tls-connect._tcp\t192.168.130.187:34553"
32
+
33
+ Returns:
34
+ Tuple of (name, service_type, address) or None if invalid
35
+ """
36
+ # Split by tab characters
37
+ parts = line.split("\t")
38
+ if len(parts) != 3:
39
+ return None
40
+
41
+ name = parts[0].strip()
42
+ service_type = parts[1].strip()
43
+ address = parts[2].strip()
44
+
45
+ # Validate we have all parts
46
+ if not (name and service_type and address):
47
+ return None
48
+
49
+ return name, service_type, address
50
+
51
+
52
+ def _parse_address(address: str) -> tuple[str, int] | None:
53
+ """
54
+ Parse IP:port from address string.
55
+
56
+ Args:
57
+ address: Format "ip:port", e.g., "192.168.130.187:34553"
58
+
59
+ Returns:
60
+ Tuple of (ip, port) or None if invalid or 0.0.0.0
61
+ """
62
+ # Match IP:port pattern
63
+ match = re.match(r"^([\d.]+):(\d+)$", address)
64
+ if not match:
65
+ return None
66
+
67
+ ip = match.group(1)
68
+ port_str = match.group(2)
69
+
70
+ # Skip 0.0.0.0 addresses (device not properly initialized)
71
+ if ip == "0.0.0.0":
72
+ return None
73
+
74
+ # Validate IP format
75
+ ip_pattern = r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$"
76
+ if not re.match(ip_pattern, ip):
77
+ return None
78
+
79
+ # Validate IP octets
80
+ octets = ip.split(".")
81
+ if not all(0 <= int(octet) <= 255 for octet in octets):
82
+ return None
83
+
84
+ # Parse port
85
+ try:
86
+ port = int(port_str)
87
+ if not (1 <= port <= 65535):
88
+ return None
89
+ except ValueError:
90
+ return None
91
+
92
+ return ip, port
93
+
94
+
95
+ def discover_mdns_devices(adb_path: str = "adb") -> list[MdnsDevice]:
96
+ """
97
+ Discover wireless ADB devices via mDNS.
98
+
99
+ Args:
100
+ adb_path: Path to adb executable
101
+
102
+ Returns:
103
+ List of discovered devices, consolidated by device name
104
+
105
+ Example:
106
+ >>> devices = discover_mdns_devices()
107
+ >>> for dev in devices:
108
+ ... print(f"{dev.name} at {dev.ip}:{dev.port}")
109
+ adb-243a09b7-cbCO6P at 192.168.130.187:34553
110
+ """
111
+ try:
112
+ result = run_cmd_silently_sync([adb_path, "mdns", "services"], timeout=5)
113
+
114
+ # Check for errors
115
+ if result.returncode != 0:
116
+ return []
117
+
118
+ output = result.stdout
119
+ if not output:
120
+ return []
121
+
122
+ # Parse devices by name (consolidate multiple service types)
123
+ devices_dict: dict[str, dict] = {}
124
+
125
+ for line in output.splitlines():
126
+ # Skip header line
127
+ if "List of discovered mdns services" in line:
128
+ continue
129
+
130
+ # Parse line
131
+ parsed = _parse_mdns_line(line)
132
+ if not parsed:
133
+ continue
134
+
135
+ name, service_type, address = parsed
136
+
137
+ # Track pairing service separately (even if 0.0.0.0)
138
+ is_pairing_service = "_adb-tls-pairing._tcp" in service_type
139
+ is_connect_service = "_adb-tls-connect._tcp" in service_type
140
+
141
+ if name not in devices_dict:
142
+ devices_dict[name] = {
143
+ "name": name,
144
+ "ip": None,
145
+ "port": None,
146
+ "service_type": None,
147
+ "has_pairing": False,
148
+ "pairing_port": None,
149
+ }
150
+
151
+ # If this is a pairing service, mark it and save pairing port
152
+ if is_pairing_service:
153
+ devices_dict[name]["has_pairing"] = True
154
+ # Extract pairing port directly from address string (even if IP is 0.0.0.0)
155
+ if ":" in address:
156
+ try:
157
+ port_str = address.split(":")[-1]
158
+ pairing_port = int(port_str)
159
+ if 1 <= pairing_port <= 65535:
160
+ devices_dict[name]["pairing_port"] = pairing_port
161
+ except ValueError:
162
+ pass
163
+
164
+ # Only use connect service for actual connection
165
+ # Parse address for connect service
166
+ addr_parsed = _parse_address(address)
167
+ if is_connect_service and addr_parsed:
168
+ ip, port = addr_parsed
169
+ devices_dict[name]["ip"] = ip
170
+ devices_dict[name]["port"] = port
171
+ devices_dict[name]["service_type"] = service_type
172
+
173
+ # Convert to MdnsDevice objects (filter out devices without valid address)
174
+ devices = []
175
+ for dev_data in devices_dict.values():
176
+ if dev_data["ip"] and dev_data["port"]:
177
+ devices.append(
178
+ MdnsDevice(
179
+ name=dev_data["name"],
180
+ ip=dev_data["ip"],
181
+ port=dev_data["port"],
182
+ has_pairing=dev_data["has_pairing"],
183
+ service_type=dev_data["service_type"],
184
+ pairing_port=dev_data["pairing_port"],
185
+ )
186
+ )
187
+
188
+ return devices
189
+
190
+ except Exception:
191
+ # Return empty list on any error (timeout, command not found, etc.)
192
+ return []
@@ -0,0 +1,60 @@
1
+ """ADB wireless pairing support for Android 11+."""
2
+
3
+ from AutoGLM_GUI.platform_utils import run_cmd_silently_sync
4
+
5
+
6
+ def pair_device(
7
+ ip: str,
8
+ port: int,
9
+ pairing_code: str,
10
+ adb_path: str = "adb",
11
+ ) -> tuple[bool, str]:
12
+ """
13
+ Pair with Android device using wireless debugging (Android 11+).
14
+
15
+ Args:
16
+ ip: Device IP address
17
+ port: Pairing port (NOT connection port, typically shown in "Pair device with code" dialog)
18
+ pairing_code: 6-digit pairing code from device
19
+ adb_path: Path to adb executable
20
+
21
+ Returns:
22
+ Tuple of (success, message)
23
+
24
+ Example:
25
+ >>> pair_device("192.168.1.100", 37831, "197872")
26
+ (True, "Successfully paired to 192.168.1.100:37831")
27
+ """
28
+ # Validate pairing code format (6 digits)
29
+ if not pairing_code.isdigit() or len(pairing_code) != 6:
30
+ return False, "Pairing code must be 6 digits"
31
+
32
+ address = f"{ip}:{port}"
33
+
34
+ try:
35
+ # Execute: adb pair ip:port pairing_code
36
+ result = run_cmd_silently_sync(
37
+ [adb_path, "pair", address, pairing_code], timeout=30
38
+ )
39
+
40
+ output = result.stdout + result.stderr
41
+
42
+ # Check for success indicators
43
+ if "Successfully paired" in output or "success" in output.lower():
44
+ return True, f"Successfully paired to {address}"
45
+ elif "failed" in output.lower():
46
+ # Extract error details
47
+ if "pairing code" in output.lower():
48
+ return False, "Invalid pairing code"
49
+ elif "refused" in output.lower():
50
+ return (
51
+ False,
52
+ "Connection refused - check if wireless debugging is enabled",
53
+ )
54
+ else:
55
+ return False, f"Pairing failed: {output.strip()}"
56
+ else:
57
+ return False, output.strip() or "Unknown pairing error"
58
+
59
+ except Exception as e:
60
+ return False, f"Pairing error: {e}"
@@ -0,0 +1,372 @@
1
+ """QR code-based wireless ADB pairing module.
2
+
3
+ This module provides QR code pairing functionality for Android 11+ devices
4
+ with wireless debugging support. It generates QR codes and listens for mDNS
5
+ service advertisements to automatically pair and connect devices.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import secrets
12
+ import threading
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime
16
+ from typing import Dict, Optional, Set, Tuple
17
+
18
+ from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
19
+
20
+ from AutoGLM_GUI.logger import logger
21
+ from AutoGLM_GUI.platform_utils import run_cmd_silently_sync
22
+
23
+ # mDNS service types
24
+ PAIR_SERVICE_TYPE = "_adb-tls-pairing._tcp.local."
25
+ CONNECT_SERVICE_TYPE = "_adb-tls-connect._tcp.local."
26
+
27
+ # QR code payload format
28
+ QR_PAYLOAD_TEMPLATE = "WIFI:T:ADB;S:{name};P:{password};;"
29
+
30
+ # Success indicators in ADB output
31
+ SUCCESS_PAIR = "Successfully paired"
32
+ SUCCESS_CONNECT = "connected"
33
+
34
+
35
+ @dataclass
36
+ class PairingSession:
37
+ """Represents an active QR pairing session."""
38
+
39
+ session_id: str
40
+ name: str # Session name (S field in QR code)
41
+ password: str # Pairing password (P field in QR code)
42
+ qr_payload: str # Full QR code payload
43
+ status: str # Current status: "listening" | "pairing" | "paired" | "connecting" | "connected" | "timeout" | "error"
44
+ device_id: Optional[str] = None # Device ID after connection (ip:port)
45
+ error_message: Optional[str] = None # Error details if status is "error"
46
+ created_at: float = field(default_factory=lambda: datetime.now().timestamp())
47
+ expires_at: float = 0.0 # Unix timestamp when session expires
48
+ zeroconf: Optional[Zeroconf] = None # Zeroconf instance
49
+ listener: Optional[QRPairingListener] = None # Service listener
50
+ thread: Optional[threading.Thread] = None # Listener thread
51
+
52
+
53
+ def _pick_host_from_info(info) -> Optional[str]:
54
+ """Extract preferred host from service info (IPv4 preferred)."""
55
+ try:
56
+ # Prefer IPv4 addresses
57
+ addrs = info.parsed_addresses()
58
+ for addr in addrs:
59
+ # Simple IPv4 pattern check
60
+ parts = addr.split(".")
61
+ if len(parts) == 4 and all(
62
+ p.isdigit() and 0 <= int(p) <= 255 for p in parts
63
+ ):
64
+ return addr
65
+ # Fallback to first address
66
+ if addrs:
67
+ return addrs[0]
68
+ except Exception:
69
+ pass
70
+
71
+ # Fallback to mDNS hostname (.local.)
72
+ if hasattr(info, "server") and info.server:
73
+ return info.server.rstrip(".")
74
+
75
+ return None
76
+
77
+
78
+ def _adb_pair(host: str, port: int, password: str, adb_path: str = "adb") -> bool:
79
+ """Execute ADB pair command."""
80
+ logger.info(f"[QR Pair] Executing: adb pair {host}:{port}")
81
+ result = run_cmd_silently_sync(
82
+ [adb_path, "pair", f"{host}:{port}", password], timeout=25
83
+ )
84
+ output = result.stdout.strip()
85
+ logger.debug(f"[QR Pair] Pair output: {output}")
86
+
87
+ success = result.returncode == 0 and SUCCESS_PAIR in output
88
+ if success:
89
+ logger.info(f"[QR Pair] Successfully paired with {host}:{port}")
90
+ else:
91
+ logger.warning(f"[QR Pair] Pairing failed for {host}:{port}: {output}")
92
+
93
+ return success
94
+
95
+
96
+ def _adb_connect(host: str, port: int, adb_path: str = "adb") -> bool:
97
+ """Execute ADB connect command."""
98
+ logger.info(f"[QR Pair] Executing: adb connect {host}:{port}")
99
+ result = run_cmd_silently_sync([adb_path, "connect", f"{host}:{port}"], timeout=20)
100
+ output = result.stdout.strip()
101
+ logger.debug(f"[QR Pair] Connect output: {output}")
102
+
103
+ success = result.returncode == 0 and SUCCESS_CONNECT in output.lower()
104
+ if success:
105
+ logger.info(f"[QR Pair] Successfully connected to {host}:{port}")
106
+ else:
107
+ logger.warning(f"[QR Pair] Connection failed for {host}:{port}: {output}")
108
+
109
+ return success
110
+
111
+
112
+ class QRPairingListener(ServiceListener):
113
+ """mDNS service listener for QR pairing."""
114
+
115
+ def __init__(self, session: PairingSession, adb_path: str):
116
+ self.session = session
117
+ self.adb_path = adb_path
118
+
119
+ self.paired: bool = False
120
+ self.connected: bool = False
121
+
122
+ self.attempted_pair: Set[Tuple[str, int]] = set()
123
+ self.attempted_connect: Set[Tuple[str, int]] = set()
124
+
125
+ self.last_paired_host: Optional[str] = None
126
+
127
+ def add_service(self, zc: Zeroconf, service_type: str, name: str) -> None:
128
+ """Handle new service discovery."""
129
+ info = zc.get_service_info(service_type, name, timeout=3000)
130
+ if not info:
131
+ logger.debug(f"[QR Pair] No info for service: {name}")
132
+ return
133
+
134
+ host = _pick_host_from_info(info)
135
+ if not host:
136
+ logger.debug(f"[QR Pair] No valid host for service: {name}")
137
+ return
138
+
139
+ port = info.port
140
+ if port is None:
141
+ logger.debug(f"[QR Pair] No valid port for service: {name}")
142
+ return
143
+
144
+ key = (host, port)
145
+
146
+ # Handle pairing service
147
+ if service_type == PAIR_SERVICE_TYPE and not self.paired:
148
+ if key in self.attempted_pair:
149
+ logger.debug(f"[QR Pair] Already attempted pairing for {host}:{port}")
150
+ return
151
+ self.attempted_pair.add(key)
152
+
153
+ logger.info(f"[QR Pair] Found pairing service: {name} -> {host}:{port}")
154
+ self.session.status = "pairing"
155
+
156
+ success = _adb_pair(host, port, self.session.password, self.adb_path)
157
+ if success:
158
+ self.paired = True
159
+ self.last_paired_host = host
160
+ self.session.status = "paired"
161
+ logger.info("[QR Pair] Pairing OK. Waiting for connect service...")
162
+
163
+ # Handle connect service
164
+ if service_type == CONNECT_SERVICE_TYPE and self.paired and not self.connected:
165
+ # Prefer same host as paired if we have it
166
+ if self.last_paired_host and host != self.last_paired_host:
167
+ logger.debug(
168
+ f"[QR Pair] Skipping connect service on different host: {host} (expected {self.last_paired_host})"
169
+ )
170
+ return
171
+
172
+ if key in self.attempted_connect:
173
+ logger.debug(
174
+ f"[QR Pair] Already attempted connection for {host}:{port}"
175
+ )
176
+ return
177
+ self.attempted_connect.add(key)
178
+
179
+ logger.info(f"[QR Pair] Found connect service: {name} -> {host}:{port}")
180
+ self.session.status = "connecting"
181
+
182
+ success = _adb_connect(host, port, self.adb_path)
183
+ if success:
184
+ self.connected = True
185
+ self.session.status = "connected"
186
+ self.session.device_id = f"{host}:{port}"
187
+ logger.info(f"[QR Pair] Connected! Device ID: {self.session.device_id}")
188
+
189
+ def update_service(self, zc: Zeroconf, service_type: str, name: str) -> None:
190
+ """Handle service updates (treat as adds)."""
191
+ self.add_service(zc, service_type, name)
192
+
193
+ def remove_service(self, _zc: Zeroconf, _service_type: str, _name: str) -> None:
194
+ """Handle service removal (no action needed)."""
195
+ pass
196
+
197
+
198
+ class QRPairingManager:
199
+ """Manages active QR pairing sessions."""
200
+
201
+ def __init__(self):
202
+ self._sessions: Dict[str, PairingSession] = {}
203
+
204
+ def create_session(
205
+ self, timeout: int = 90, adb_path: str = "adb"
206
+ ) -> PairingSession:
207
+ """Create a new pairing session with QR code.
208
+
209
+ Args:
210
+ timeout: Session timeout in seconds (default 90)
211
+ adb_path: Path to ADB executable (default "adb")
212
+
213
+ Returns:
214
+ PairingSession with generated QR code payload
215
+ """
216
+ session_id = str(uuid.uuid4())
217
+ name = f"debug-{secrets.token_hex(4)}" # 8 hex chars
218
+ password = secrets.token_hex(8) # 16 hex chars (64-bit entropy)
219
+ qr_payload = QR_PAYLOAD_TEMPLATE.format(name=name, password=password)
220
+
221
+ now = datetime.now().timestamp()
222
+ session = PairingSession(
223
+ session_id=session_id,
224
+ name=name,
225
+ password=password,
226
+ qr_payload=qr_payload,
227
+ status="listening",
228
+ created_at=now,
229
+ expires_at=now + timeout,
230
+ )
231
+
232
+ # Start mDNS listener in background thread
233
+ self._start_listener(session, adb_path)
234
+
235
+ self._sessions[session_id] = session
236
+ logger.info(f"[QR Pair] Created session {session_id} (timeout={timeout}s)")
237
+ logger.debug(f"[QR Pair] QR payload: {qr_payload}")
238
+
239
+ return session
240
+
241
+ def _start_listener(self, session: PairingSession, adb_path: str):
242
+ """Start zeroconf listener (runs in thread pool to avoid blocking)."""
243
+
244
+ def _listen():
245
+ """Listener thread function."""
246
+ try:
247
+ zc = Zeroconf()
248
+ listener = QRPairingListener(session, adb_path)
249
+ session.zeroconf = zc
250
+ session.listener = listener
251
+
252
+ # Register service browsers
253
+ ServiceBrowser(zc, PAIR_SERVICE_TYPE, listener)
254
+ ServiceBrowser(zc, CONNECT_SERVICE_TYPE, listener)
255
+
256
+ logger.info(
257
+ f"[QR Pair] Listening for mDNS services ({PAIR_SERVICE_TYPE}, {CONNECT_SERVICE_TYPE})"
258
+ )
259
+
260
+ # Wait until timeout or connected
261
+ deadline = session.expires_at
262
+ while datetime.now().timestamp() < deadline:
263
+ if listener.connected:
264
+ logger.info("[QR Pair] Listener detected connection, stopping")
265
+ break
266
+ # Sleep in small increments to check frequently
267
+ import time
268
+
269
+ time.sleep(0.2)
270
+
271
+ # Check final status
272
+ if not listener.connected:
273
+ session.status = "timeout"
274
+ logger.warning(f"[QR Pair] Session {session.session_id} timed out")
275
+
276
+ except Exception as e:
277
+ logger.exception(f"[QR Pair] Listener error: {e}")
278
+ session.status = "error"
279
+ session.error_message = str(e)
280
+ finally:
281
+ # Cleanup zeroconf
282
+ if session.zeroconf:
283
+ try:
284
+ session.zeroconf.close()
285
+ logger.debug(
286
+ f"[QR Pair] Zeroconf closed for session {session.session_id}"
287
+ )
288
+ except Exception as e:
289
+ logger.error(f"[QR Pair] Error closing zeroconf: {e}")
290
+
291
+ # Run in dedicated thread (non-blocking for FastAPI)
292
+ thread = threading.Thread(
293
+ target=_listen,
294
+ name=f"QRPairing-{session.session_id[:8]}",
295
+ daemon=True, # 守护线程,主程序退出时自动终止
296
+ )
297
+ session.thread = thread
298
+ thread.start()
299
+ logger.debug(
300
+ f"[QR Pair] Started listener thread for session {session.session_id}"
301
+ )
302
+
303
+ def get_session(self, session_id: str) -> Optional[PairingSession]:
304
+ """Get session by ID.
305
+
306
+ Args:
307
+ session_id: Session UUID
308
+
309
+ Returns:
310
+ PairingSession if found, None otherwise
311
+ """
312
+ return self._sessions.get(session_id)
313
+
314
+ def cancel_session(self, session_id: str) -> bool:
315
+ """Cancel and cleanup a session.
316
+
317
+ Args:
318
+ session_id: Session UUID to cancel
319
+
320
+ Returns:
321
+ True if session was found and cancelled, False otherwise
322
+ """
323
+ session = self._sessions.pop(session_id, None)
324
+ if session:
325
+ logger.info(f"[QR Pair] Cancelling session {session_id}")
326
+
327
+ # Close zeroconf (this will cause the listener thread to exit)
328
+ if session.zeroconf:
329
+ try:
330
+ session.zeroconf.close()
331
+ logger.debug(
332
+ f"[QR Pair] Zeroconf closed for cancelled session {session_id}"
333
+ )
334
+ except Exception as e:
335
+ logger.error(
336
+ f"[QR Pair] Error closing zeroconf during cancellation: {e}"
337
+ )
338
+
339
+ # Wait for thread to finish (with timeout)
340
+ if session.thread and session.thread.is_alive():
341
+ logger.debug("[QR Pair] Waiting for listener thread to finish...")
342
+ session.thread.join(timeout=2.0) # 最多等待2秒
343
+ if session.thread.is_alive():
344
+ logger.warning(
345
+ "[QR Pair] Listener thread did not finish in time (will be abandoned as daemon)"
346
+ )
347
+
348
+ return True
349
+ else:
350
+ logger.warning(f"[QR Pair] Session {session_id} not found for cancellation")
351
+ return False
352
+
353
+ async def cleanup_expired_sessions(self):
354
+ """Background task to cleanup expired sessions.
355
+
356
+ Runs indefinitely, checking every 60 seconds for expired sessions.
357
+ """
358
+ logger.info("[QR Pair] Starting cleanup task")
359
+ while True:
360
+ await asyncio.sleep(60) # Check every minute
361
+ now = datetime.now().timestamp()
362
+ expired = [
363
+ sid for sid, sess in self._sessions.items() if now > sess.expires_at
364
+ ]
365
+
366
+ for sid in expired:
367
+ logger.info(f"[QR Pair] Cleaning up expired session {sid}")
368
+ self.cancel_session(sid)
369
+
370
+
371
+ # Global singleton instance
372
+ qr_pairing_manager = QRPairingManager()
@@ -1,5 +1,6 @@
1
1
  """FastAPI application factory and route registration."""
2
2
 
3
+ import asyncio
3
4
  import sys
4
5
  from importlib.resources import files
5
6
  from pathlib import Path
@@ -10,6 +11,7 @@ from fastapi.responses import FileResponse
10
11
  from fastapi.staticfiles import StaticFiles
11
12
 
12
13
  from AutoGLM_GUI.version import APP_VERSION
14
+ from AutoGLM_GUI.adb_plus.qr_pair import qr_pairing_manager
13
15
 
14
16
  from . import agents, control, devices, media, version
15
17
 
@@ -56,6 +58,12 @@ def create_app() -> FastAPI:
56
58
  app.include_router(media.router)
57
59
  app.include_router(version.router)
58
60
 
61
+ @app.on_event("startup")
62
+ async def startup_event():
63
+ """Initialize background tasks on server startup."""
64
+ # Start QR pairing session cleanup task
65
+ asyncio.create_task(qr_pairing_manager.cleanup_expired_sessions())
66
+
59
67
  static_dir = _get_static_dir()
60
68
  if static_dir is not None and static_dir.exists():
61
69
  assets_dir = static_dir / "assets"