autoglm-gui 0.4.9__py3-none-any.whl → 0.4.11__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/__main__.py +63 -15
- AutoGLM_GUI/adb_plus/__init__.py +2 -0
- AutoGLM_GUI/adb_plus/keyboard_installer.py +380 -0
- AutoGLM_GUI/api/agents.py +123 -1
- AutoGLM_GUI/api/media.py +36 -43
- AutoGLM_GUI/config_manager.py +124 -0
- AutoGLM_GUI/logger.py +85 -0
- AutoGLM_GUI/platform_utils.py +11 -5
- AutoGLM_GUI/resources/apks/ADBKeyBoard.LICENSE.txt +339 -0
- AutoGLM_GUI/resources/apks/ADBKeyBoard.README.txt +1 -0
- AutoGLM_GUI/resources/apks/ADBKeyboard.apk +0 -0
- AutoGLM_GUI/schemas.py +17 -0
- AutoGLM_GUI/scrcpy_stream.py +37 -41
- AutoGLM_GUI/state.py +2 -1
- AutoGLM_GUI/static/assets/{about-BI6OV6gm.js → about-wSo3UgQ-.js} +1 -1
- AutoGLM_GUI/static/assets/chat-BcY2K0yj.js +25 -0
- AutoGLM_GUI/static/assets/{index-Do7ha9Kf.js → index-B5u1xtK1.js} +1 -1
- AutoGLM_GUI/static/assets/index-CHrYo3Qj.css +1 -0
- AutoGLM_GUI/static/assets/{index-Dn3vR6uV.js → index-D5BALRbT.js} +5 -5
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-0.4.9.dist-info → autoglm_gui-0.4.11.dist-info}/METADATA +14 -2
- {autoglm_gui-0.4.9.dist-info → autoglm_gui-0.4.11.dist-info}/RECORD +25 -19
- AutoGLM_GUI/static/assets/chat-C_2Cot0q.js +0 -25
- AutoGLM_GUI/static/assets/index-DCrxTz-A.css +0 -1
- {autoglm_gui-0.4.9.dist-info → autoglm_gui-0.4.11.dist-info}/WHEEL +0 -0
- {autoglm_gui-0.4.9.dist-info → autoglm_gui-0.4.11.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-0.4.9.dist-info → autoglm_gui-0.4.11.dist-info}/licenses/LICENSE +0 -0
AutoGLM_GUI/api/media.py
CHANGED
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
|
8
8
|
|
|
9
9
|
from AutoGLM_GUI.adb_plus import capture_screenshot
|
|
10
|
+
from AutoGLM_GUI.logger import logger
|
|
10
11
|
from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
|
|
11
12
|
from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
|
|
12
13
|
from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
|
|
@@ -24,10 +25,10 @@ async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
|
24
25
|
if device_id in scrcpy_locks:
|
|
25
26
|
async with scrcpy_locks[device_id]:
|
|
26
27
|
if device_id in scrcpy_streamers:
|
|
27
|
-
|
|
28
|
+
logger.info(f"Stopping streamer for device {device_id}")
|
|
28
29
|
scrcpy_streamers[device_id].stop()
|
|
29
30
|
del scrcpy_streamers[device_id]
|
|
30
|
-
|
|
31
|
+
logger.info(f"Streamer reset for device {device_id}")
|
|
31
32
|
return {
|
|
32
33
|
"success": True,
|
|
33
34
|
"message": f"Video stream reset for device {device_id}",
|
|
@@ -45,7 +46,7 @@ async def reset_video_stream(device_id: str | None = None) -> dict:
|
|
|
45
46
|
if dev_id in scrcpy_streamers:
|
|
46
47
|
scrcpy_streamers[dev_id].stop()
|
|
47
48
|
del scrcpy_streamers[dev_id]
|
|
48
|
-
|
|
49
|
+
logger.info("All streamers reset")
|
|
49
50
|
return {"success": True, "message": "All video streams reset"}
|
|
50
51
|
|
|
51
52
|
|
|
@@ -84,7 +85,7 @@ async def video_stream_ws(
|
|
|
84
85
|
await websocket.send_json({"error": "device_id is required"})
|
|
85
86
|
return
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
logger.info(f"WebSocket connection for device {device_id}")
|
|
88
89
|
|
|
89
90
|
# Debug: Save stream to file for analysis (controlled by DEBUG_SAVE_VIDEO_STREAM env var)
|
|
90
91
|
debug_file = None
|
|
@@ -95,34 +96,34 @@ async def video_stream_ws(
|
|
|
95
96
|
debug_dir / f"{device_id}_{int(__import__('time').time())}.h264"
|
|
96
97
|
)
|
|
97
98
|
debug_file = open(debug_file_path, "wb")
|
|
98
|
-
|
|
99
|
+
logger.debug(f"DEBUG: Saving stream to {debug_file_path}")
|
|
99
100
|
|
|
100
101
|
if device_id not in scrcpy_locks:
|
|
101
102
|
scrcpy_locks[device_id] = asyncio.Lock()
|
|
102
103
|
|
|
103
104
|
async with scrcpy_locks[device_id]:
|
|
104
105
|
if device_id not in scrcpy_streamers:
|
|
105
|
-
|
|
106
|
+
logger.info(f"Creating streamer for device {device_id}")
|
|
106
107
|
scrcpy_streamers[device_id] = ScrcpyStreamer(
|
|
107
108
|
device_id=device_id, max_size=1280, bit_rate=4_000_000
|
|
108
109
|
)
|
|
109
110
|
|
|
110
111
|
try:
|
|
111
|
-
|
|
112
|
+
logger.info(f"Starting scrcpy server for device {device_id}")
|
|
112
113
|
await scrcpy_streamers[device_id].start()
|
|
113
|
-
|
|
114
|
+
logger.info(f"Scrcpy server started for device {device_id}")
|
|
114
115
|
|
|
115
116
|
# Read NAL units until we have SPS, PPS, and IDR
|
|
116
117
|
streamer = scrcpy_streamers[device_id]
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
logger.debug("Reading NAL units for initialization...")
|
|
119
120
|
for attempt in range(20): # Max 20 NAL units for initialization
|
|
120
121
|
try:
|
|
121
122
|
nal_unit = await streamer.read_nal_unit(auto_cache=True)
|
|
122
123
|
nal_type = nal_unit[4] & 0x1F if len(nal_unit) > 4 else -1
|
|
123
124
|
nal_type_names = {5: "IDR", 7: "SPS", 8: "PPS"}
|
|
124
|
-
|
|
125
|
-
f"
|
|
125
|
+
logger.debug(
|
|
126
|
+
f"Read NAL unit: type={nal_type_names.get(nal_type, nal_type)}, size={len(nal_unit)} bytes"
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
# Check if we have all required parameter sets
|
|
@@ -131,12 +132,12 @@ async def video_stream_ws(
|
|
|
131
132
|
and streamer.cached_pps
|
|
132
133
|
and streamer.cached_idr
|
|
133
134
|
):
|
|
134
|
-
|
|
135
|
-
f"
|
|
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"
|
|
136
137
|
)
|
|
137
138
|
break
|
|
138
139
|
except Exception as e:
|
|
139
|
-
|
|
140
|
+
logger.warning(f"Failed to read NAL unit: {e}")
|
|
140
141
|
await asyncio.sleep(0.5)
|
|
141
142
|
continue
|
|
142
143
|
|
|
@@ -149,8 +150,8 @@ async def video_stream_ws(
|
|
|
149
150
|
|
|
150
151
|
# Send initialization data as ONE message (SPS+PPS+IDR combined)
|
|
151
152
|
await websocket.send_bytes(init_data)
|
|
152
|
-
|
|
153
|
-
f"
|
|
153
|
+
logger.debug(
|
|
154
|
+
f"✓ Sent initialization data to first client: {len(init_data)} bytes total"
|
|
154
155
|
)
|
|
155
156
|
|
|
156
157
|
# Debug: Save to file
|
|
@@ -159,10 +160,7 @@ async def video_stream_ws(
|
|
|
159
160
|
debug_file.flush()
|
|
160
161
|
|
|
161
162
|
except Exception as e:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
print(f"[video/stream] Failed to start streamer: {e}")
|
|
165
|
-
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
163
|
+
logger.exception(f"Failed to start streamer: {e}")
|
|
166
164
|
scrcpy_streamers[device_id].stop()
|
|
167
165
|
del scrcpy_streamers[device_id]
|
|
168
166
|
try:
|
|
@@ -171,7 +169,7 @@ async def video_stream_ws(
|
|
|
171
169
|
pass
|
|
172
170
|
return
|
|
173
171
|
else:
|
|
174
|
-
|
|
172
|
+
logger.info(f"Reusing streamer for device {device_id}")
|
|
175
173
|
|
|
176
174
|
streamer = scrcpy_streamers[device_id]
|
|
177
175
|
# CRITICAL: Send complete initialization data (SPS+PPS+IDR)
|
|
@@ -183,29 +181,29 @@ async def video_stream_ws(
|
|
|
183
181
|
init_data = streamer.get_initialization_data()
|
|
184
182
|
if init_data:
|
|
185
183
|
break
|
|
186
|
-
|
|
187
|
-
f"
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Waiting for initialization data (attempt {attempt + 1}/10)..."
|
|
188
186
|
)
|
|
189
187
|
await asyncio.sleep(0.5)
|
|
190
188
|
|
|
191
189
|
if init_data:
|
|
192
190
|
# Log what we're sending
|
|
193
|
-
|
|
194
|
-
f"
|
|
191
|
+
logger.debug(
|
|
192
|
+
f"✓ Sending cached initialization data for device {device_id}:"
|
|
195
193
|
)
|
|
196
|
-
|
|
194
|
+
logger.debug(
|
|
197
195
|
f" - SPS: {len(streamer.cached_sps) if streamer.cached_sps else 0}B"
|
|
198
196
|
)
|
|
199
|
-
|
|
197
|
+
logger.debug(
|
|
200
198
|
f" - PPS: {len(streamer.cached_pps) if streamer.cached_pps else 0}B"
|
|
201
199
|
)
|
|
202
|
-
|
|
200
|
+
logger.debug(
|
|
203
201
|
f" - IDR: {len(streamer.cached_idr) if streamer.cached_idr else 0}B"
|
|
204
202
|
)
|
|
205
|
-
|
|
203
|
+
logger.debug(f" - Total: {len(init_data)} bytes")
|
|
206
204
|
|
|
207
205
|
await websocket.send_bytes(init_data)
|
|
208
|
-
|
|
206
|
+
logger.debug("✓ Initialization data sent successfully")
|
|
209
207
|
|
|
210
208
|
# Debug: Save to file
|
|
211
209
|
if debug_file:
|
|
@@ -213,7 +211,7 @@ async def video_stream_ws(
|
|
|
213
211
|
debug_file.flush()
|
|
214
212
|
else:
|
|
215
213
|
error_msg = f"Initialization data not ready for device {device_id} after 5 seconds"
|
|
216
|
-
|
|
214
|
+
logger.error(f"ERROR: {error_msg}")
|
|
217
215
|
try:
|
|
218
216
|
await websocket.send_json({"error": error_msg})
|
|
219
217
|
except Exception:
|
|
@@ -239,11 +237,9 @@ async def video_stream_ws(
|
|
|
239
237
|
|
|
240
238
|
nal_count += 1
|
|
241
239
|
if nal_count % 100 == 0:
|
|
242
|
-
|
|
243
|
-
f"[video/stream] Device {device_id}: Sent {nal_count} NAL units"
|
|
244
|
-
)
|
|
240
|
+
logger.debug(f"Device {device_id}: Sent {nal_count} NAL units")
|
|
245
241
|
except ConnectionError as e:
|
|
246
|
-
|
|
242
|
+
logger.warning(f"Device {device_id}: Connection error: {e}")
|
|
247
243
|
stream_failed = True
|
|
248
244
|
try:
|
|
249
245
|
await websocket.send_json({"error": f"Stream error: {str(e)}"})
|
|
@@ -252,12 +248,9 @@ async def video_stream_ws(
|
|
|
252
248
|
break
|
|
253
249
|
|
|
254
250
|
except WebSocketDisconnect:
|
|
255
|
-
|
|
251
|
+
logger.info(f"Device {device_id}: Client disconnected")
|
|
256
252
|
except Exception as e:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
print(f"[video/stream] Device {device_id}: Error: {e}")
|
|
260
|
-
print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
|
|
253
|
+
logger.exception(f"Device {device_id}: Error: {e}")
|
|
261
254
|
stream_failed = True
|
|
262
255
|
try:
|
|
263
256
|
await websocket.send_json({"error": str(e)})
|
|
@@ -267,13 +260,13 @@ async def video_stream_ws(
|
|
|
267
260
|
if stream_failed:
|
|
268
261
|
async with scrcpy_locks[device_id]:
|
|
269
262
|
if device_id in scrcpy_streamers:
|
|
270
|
-
|
|
263
|
+
logger.info(f"Resetting streamer for device {device_id}")
|
|
271
264
|
scrcpy_streamers[device_id].stop()
|
|
272
265
|
del scrcpy_streamers[device_id]
|
|
273
266
|
|
|
274
267
|
# Debug: Close file
|
|
275
268
|
if debug_file:
|
|
276
269
|
debug_file.close()
|
|
277
|
-
|
|
270
|
+
logger.debug("DEBUG: Closed debug file")
|
|
278
271
|
|
|
279
|
-
|
|
272
|
+
logger.info(f"Device {device_id}: Stream ended")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""配置文件管理模块."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from AutoGLM_GUI.logger import logger
|
|
7
|
+
|
|
8
|
+
# 默认配置
|
|
9
|
+
DEFAULT_CONFIG = {"base_url": "", "model_name": "autoglm-phone-9b", "api_key": "EMPTY"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_config_path() -> Path:
|
|
13
|
+
"""获取配置文件路径.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Path: 配置文件路径 (~/.config/autoglm/config.json)
|
|
17
|
+
"""
|
|
18
|
+
config_dir = Path.home() / ".config" / "autoglm"
|
|
19
|
+
return config_dir / "config.json"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_config_file() -> dict | None:
|
|
23
|
+
"""从文件加载配置.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
dict | None: 配置字典,如果文件不存在或加载失败则返回 None
|
|
27
|
+
"""
|
|
28
|
+
config_path = get_config_path()
|
|
29
|
+
|
|
30
|
+
# 文件不存在(首次运行)
|
|
31
|
+
if not config_path.exists():
|
|
32
|
+
logger.debug(f"Config file not found at {config_path}")
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
37
|
+
config = json.load(f)
|
|
38
|
+
logger.info(f"Loaded configuration from {config_path}")
|
|
39
|
+
return config
|
|
40
|
+
except json.JSONDecodeError as e:
|
|
41
|
+
logger.warning(f"Failed to parse config file {config_path}: {e}")
|
|
42
|
+
return None
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.error(f"Failed to read config file {config_path}: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_config_file(config: dict) -> bool:
|
|
49
|
+
"""保存配置到文件(原子写入).
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
config: 配置字典
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: 成功返回 True,失败返回 False
|
|
56
|
+
"""
|
|
57
|
+
config_path = get_config_path()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# 确保目录存在
|
|
61
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
# 原子写入:先写入临时文件,然后重命名
|
|
64
|
+
temp_path = config_path.with_suffix(".tmp")
|
|
65
|
+
with open(temp_path, "w", encoding="utf-8") as f:
|
|
66
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
67
|
+
|
|
68
|
+
# 重命名(原子操作)
|
|
69
|
+
temp_path.replace(config_path)
|
|
70
|
+
|
|
71
|
+
logger.info(f"Configuration saved to {config_path}")
|
|
72
|
+
return True
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Failed to save config file {config_path}: {e}")
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def delete_config_file() -> bool:
|
|
79
|
+
"""删除配置文件.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
bool: 成功返回 True,失败返回 False
|
|
83
|
+
"""
|
|
84
|
+
config_path = get_config_path()
|
|
85
|
+
|
|
86
|
+
if not config_path.exists():
|
|
87
|
+
logger.debug(f"Config file does not exist: {config_path}")
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
config_path.unlink()
|
|
92
|
+
logger.info(f"Configuration deleted: {config_path}")
|
|
93
|
+
return True
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Failed to delete config file {config_path}: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def merge_configs(file_config: dict | None, cli_config: dict | None) -> dict:
|
|
100
|
+
"""合并配置(优先级:CLI > 文件 > 默认值).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
file_config: 从文件加载的配置(可为 None)
|
|
104
|
+
cli_config: CLI 参数配置(可为 None)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict: 合并后的配置字典
|
|
108
|
+
"""
|
|
109
|
+
# 从默认配置开始
|
|
110
|
+
merged = DEFAULT_CONFIG.copy()
|
|
111
|
+
|
|
112
|
+
# 应用文件配置(如果存在)
|
|
113
|
+
if file_config:
|
|
114
|
+
for key in DEFAULT_CONFIG.keys():
|
|
115
|
+
if key in file_config:
|
|
116
|
+
merged[key] = file_config[key]
|
|
117
|
+
|
|
118
|
+
# 应用 CLI 配置(最高优先级)
|
|
119
|
+
if cli_config:
|
|
120
|
+
for key in DEFAULT_CONFIG.keys():
|
|
121
|
+
if key in cli_config:
|
|
122
|
+
merged[key] = cli_config[key]
|
|
123
|
+
|
|
124
|
+
return merged
|
AutoGLM_GUI/logger.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized logging configuration using loguru.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
# Remove default handler
|
|
10
|
+
logger.remove()
|
|
11
|
+
|
|
12
|
+
# Default configuration - will be overridden by configure_logger()
|
|
13
|
+
_configured = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configure_logger(
|
|
17
|
+
console_level: str = "INFO",
|
|
18
|
+
log_file: str | None = "logs/autoglm_{time:YYYY-MM-DD}.log",
|
|
19
|
+
log_level: str = "DEBUG",
|
|
20
|
+
rotation: str = "100 MB",
|
|
21
|
+
retention: str = "7 days",
|
|
22
|
+
compression: str = "zip",
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Configure the global logger with console and file handlers.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
console_level: Console output level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
29
|
+
log_file: Log file path (None to disable file logging)
|
|
30
|
+
log_level: File logging level
|
|
31
|
+
rotation: Log rotation policy (e.g., "100 MB", "1 day")
|
|
32
|
+
retention: Log retention policy (e.g., "7 days", "1 week")
|
|
33
|
+
compression: Compression format for rotated logs (e.g., "zip", "gz")
|
|
34
|
+
"""
|
|
35
|
+
global _configured
|
|
36
|
+
|
|
37
|
+
# Remove existing handlers if reconfiguring
|
|
38
|
+
if _configured:
|
|
39
|
+
logger.remove()
|
|
40
|
+
|
|
41
|
+
# Console handler with colors
|
|
42
|
+
logger.add(
|
|
43
|
+
sys.stderr,
|
|
44
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
|
45
|
+
level=console_level,
|
|
46
|
+
colorize=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# File handler
|
|
50
|
+
if log_file:
|
|
51
|
+
# Create logs directory if it doesn't exist
|
|
52
|
+
log_path = Path(log_file)
|
|
53
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
logger.add(
|
|
56
|
+
log_file,
|
|
57
|
+
rotation=rotation,
|
|
58
|
+
retention=retention,
|
|
59
|
+
compression=compression,
|
|
60
|
+
level=log_level,
|
|
61
|
+
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
62
|
+
encoding="utf-8",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Separate error log file
|
|
66
|
+
error_file = str(log_path.parent / f"errors_{log_path.name.split('_', 1)[1]}")
|
|
67
|
+
logger.add(
|
|
68
|
+
error_file,
|
|
69
|
+
rotation="50 MB",
|
|
70
|
+
retention="30 days",
|
|
71
|
+
compression=compression,
|
|
72
|
+
level="ERROR",
|
|
73
|
+
format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {name}:{function}:{line} - {message}",
|
|
74
|
+
backtrace=True,
|
|
75
|
+
diagnose=True,
|
|
76
|
+
encoding="utf-8",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_configured = True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Default initialization (can be reconfigured later)
|
|
83
|
+
configure_logger()
|
|
84
|
+
|
|
85
|
+
__all__ = ["logger", "configure_logger"]
|
AutoGLM_GUI/platform_utils.py
CHANGED
|
@@ -12,18 +12,24 @@ def is_windows() -> bool:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
async def run_cmd_silently(cmd: Sequence[str]) -> subprocess.CompletedProcess:
|
|
15
|
-
"""Run a command, suppressing output; safe for async contexts on all platforms."""
|
|
15
|
+
"""Run a command, suppressing output but preserving it in the result; safe for async contexts on all platforms."""
|
|
16
16
|
if is_windows():
|
|
17
17
|
# Avoid blocking the event loop with a blocking subprocess call on Windows.
|
|
18
18
|
return await asyncio.to_thread(
|
|
19
|
-
subprocess.run, cmd, capture_output=True, check=False
|
|
19
|
+
subprocess.run, cmd, capture_output=True, text=True, check=False
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
# Use PIPE on macOS/Linux to capture output
|
|
22
23
|
process = await asyncio.create_subprocess_exec(
|
|
23
|
-
*cmd, stdout=subprocess.
|
|
24
|
+
*cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
24
25
|
)
|
|
25
|
-
await process.
|
|
26
|
-
|
|
26
|
+
stdout, stderr = await process.communicate()
|
|
27
|
+
# Decode bytes to string for API consistency across platforms
|
|
28
|
+
stdout_str = stdout.decode("utf-8") if stdout else ""
|
|
29
|
+
stderr_str = stderr.decode("utf-8") if stderr else ""
|
|
30
|
+
# Return CompletedProcess with stdout/stderr for API consistency across platforms
|
|
31
|
+
return_code = process.returncode if process.returncode is not None else -1
|
|
32
|
+
return subprocess.CompletedProcess(cmd, return_code, stdout_str, stderr_str)
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
async def spawn_process(cmd: Sequence[str], *, capture_output: bool = False) -> Any:
|