myagent-ai 1.18.1 → 1.18.2

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.
@@ -79,6 +79,8 @@ class VNCManager:
79
79
 
80
80
  # 状态标记
81
81
  self._running = False
82
+ self._xvfb_attached = False # 是否附加到已有的 Xvfb 实例
83
+ self._xvfb_attached_pid: Optional[int] = None # 附加的 Xvfb PID
82
84
  self._lock = asyncio.Lock()
83
85
 
84
86
  # noVNC web 文件路径(用于 aiohttp 静态文件服务)
@@ -88,10 +90,27 @@ class VNCManager:
88
90
  def is_running(self) -> bool:
89
91
  """VNC 服务是否正在运行"""
90
92
  if not self._running:
91
- return False
93
+ # 即使内部标记为未运行,也检查系统上是否有 VNC 服务
94
+ # (myagent 重启后 _running 会重置为 False)
95
+ if self._detect_existing_services():
96
+ self._running = True
97
+ logger.info("检测到已有的 VNC 服务正在运行")
98
+ else:
99
+ return False
100
+
92
101
  # 检查各进程是否存活
102
+ # Xvfb: 如果是附加的实例,通过 PID 检查
103
+ if self._xvfb_attached:
104
+ if self._xvfb_attached_pid and not self._is_pid_alive(self._xvfb_attached_pid):
105
+ logger.warning("附加的 Xvfb 进程已退出")
106
+ self._running = False
107
+ return False
108
+ elif self._xvfb_process and self._xvfb_process.poll() is not None:
109
+ logger.warning(f"Xvfb 进程已退出 (exit code: {self._xvfb_process.returncode})")
110
+ self._running = False
111
+ return False
112
+
93
113
  for proc, name in [
94
- (self._xvfb_process, "Xvfb"),
95
114
  (self._x11vnc_process, "x11vnc"),
96
115
  (self._websockify_process, "websockify"),
97
116
  ]:
@@ -101,6 +120,72 @@ class VNCManager:
101
120
  return False
102
121
  return True
103
122
 
123
+ def _is_pid_alive(self, pid: int) -> bool:
124
+ """检查进程是否存活"""
125
+ try:
126
+ os.kill(pid, 0) # 发送信号 0,不实际杀死进程
127
+ return True
128
+ except (OSError, ProcessLookupError):
129
+ return False
130
+
131
+ def _detect_existing_services(self) -> bool:
132
+ """检测系统上是否已有 VNC 相关服务在运行(用于 myagent 重启后恢复状态)"""
133
+ display_num = self.display.replace(":", "")
134
+ socket_path = f"/tmp/.X11-unix/X{display_num}"
135
+
136
+ # 检查 Xvfb socket 和进程
137
+ xvfb_ok = False
138
+ if os.path.exists(socket_path):
139
+ try:
140
+ result = subprocess.run(
141
+ ["pgrep", "-f", f"Xvfb {self.display}"],
142
+ capture_output=True, text=True, timeout=5,
143
+ )
144
+ if result.returncode == 0 and result.stdout.strip():
145
+ xvfb_ok = True
146
+ except Exception:
147
+ pass
148
+
149
+ if not xvfb_ok:
150
+ return False
151
+
152
+ # 检查 x11vnc 是否在监听端口
153
+ x11vnc_ok = self._is_port_listening(self.x11vnc_port)
154
+ if not x11vnc_ok:
155
+ return False
156
+
157
+ # 检查 websockify 是否在监听端口
158
+ websockify_ok = self._is_port_listening(self.novnc_port)
159
+
160
+ if xvfb_ok and x11vnc_ok and websockify_ok:
161
+ # 恢复附加状态
162
+ self._xvfb_attached = True
163
+ try:
164
+ result = subprocess.run(
165
+ ["pgrep", "-f", f"Xvfb {self.display}"],
166
+ capture_output=True, text=True, timeout=5,
167
+ )
168
+ if result.returncode == 0:
169
+ self._xvfb_attached_pid = int(result.stdout.strip().split()[0])
170
+ except Exception:
171
+ pass
172
+ return True
173
+
174
+ return False
175
+
176
+ @staticmethod
177
+ def _is_port_listening(port: int) -> bool:
178
+ """检查端口是否在监听"""
179
+ try:
180
+ import socket
181
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
182
+ s.settimeout(0.5)
183
+ s.connect(("127.0.0.1", port))
184
+ s.close()
185
+ return True
186
+ except (OSError, ConnectionRefusedError):
187
+ return False
188
+
104
189
  @property
105
190
  def display_env(self) -> Dict[str, str]:
106
191
  """返回 DISPLAY 环境变量字典,供子进程使用"""
@@ -109,29 +194,39 @@ class VNCManager:
109
194
  @property
110
195
  def status(self) -> Dict[str, Any]:
111
196
  """获取当前状态信息"""
112
- xvfb_alive = self._xvfb_process is not None and self._xvfb_process.poll() is None
197
+ running = self.is_running
198
+
199
+ # Xvfb: 附加模式 vs 自启动模式
200
+ if self._xvfb_attached:
201
+ xvfb_alive = self._xvfb_attached_pid is not None and self._is_pid_alive(self._xvfb_attached_pid)
202
+ xvfb_pid = self._xvfb_attached_pid if xvfb_alive else None
203
+ else:
204
+ xvfb_alive = self._xvfb_process is not None and self._xvfb_process.poll() is None
205
+ xvfb_pid = self._xvfb_process.pid if xvfb_alive else None
206
+
113
207
  vnc_alive = self._x11vnc_process is not None and self._x11vnc_process.poll() is None
114
208
  ws_alive = self._websockify_process is not None and self._websockify_process.poll() is None
115
209
 
116
210
  return {
117
- "running": self.is_running,
211
+ "running": running,
118
212
  "display": self.display,
119
213
  "resolution": self.resolution,
120
214
  "xvfb": {
121
- "alive": xvfb_alive,
122
- "pid": self._xvfb_process.pid if xvfb_alive else None,
215
+ "alive": xvfb_alive or running, # 如果整体在运行,Xvfb 肯定是活的
216
+ "pid": xvfb_pid,
217
+ "attached": self._xvfb_attached,
123
218
  },
124
219
  "x11vnc": {
125
- "alive": vnc_alive,
220
+ "alive": vnc_alive or running,
126
221
  "port": self.x11vnc_port,
127
222
  "pid": self._x11vnc_process.pid if vnc_alive else None,
128
223
  },
129
224
  "websockify": {
130
- "alive": ws_alive,
225
+ "alive": ws_alive or running,
131
226
  "port": self.novnc_port,
132
227
  "pid": self._websockify_process.pid if ws_alive else None,
133
228
  },
134
- "novnc_url": f"/vnc/vnc.html?autoconnect=true&resize=scale" if self.is_running else None,
229
+ "novnc_url": f"/vnc/vnc.html?autoconnect=true&resize=scale" if running else None,
135
230
  }
136
231
 
137
232
  def get_novnc_url(self, base_url: str = "") -> Optional[str]:
@@ -280,7 +375,7 @@ class VNCManager:
280
375
  """启动 VNC 远程桌面服务。
281
376
 
282
377
  启动顺序:
283
- 1. Xvfb (虚拟显示)
378
+ 1. Xvfb (虚拟显示) — 如果已在运行则附加
284
379
  2. x11vnc (VNC 服务器)
285
380
  3. websockify (WebSocket 代理)
286
381
 
@@ -288,7 +383,9 @@ class VNCManager:
288
383
  {"success": bool, "message": str, "status": dict}
289
384
  """
290
385
  async with self._lock:
291
- if self._running:
386
+ # 检查是否已在运行(包括检测系统上已有的服务)
387
+ if self._running or self._detect_existing_services():
388
+ self._running = True
292
389
  return {
293
390
  "success": True,
294
391
  "message": "VNC 服务已在运行中",
@@ -300,31 +397,7 @@ class VNCManager:
300
397
  if not ok:
301
398
  return {"success": False, "message": f"依赖检查失败: {dep_msg}"}
302
399
 
303
- # Step 2: 检查显示号是否已被占用
304
- display_num = self.display.replace(":", "")
305
- lock_file = f"/tmp/.X{display_num}-lock"
306
- if os.path.exists(lock_file):
307
- # 检查是否有 Xvfb 进程在运行
308
- try:
309
- result = subprocess.run(
310
- ["pgrep", "-f", f"Xvfb {self.display}"],
311
- capture_output=True, text=True, timeout=5,
312
- )
313
- if result.returncode == 0 and result.stdout.strip():
314
- logger.info(f"Xvfb {self.display} 已在运行,跳过启动")
315
- else:
316
- # 锁文件存在但进程不在,清理锁文件
317
- try:
318
- os.remove(lock_file)
319
- socket_file = f"/tmp/.X11-unix/X{display_num}"
320
- if os.path.exists(socket_file):
321
- os.remove(socket_file)
322
- except OSError:
323
- pass
324
- except Exception:
325
- pass
326
-
327
- # Step 3: 启动 Xvfb
400
+ # Step 2: 启动或附加 Xvfb
328
401
  xvfb_ok = await self._start_xvfb()
329
402
  if not xvfb_ok:
330
403
  await self.stop()
@@ -333,7 +406,7 @@ class VNCManager:
333
406
  # 等待 Xvfb 就绪
334
407
  await asyncio.sleep(1)
335
408
 
336
- # Step 4: 启动 x11vnc
409
+ # Step 3: 启动 x11vnc
337
410
  vnc_ok = await self._start_x11vnc()
338
411
  if not vnc_ok:
339
412
  await self.stop()
@@ -342,13 +415,13 @@ class VNCManager:
342
415
  # 等待 x11vnc 就绪
343
416
  await asyncio.sleep(1)
344
417
 
345
- # Step 5: 启动 websockify
418
+ # Step 4: 启动 websockify
346
419
  ws_ok = await self._start_websockify()
347
420
  if not ws_ok:
348
421
  await self.stop()
349
422
  return {"success": False, "message": "websockify 启动失败", "status": self.status}
350
423
 
351
- # Step 6: 查找 noVNC web 文件
424
+ # Step 5: 查找 noVNC web 文件
352
425
  self._novnc_web_dir = self._find_novnc_dir()
353
426
  if self._novnc_web_dir:
354
427
  logger.info(f"noVNC web 文件: {self._novnc_web_dir}")
@@ -383,20 +456,27 @@ class VNCManager:
383
456
  errors.extend(await self._kill_process(self._x11vnc_process, "x11vnc"))
384
457
  self._x11vnc_process = None
385
458
 
459
+ # Xvfb: 只有自己启动的才杀,附加的不杀
386
460
  if self._xvfb_process:
387
461
  errors.extend(await self._kill_process(self._xvfb_process, "Xvfb"))
388
462
  self._xvfb_process = None
389
-
390
- # 清理锁文件和 socket
391
- display_num = self.display.replace(":", "")
392
- for f in [f"/tmp/.X{display_num}-lock", f"/tmp/.X11-unix/X{display_num}"]:
393
- try:
394
- if os.path.exists(f):
395
- os.remove(f)
396
- except OSError:
397
- pass
463
+ elif self._xvfb_attached:
464
+ logger.info(f"Xvfb {self.display} 是附加到已有实例,不停止 (PID={self._xvfb_attached_pid})")
465
+ self._xvfb_attached = False
466
+ self._xvfb_attached_pid = None
467
+
468
+ # 清理锁文件和 socket (只清理自己启动的)
469
+ if not self._xvfb_attached:
470
+ display_num = self.display.replace(":", "")
471
+ for f in [f"/tmp/.X{display_num}-lock", f"/tmp/.X11-unix/X{display_num}"]:
472
+ try:
473
+ if os.path.exists(f):
474
+ os.remove(f)
475
+ except OSError:
476
+ pass
398
477
 
399
478
  self._running = False
479
+ self._xvfb_attached = False
400
480
 
401
481
  if errors:
402
482
  return {"success": True, "message": f"VNC 已停止 (部分警告: {'; '.join(errors)})"}
@@ -489,8 +569,40 @@ class VNCManager:
489
569
  # ── 内部方法 ──────────────────────────────────────────
490
570
 
491
571
  async def _start_xvfb(self) -> bool:
492
- """启动 Xvfb 虚拟显示服务器"""
572
+ """启动或附加到 Xvfb 虚拟显示服务器"""
493
573
  try:
574
+ display_num = self.display.replace(":", "")
575
+ socket_path = f"/tmp/.X11-unix/X{display_num}"
576
+
577
+ # 检查 Xvfb 是否已在运行
578
+ if os.path.exists(socket_path):
579
+ try:
580
+ result = subprocess.run(
581
+ ["pgrep", "-f", f"Xvfb {self.display}"],
582
+ capture_output=True, text=True, timeout=5,
583
+ )
584
+ if result.returncode == 0 and result.stdout.strip():
585
+ # Xvfb 已在运行,附加到它
586
+ pid = int(result.stdout.strip().split()[0])
587
+ self._xvfb_attached = True
588
+ self._xvfb_attached_pid = pid
589
+ logger.info(f"Xvfb {self.display} 已在运行 (PID={pid}),附加到现有实例")
590
+ return True
591
+ else:
592
+ # 锁文件/socket 存在但进程不在,清理
593
+ logger.info(f"Xvfb {self.display} socket 存在但进程不在,清理残留文件")
594
+ lock_file = f"/tmp/.X{display_num}-lock"
595
+ try:
596
+ os.remove(lock_file)
597
+ except OSError:
598
+ pass
599
+ try:
600
+ os.remove(socket_path)
601
+ except OSError:
602
+ pass
603
+ except Exception as e:
604
+ logger.warning(f"检查 Xvfb 状态异常: {e}")
605
+
494
606
  cmd = [
495
607
  "Xvfb",
496
608
  self.display,
@@ -529,8 +641,6 @@ class VNCManager:
529
641
  return False
530
642
 
531
643
  # 验证 X socket 是否已创建
532
- display_num = self.display.replace(":", "")
533
- socket_path = f"/tmp/.X11-unix/X{display_num}"
534
644
  for _ in range(10):
535
645
  if os.path.exists(socket_path):
536
646
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.18.1",
3
+ "version": "1.18.2",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -942,8 +942,18 @@ function updateVNCButton() {
942
942
  }
943
943
 
944
944
  async function toggleVNC() {
945
+ // 先刷新一次状态(避免因轮询间隔导致状态过期)
946
+ try {
947
+ var latest = await api('/api/vnc/status');
948
+ if (latest && latest.running) {
949
+ vncStatus = latest;
950
+ updateVNCButton();
951
+ openVNCWindow();
952
+ return;
953
+ }
954
+ } catch (e) {}
955
+
945
956
  if (vncStatus.running) {
946
- // VNC 正在运行,打开远程桌面窗口
947
957
  openVNCWindow();
948
958
  return;
949
959
  }
@@ -957,12 +967,18 @@ async function toggleVNC() {
957
967
  try {
958
968
  var result = await api('/api/vnc/start', { method: 'POST' });
959
969
  if (result.success) {
960
- toast('远程桌面已启动', 'success');
961
970
  await refreshVNCStatus();
962
- // 延迟打开远程桌面窗口(等待服务完全就绪)
963
- setTimeout(function() {
971
+ // 如果是"已在运行"(附加到已有实例),立刻打开窗口
972
+ if (result.message && result.message.indexOf('已在运行') > -1) {
973
+ toast('远程桌面已连接', 'success');
964
974
  openVNCWindow();
965
- }, 2000);
975
+ } else {
976
+ toast('远程桌面已启动', 'success');
977
+ // 新启动的需要等服务完全就绪
978
+ setTimeout(function() {
979
+ openVNCWindow();
980
+ }, 2000);
981
+ }
966
982
  } else {
967
983
  toast('启动失败: ' + (result.message || '未知错误'), 'error');
968
984
  }