autoglm-gui 0.4.11__py3-none-any.whl → 0.4.13__py3-none-any.whl
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.
- AutoGLM_GUI/__init__.py +8 -0
- AutoGLM_GUI/__main__.py +29 -34
- AutoGLM_GUI/adb_plus/__init__.py +6 -0
- AutoGLM_GUI/adb_plus/device.py +50 -0
- AutoGLM_GUI/adb_plus/ip.py +78 -0
- AutoGLM_GUI/adb_plus/serial.py +35 -0
- AutoGLM_GUI/api/__init__.py +10 -1
- AutoGLM_GUI/api/agents.py +76 -67
- AutoGLM_GUI/api/devices.py +96 -6
- AutoGLM_GUI/api/media.py +12 -235
- AutoGLM_GUI/api/version.py +192 -0
- AutoGLM_GUI/config_manager.py +538 -97
- AutoGLM_GUI/exceptions.py +7 -0
- AutoGLM_GUI/platform_utils.py +19 -0
- AutoGLM_GUI/schemas.py +46 -2
- AutoGLM_GUI/scrcpy_protocol.py +46 -0
- AutoGLM_GUI/scrcpy_stream.py +192 -307
- AutoGLM_GUI/server.py +7 -2
- AutoGLM_GUI/socketio_server.py +125 -0
- AutoGLM_GUI/static/assets/{about-wSo3UgQ-.js → about-29B5FDM8.js} +1 -1
- AutoGLM_GUI/static/assets/chat-DTN2oKtA.js +149 -0
- AutoGLM_GUI/static/assets/index-Dy550Qqg.css +1 -0
- AutoGLM_GUI/static/assets/{index-B5u1xtK1.js → index-mVNV0VwM.js} +1 -1
- AutoGLM_GUI/static/assets/index-wu8Wjf12.js +10 -0
- AutoGLM_GUI/static/assets/worker-D6BRitjy.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/METADATA +25 -2
- autoglm_gui-0.4.13.dist-info/RECORD +57 -0
- AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +0 -339
- AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +0 -1
- AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
- AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +0 -25
- AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +0 -1
- AutoGLM_GUI/static/assets/index-D5BALRbT.js +0 -10
- autoglm_gui-0.4.11.dist-info/RECORD +0 -52
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.4.11.dist-info → autoglm_gui-0.4.13.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/media.py
CHANGED
|
@@ -1,52 +1,28 @@
|
|
|
1
|
-
"""Media routes: screenshot
|
|
1
|
+
"""Media routes: screenshot and stream reset."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
3
|
+
from __future__ import annotations
|
|
6
4
|
|
|
7
|
-
from fastapi import APIRouter
|
|
5
|
+
from fastapi import APIRouter
|
|
8
6
|
|
|
9
7
|
from AutoGLM_GUI.adb_plus import capture_screenshot
|
|
10
8
|
from AutoGLM_GUI.logger import logger
|
|
11
9
|
from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
|
|
12
|
-
from AutoGLM_GUI.
|
|
13
|
-
from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
|
|
10
|
+
from AutoGLM_GUI.socketio_server import stop_streamers
|
|
14
11
|
|
|
15
12
|
router = APIRouter()
|
|
16
13
|
|
|
17
|
-
# Debug configuration: Set DEBUG_SAVE_VIDEO_STREAM=1 to save streams to debug_streams/
|
|
18
|
-
DEBUG_SAVE_STREAM = os.getenv("DEBUG_SAVE_VIDEO_STREAM", "0") == "1"
|
|
19
|
-
|
|
20
14
|
|
|
21
15
|
@router.post("/api/video/reset")
|
|
22
16
|
async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
23
|
-
"""Reset
|
|
17
|
+
"""Reset active scrcpy streams (Socket.IO)."""
|
|
18
|
+
stop_streamers(device_id=device_id)
|
|
24
19
|
if device_id:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logger.info(f"Streamer reset for device {device_id}")
|
|
32
|
-
return {
|
|
33
|
-
"success": True,
|
|
34
|
-
"message": f"Video stream reset for device {device_id}",
|
|
35
|
-
}
|
|
36
|
-
return {
|
|
37
|
-
"success": True,
|
|
38
|
-
"message": f"No active video stream for device {device_id}",
|
|
39
|
-
}
|
|
40
|
-
return {"success": True, "message": f"No video stream for device {device_id}"}
|
|
41
|
-
|
|
42
|
-
device_ids = list(scrcpy_streamers.keys())
|
|
43
|
-
for dev_id in device_ids:
|
|
44
|
-
if dev_id in scrcpy_locks:
|
|
45
|
-
async with scrcpy_locks[dev_id]:
|
|
46
|
-
if dev_id in scrcpy_streamers:
|
|
47
|
-
scrcpy_streamers[dev_id].stop()
|
|
48
|
-
del scrcpy_streamers[dev_id]
|
|
49
|
-
logger.info("All streamers reset")
|
|
20
|
+
logger.info("Video stream reset for device %s", device_id)
|
|
21
|
+
return {
|
|
22
|
+
"success": True,
|
|
23
|
+
"message": f"Video stream reset for device {device_id}",
|
|
24
|
+
}
|
|
25
|
+
logger.info("All video streams reset")
|
|
50
26
|
return {"success": True, "message": "All video streams reset"}
|
|
51
27
|
|
|
52
28
|
|
|
@@ -71,202 +47,3 @@ def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
|
|
|
71
47
|
is_sensitive=False,
|
|
72
48
|
error=str(e),
|
|
73
49
|
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@router.websocket("/api/video/stream")
|
|
77
|
-
async def video_stream_ws(
|
|
78
|
-
websocket: WebSocket,
|
|
79
|
-
device_id: str | None = None,
|
|
80
|
-
):
|
|
81
|
-
"""Stream real-time H.264 video from scrcpy server via WebSocket(多设备支持)."""
|
|
82
|
-
await websocket.accept()
|
|
83
|
-
|
|
84
|
-
if not device_id:
|
|
85
|
-
await websocket.send_json({"error": "device_id is required"})
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
logger.info(f"WebSocket connection for device {device_id}")
|
|
89
|
-
|
|
90
|
-
# Debug: Save stream to file for analysis (controlled by DEBUG_SAVE_VIDEO_STREAM env var)
|
|
91
|
-
debug_file = None
|
|
92
|
-
if DEBUG_SAVE_STREAM:
|
|
93
|
-
debug_dir = Path("debug_streams")
|
|
94
|
-
debug_dir.mkdir(exist_ok=True)
|
|
95
|
-
debug_file_path = (
|
|
96
|
-
debug_dir / f"{device_id}_{int(__import__('time').time())}.h264"
|
|
97
|
-
)
|
|
98
|
-
debug_file = open(debug_file_path, "wb")
|
|
99
|
-
logger.debug(f"DEBUG: Saving stream to {debug_file_path}")
|
|
100
|
-
|
|
101
|
-
if device_id not in scrcpy_locks:
|
|
102
|
-
scrcpy_locks[device_id] = asyncio.Lock()
|
|
103
|
-
|
|
104
|
-
async with scrcpy_locks[device_id]:
|
|
105
|
-
if device_id not in scrcpy_streamers:
|
|
106
|
-
logger.info(f"Creating streamer for device {device_id}")
|
|
107
|
-
scrcpy_streamers[device_id] = ScrcpyStreamer(
|
|
108
|
-
device_id=device_id, max_size=1280, bit_rate=4_000_000
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
try:
|
|
112
|
-
logger.info(f"Starting scrcpy server for device {device_id}")
|
|
113
|
-
await scrcpy_streamers[device_id].start()
|
|
114
|
-
logger.info(f"Scrcpy server started for device {device_id}")
|
|
115
|
-
|
|
116
|
-
# Read NAL units until we have SPS, PPS, and IDR
|
|
117
|
-
streamer = scrcpy_streamers[device_id]
|
|
118
|
-
|
|
119
|
-
logger.debug("Reading NAL units for initialization...")
|
|
120
|
-
for attempt in range(20): # Max 20 NAL units for initialization
|
|
121
|
-
try:
|
|
122
|
-
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
123
|
-
nal_type = nal_unit[4] & 0x1F if len(nal_unit) > 4 else -1
|
|
124
|
-
nal_type_names = {5: "IDR", 7: "SPS", 8: "PPS"}
|
|
125
|
-
logger.debug(
|
|
126
|
-
f"Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Check if we have all required parameter sets
|
|
130
|
-
if (
|
|
131
|
-
streamer.cached_sps
|
|
132
|
-
and streamer.cached_pps
|
|
133
|
-
and streamer.cached_idr
|
|
134
|
-
):
|
|
135
|
-
logger.debug(
|
|
136
|
-
f"✓ Initialization complete: SPS={len(streamer.cached_sps)}B, PPS={len(streamer.cached_pps)}B, IDR={len(streamer.cached_idr)}B"
|
|
137
|
-
)
|
|
138
|
-
break
|
|
139
|
-
except Exception as e:
|
|
140
|
-
logger.warning(f"Failed to read NAL unit: {e}")
|
|
141
|
-
await asyncio.sleep(0.5)
|
|
142
|
-
continue
|
|
143
|
-
|
|
144
|
-
# Get initialization data (SPS + PPS + IDR)
|
|
145
|
-
init_data = streamer.get_initialization_data()
|
|
146
|
-
if not init_data:
|
|
147
|
-
raise RuntimeError(
|
|
148
|
-
"Failed to get initialization data (missing SPS/PPS/IDR)"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
# Send initialization data as ONE message (SPS+PPS+IDR combined)
|
|
152
|
-
await websocket.send_bytes(init_data)
|
|
153
|
-
logger.debug(
|
|
154
|
-
f"✓ Sent initialization data to first client: {len(init_data)} bytes total"
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# Debug: Save to file
|
|
158
|
-
if debug_file:
|
|
159
|
-
debug_file.write(init_data)
|
|
160
|
-
debug_file.flush()
|
|
161
|
-
|
|
162
|
-
except Exception as e:
|
|
163
|
-
logger.exception(f"Failed to start streamer: {e}")
|
|
164
|
-
scrcpy_streamers[device_id].stop()
|
|
165
|
-
del scrcpy_streamers[device_id]
|
|
166
|
-
try:
|
|
167
|
-
await websocket.send_json({"error": str(e)})
|
|
168
|
-
except Exception:
|
|
169
|
-
pass
|
|
170
|
-
return
|
|
171
|
-
else:
|
|
172
|
-
logger.info(f"Reusing streamer for device {device_id}")
|
|
173
|
-
|
|
174
|
-
streamer = scrcpy_streamers[device_id]
|
|
175
|
-
# CRITICAL: Send complete initialization data (SPS+PPS+IDR)
|
|
176
|
-
# Without IDR frame, decoder cannot start and will show black screen
|
|
177
|
-
|
|
178
|
-
# Wait for initialization data to be ready (max 5 seconds)
|
|
179
|
-
init_data = None
|
|
180
|
-
for attempt in range(10):
|
|
181
|
-
init_data = streamer.get_initialization_data()
|
|
182
|
-
if init_data:
|
|
183
|
-
break
|
|
184
|
-
logger.debug(
|
|
185
|
-
f"Waiting for initialization data (attempt {attempt + 1}/10)..."
|
|
186
|
-
)
|
|
187
|
-
await asyncio.sleep(0.5)
|
|
188
|
-
|
|
189
|
-
if init_data:
|
|
190
|
-
# Log what we're sending
|
|
191
|
-
logger.debug(
|
|
192
|
-
f"✓ Sending cached initialization data for device {device_id}:"
|
|
193
|
-
)
|
|
194
|
-
logger.debug(
|
|
195
|
-
f" - SPS: {len(streamer.cached_sps) if streamer.cached_sps else 0}B"
|
|
196
|
-
)
|
|
197
|
-
logger.debug(
|
|
198
|
-
f" - PPS: {len(streamer.cached_pps) if streamer.cached_pps else 0}B"
|
|
199
|
-
)
|
|
200
|
-
logger.debug(
|
|
201
|
-
f" - IDR: {len(streamer.cached_idr) if streamer.cached_idr else 0}B"
|
|
202
|
-
)
|
|
203
|
-
logger.debug(f" - Total: {len(init_data)} bytes")
|
|
204
|
-
|
|
205
|
-
await websocket.send_bytes(init_data)
|
|
206
|
-
logger.debug("✓ Initialization data sent successfully")
|
|
207
|
-
|
|
208
|
-
# Debug: Save to file
|
|
209
|
-
if debug_file:
|
|
210
|
-
debug_file.write(init_data)
|
|
211
|
-
debug_file.flush()
|
|
212
|
-
else:
|
|
213
|
-
error_msg = f"Initialization data not ready for device {device_id} after 5 seconds"
|
|
214
|
-
logger.error(f"ERROR: {error_msg}")
|
|
215
|
-
try:
|
|
216
|
-
await websocket.send_json({"error": error_msg})
|
|
217
|
-
except Exception:
|
|
218
|
-
pass
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
streamer = scrcpy_streamers[device_id]
|
|
222
|
-
|
|
223
|
-
stream_failed = False
|
|
224
|
-
try:
|
|
225
|
-
nal_count = 0
|
|
226
|
-
while True:
|
|
227
|
-
try:
|
|
228
|
-
# Read one complete NAL unit
|
|
229
|
-
# Each WebSocket message = one complete NAL unit (clear semantic boundary)
|
|
230
|
-
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
231
|
-
await websocket.send_bytes(nal_unit)
|
|
232
|
-
|
|
233
|
-
# Debug: Save to file
|
|
234
|
-
if debug_file:
|
|
235
|
-
debug_file.write(nal_unit)
|
|
236
|
-
debug_file.flush()
|
|
237
|
-
|
|
238
|
-
nal_count += 1
|
|
239
|
-
if nal_count % 100 == 0:
|
|
240
|
-
logger.debug(f"Device {device_id}: Sent {nal_count} NAL units")
|
|
241
|
-
except ConnectionError as e:
|
|
242
|
-
logger.warning(f"Device {device_id}: Connection error: {e}")
|
|
243
|
-
stream_failed = True
|
|
244
|
-
try:
|
|
245
|
-
await websocket.send_json({"error": f"Stream error: {str(e)}"})
|
|
246
|
-
except Exception:
|
|
247
|
-
pass
|
|
248
|
-
break
|
|
249
|
-
|
|
250
|
-
except WebSocketDisconnect:
|
|
251
|
-
logger.info(f"Device {device_id}: Client disconnected")
|
|
252
|
-
except Exception as e:
|
|
253
|
-
logger.exception(f"Device {device_id}: Error: {e}")
|
|
254
|
-
stream_failed = True
|
|
255
|
-
try:
|
|
256
|
-
await websocket.send_json({"error": str(e)})
|
|
257
|
-
except Exception:
|
|
258
|
-
pass
|
|
259
|
-
|
|
260
|
-
if stream_failed:
|
|
261
|
-
async with scrcpy_locks[device_id]:
|
|
262
|
-
if device_id in scrcpy_streamers:
|
|
263
|
-
logger.info(f"Resetting streamer for device {device_id}")
|
|
264
|
-
scrcpy_streamers[device_id].stop()
|
|
265
|
-
del scrcpy_streamers[device_id]
|
|
266
|
-
|
|
267
|
-
# Debug: Close file
|
|
268
|
-
if debug_file:
|
|
269
|
-
debug_file.close()
|
|
270
|
-
logger.debug("DEBUG: Closed debug file")
|
|
271
|
-
|
|
272
|
-
logger.info(f"Device {device_id}: Stream ended")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Version check API for detecting updates from GitHub Releases."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter
|
|
10
|
+
|
|
11
|
+
from AutoGLM_GUI.logger import logger
|
|
12
|
+
from AutoGLM_GUI.schemas import VersionCheckResponse
|
|
13
|
+
from AutoGLM_GUI.version import APP_VERSION
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
# In-memory cache for version check results
|
|
18
|
+
_version_cache: dict[str, Any] = {
|
|
19
|
+
"data": None,
|
|
20
|
+
"timestamp": 0,
|
|
21
|
+
"ttl": 3600, # 1 hour cache TTL
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# GitHub repository information
|
|
25
|
+
GITHUB_REPO = "suyiiyii/AutoGLM-GUI"
|
|
26
|
+
GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_version(version_str: str) -> tuple[int, ...] | None:
|
|
30
|
+
"""
|
|
31
|
+
Parse semantic version string into tuple of integers.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
version_str: Version string like "0.4.12" or "v0.5.0"
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of version numbers like (0, 4, 12) or None if invalid
|
|
38
|
+
"""
|
|
39
|
+
# Handle dev/unknown versions
|
|
40
|
+
if version_str in ("dev", "unknown", "..."):
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Strip 'v' prefix if present
|
|
44
|
+
version_str = version_str.lstrip("v")
|
|
45
|
+
|
|
46
|
+
# Remove pre-release tags (e.g., "-beta", "-rc1")
|
|
47
|
+
version_str = re.split(r"[-+]", version_str)[0]
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return tuple(int(x) for x in version_str.split("."))
|
|
51
|
+
except (ValueError, AttributeError):
|
|
52
|
+
logger.warning(f"Failed to parse version: {version_str}")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compare_versions(current: str, latest: str) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Compare two semantic versions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
current: Current version string
|
|
62
|
+
latest: Latest version string
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if update is available (latest > current), False otherwise
|
|
66
|
+
"""
|
|
67
|
+
current_tuple = parse_version(current)
|
|
68
|
+
latest_tuple = parse_version(latest)
|
|
69
|
+
|
|
70
|
+
# If either version is invalid, assume no update
|
|
71
|
+
if current_tuple is None or latest_tuple is None:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
return latest_tuple > current_tuple
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def fetch_latest_release() -> dict[str, Any] | None:
|
|
78
|
+
"""
|
|
79
|
+
Fetch latest release information from GitHub API.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Release data dict with 'tag_name', 'html_url', 'published_at' or None on error
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
# Create request with User-Agent header (required by GitHub API)
|
|
86
|
+
req = urllib.request.Request(
|
|
87
|
+
GITHUB_API_URL,
|
|
88
|
+
headers={"User-Agent": f"AutoGLM-GUI/{APP_VERSION}"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Fetch data with 10-second timeout
|
|
92
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
93
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
94
|
+
logger.debug(
|
|
95
|
+
f"Successfully fetched latest release: {data.get('tag_name', 'unknown')}"
|
|
96
|
+
)
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
except urllib.error.HTTPError as e:
|
|
100
|
+
if e.code == 403:
|
|
101
|
+
logger.warning(
|
|
102
|
+
"GitHub API rate limit exceeded (HTTP 403), using cached data if available"
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
logger.warning(f"GitHub API HTTP error {e.code}: {e.reason}")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
except urllib.error.URLError as e:
|
|
109
|
+
logger.warning(f"Network error fetching latest release: {e.reason}")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
except json.JSONDecodeError as e:
|
|
113
|
+
logger.error(f"Failed to parse GitHub API response: {e}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Unexpected error fetching latest release: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.get("/api/version/latest", response_model=VersionCheckResponse)
|
|
122
|
+
def check_version() -> VersionCheckResponse:
|
|
123
|
+
"""
|
|
124
|
+
Check for available updates from GitHub Releases.
|
|
125
|
+
|
|
126
|
+
Returns version comparison results with caching to minimize API calls.
|
|
127
|
+
Cache TTL is 1 hour to stay within GitHub API rate limits (60 req/hour).
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
VersionCheckResponse with update information
|
|
131
|
+
"""
|
|
132
|
+
current_time = time.time()
|
|
133
|
+
|
|
134
|
+
# Check if cache is still valid
|
|
135
|
+
if (
|
|
136
|
+
_version_cache["data"] is not None
|
|
137
|
+
and current_time - _version_cache["timestamp"] < _version_cache["ttl"]
|
|
138
|
+
):
|
|
139
|
+
logger.debug(
|
|
140
|
+
f"Using cached version check result (age: {int(current_time - _version_cache['timestamp'])}s)"
|
|
141
|
+
)
|
|
142
|
+
return _version_cache["data"]
|
|
143
|
+
|
|
144
|
+
# Fetch latest release from GitHub
|
|
145
|
+
release_data = fetch_latest_release()
|
|
146
|
+
|
|
147
|
+
if release_data is None:
|
|
148
|
+
# If fetch failed, check if we have cached data to fall back to
|
|
149
|
+
if _version_cache["data"] is not None:
|
|
150
|
+
logger.warning("Using stale cached data due to fetch failure")
|
|
151
|
+
return _version_cache["data"]
|
|
152
|
+
|
|
153
|
+
# No cache available, return safe defaults
|
|
154
|
+
response = VersionCheckResponse(
|
|
155
|
+
current_version=APP_VERSION,
|
|
156
|
+
latest_version=None,
|
|
157
|
+
has_update=False,
|
|
158
|
+
release_url=None,
|
|
159
|
+
published_at=None,
|
|
160
|
+
error="Failed to fetch latest version from GitHub",
|
|
161
|
+
)
|
|
162
|
+
logger.info("Version check failed, returning safe defaults")
|
|
163
|
+
return response
|
|
164
|
+
|
|
165
|
+
# Extract version information from release data
|
|
166
|
+
tag_name = release_data.get("tag_name", "")
|
|
167
|
+
latest_version = tag_name.lstrip("v") # Strip 'v' prefix
|
|
168
|
+
release_url = release_data.get("html_url")
|
|
169
|
+
published_at = release_data.get("published_at")
|
|
170
|
+
|
|
171
|
+
# Compare versions
|
|
172
|
+
has_update = compare_versions(APP_VERSION, latest_version)
|
|
173
|
+
|
|
174
|
+
# Build response
|
|
175
|
+
response = VersionCheckResponse(
|
|
176
|
+
current_version=APP_VERSION,
|
|
177
|
+
latest_version=latest_version,
|
|
178
|
+
has_update=has_update,
|
|
179
|
+
release_url=release_url,
|
|
180
|
+
published_at=published_at,
|
|
181
|
+
error=None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Update cache
|
|
185
|
+
_version_cache["data"] = response
|
|
186
|
+
_version_cache["timestamp"] = current_time
|
|
187
|
+
|
|
188
|
+
logger.info(
|
|
189
|
+
f"Version check completed: current={APP_VERSION}, latest={latest_version}, has_update={has_update}"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return response
|