myagent-ai 1.20.5 → 1.20.7

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.
@@ -371,6 +371,62 @@ class VNCManager:
371
371
 
372
372
  return None
373
373
 
374
+ def _ensure_dev_shm(self) -> None:
375
+ """[v1.20.5] 检查并修复 /dev/shm 挂载(Termux proot-distro 环境下常见问题)。
376
+
377
+ Android 对 SysV IPC / 共享内存有限制,导致 x11vnc 的 shmget() 失败。
378
+ 即使加了 -noshm,某些 x11vnc 版本仍会尝试访问 /dev/shm,
379
+ 所以提前确保目录存在且有正确权限。
380
+ """
381
+ shm_path = "/dev/shm"
382
+ try:
383
+ if not os.path.exists(shm_path):
384
+ logger.info(f"/dev/shm 不存在,尝试创建...")
385
+ os.makedirs(shm_path, exist_ok=True)
386
+ try:
387
+ os.chmod(shm_path, 0o1777)
388
+ logger.info("/dev/shm 创建成功,权限设为 1777")
389
+ except OSError as e:
390
+ logger.warning(f"/dev/shm chmod 失败 (proot 环境可能无权限): {e}")
391
+ else:
392
+ # 检查权限
393
+ st = os.stat(shm_path)
394
+ if st.st_mode & 0o777 != 0o777:
395
+ try:
396
+ os.chmod(shm_path, 0o1777)
397
+ logger.info(f"/dev/shm 权限已修正为 1777")
398
+ except OSError as e:
399
+ logger.warning(f"/dev/shm chmod 失败: {e}")
400
+ except Exception as e:
401
+ logger.warning(f"/dev/shm 检查异常: {e}")
402
+
403
+ def _kill_port_users(self, port: int) -> None:
404
+ """[v1.20.5] 强制清理占用指定端口的残留进程(Termux 环境下进程管理不彻底)。
405
+
406
+ 依次尝试: fuser -k → pkill -9 → fallback pgrep+kill
407
+ """
408
+ # 方法1: fuser -k(最彻底,直接杀掉占用端口的进程)
409
+ try:
410
+ result = subprocess.run(
411
+ ["fuser", "-k", f"{port}/tcp"],
412
+ capture_output=True, text=True, timeout=5,
413
+ )
414
+ if result.returncode == 0:
415
+ logger.info(f"fuser -k {port}/tcp 执行成功")
416
+ except (FileNotFoundError, Exception) as e:
417
+ logger.debug(f"fuser -k 不可用或失败: {e}")
418
+
419
+ def _kill_process_by_name(self, name: str) -> None:
420
+ """[v1.20.5] 按进程名强制杀死所有匹配进程(pkill -9)。"""
421
+ try:
422
+ subprocess.run(
423
+ ["pkill", "-9", name],
424
+ capture_output=True, text=True, timeout=5,
425
+ )
426
+ logger.debug(f"pkill -9 {name} 已执行")
427
+ except (FileNotFoundError, Exception) as e:
428
+ logger.debug(f"pkill -9 {name} 失败: {e}")
429
+
374
430
  async def start(self) -> Dict[str, Any]:
375
431
  """启动 VNC 远程桌面服务。
376
432
 
@@ -397,6 +453,9 @@ class VNCManager:
397
453
  if not ok:
398
454
  return {"success": False, "message": f"依赖检查失败: {dep_msg}"}
399
455
 
456
+ # Step 1.5: [v1.20.5] 检查 /dev/shm 挂载(Termux proot-distro 兼容)
457
+ self._ensure_dev_shm()
458
+
400
459
  # Step 2: 启动或附加 Xvfb
401
460
  xvfb_ok = await self._start_xvfb()
402
461
  if not xvfb_ok:
@@ -661,89 +720,67 @@ class VNCManager:
661
720
  async def _start_x11vnc(self) -> bool:
662
721
  """启动 x11vnc VNC 服务器"""
663
722
  try:
664
- # [v1.18.9] 启动前检查端口是否被占用,清理残留进程
723
+ # [v1.20.5] 增强端口清理:fuser -k + pkill -9(Termux 环境下进程管理不彻底)
665
724
  if self._is_port_listening(self.x11vnc_port):
666
- logger.warning(f"端口 {self.x11vnc_port} 已被占用,尝试清理残留 x11vnc 进程...")
667
- try:
668
- _result = subprocess.run(
669
- ["pgrep", "-f", f"x11vnc.*{self.display}"],
670
- capture_output=True, text=True, timeout=5,
671
- )
672
- if _result.returncode == 0 and _result.stdout.strip():
673
- for _pid_str in _result.stdout.strip().split("\n"):
674
- try:
675
- _pid = int(_pid_str.strip())
676
- os.kill(_pid, signal.SIGTERM)
677
- logger.info(f"杀死残留 x11vnc PID={_pid}")
678
- except (ValueError, ProcessLookupError):
679
- pass
680
- await asyncio.sleep(1.0)
681
- # SIGTERM 后仍占用则 SIGKILL
682
- if self._is_port_listening(self.x11vnc_port):
725
+ logger.warning(f"端口 {self.x11vnc_port} 已被占用,清理残留进程...")
726
+ self._kill_port_users(self.x11vnc_port)
727
+ self._kill_process_by_name("x11vnc")
728
+ self._kill_process_by_name("Xvfb")
729
+ await asyncio.sleep(1.0)
730
+ # 二次确认
731
+ if self._is_port_listening(self.x11vnc_port):
732
+ # 回退到 pgrep + kill 方式
733
+ try:
734
+ _result = subprocess.run(
735
+ ["pgrep", "-f", f"x11vnc.*{self.display}"],
736
+ capture_output=True, text=True, timeout=5,
737
+ )
738
+ if _result.returncode == 0 and _result.stdout.strip():
683
739
  for _pid_str in _result.stdout.strip().split("\n"):
684
740
  try:
685
741
  os.kill(int(_pid_str.strip()), signal.SIGKILL)
742
+ logger.info(f"SIGKILL 残留 x11vnc PID={_pid_str.strip()}")
686
743
  except (ValueError, ProcessLookupError):
687
744
  pass
688
745
  await asyncio.sleep(0.5)
689
- if self._is_port_listening(self.x11vnc_port):
690
- logger.warning(f"端口 {self.x11vnc_port} 仍被占用,但可能是旧进程,继续尝试启动")
691
- else:
692
- logger.debug(f"端口 {self.x11vnc_port} 被占用但未找到 display={self.display} 的 x11vnc 进程")
693
- except Exception as _e:
694
- logger.debug(f"清理残留 x11vnc 异常: {_e}")
695
-
696
- # [v1.17.2] 处理 x11vnc 0.9.17+ shm-helper 兼容问题
697
- # 新版 x11vnc 编译时默认启用 shm-helper,但运行时需要绝对路径
698
- # 查找 shm-helper 绝对路径
699
- shm_helper = None
700
- shm_candidates = [
701
- "/usr/lib/x11vnc/shm-helper",
702
- "/usr/lib/x11vnc0.9/shm-helper",
703
- "/usr/libexec/x11vnc/shm-helper",
704
- "/usr/local/lib/x11vnc/shm-helper",
705
- ]
706
- for _path in shm_candidates:
707
- if os.path.isfile(_path):
708
- shm_helper = _path
709
- break
710
- if not shm_helper:
711
- import shutil as _shutil
712
- shm_helper = _shutil.which("x11vnc-shm-helper") or _shutil.which("shm-helper")
713
-
746
+ except Exception as _e:
747
+ logger.debug(f"pgrep 清理异常: {_e}")
748
+ if self._is_port_listening(self.x11vnc_port):
749
+ logger.warning(f"端口 {self.x11vnc_port} 仍被占用,可能被其他服务占用")
750
+
751
+ # [v1.20.5] x11vnc 启动参数
752
+ # 关键: -noshm 禁用共享内存 — Android/Termux proot-distro 环境下
753
+ # SysV IPC (shmget) 不受支持,不加此参数会导致 shmget failed 错误
754
+ # 注意: 参数是 -noshm(无横杠分隔),不是 -no-shm
714
755
  cmd = [
715
756
  "x11vnc",
716
757
  "-display", self.display,
717
- "-forever", # 持续运行
718
- "-shared", # 允许多个客户端同时连接
719
- "-nopw", # 无密码(本地使用)
758
+ "-noshm", # [v1.20.5] 禁用共享内存(Termux proot-distro 必需)
759
+ "-forever", # 持续运行
760
+ "-shared", # 允许多个客户端同时连接
761
+ "-nopw", # 无密码(本地使用)
720
762
  "-rfbport", str(self.x11vnc_port),
721
763
  "-listen", "127.0.0.1", # 仅本地监听(安全)
722
- "-xkb", # 使用 XKB 扩展
723
- "-noxdamage", # 禁用 XDAMAGE(兼容性更好)
724
- "-nowf", # 禁用 wireframe
725
- "-nowcr", # 禁用 cursor shape updates
764
+ "-xkb", # 使用 XKB 扩展
765
+ "-noxdamage", # 禁用 XDAMAGE(兼容性更好)
766
+ "-nowf", # 禁用 wireframe
767
+ "-nowcr", # 禁用 cursor shape updates
726
768
  "-nocursorshape",
727
- "-deferupdate", "5", # 延迟更新(降低带宽)
728
- "-scale", "2/3", # 缩小 2/3(降低带宽)
769
+ "-deferupdate", "5", # 延迟更新(降低带宽)
770
+ "-scale", "2/3", # 缩小 2/3(降低带宽)
729
771
  ]
730
772
 
731
773
  # [v1.18.5] 不使用 -threads 模式:与 -scale 组合在 0.9.17 中
732
774
  # 会导致父进程 fork 后立即退出,端口延迟监听
733
775
 
734
- # [v1.19.1] 处理 shm-helper: 找到就传绝对路径,找不到就跳过
735
- # 注意: x11vnc 0.9.17 不支持 -no-shm 参数,会报 unrecognized option
736
- if shm_helper:
737
- cmd.extend(["-shm-helper", shm_helper])
738
- logger.info(f"x11vnc shm-helper: {shm_helper}")
739
- else:
740
- logger.info("x11vnc 未找到 shm-helper,跳过 SHM 相关参数")
776
+ # [v1.20.5] 移除 shm-helper 逻辑 — 改用 -noshm 完全跳过共享内存
777
+ # 旧版本 (v1.17.2 ~ v1.20.4) 尝试查找 shm-helper 并传绝对路径,
778
+ # 但在 Termux 环境下找不到该文件会导致 "expected absolute path" 错误
741
779
 
742
780
  env = {**os.environ, "DISPLAY": self.display}
743
781
 
744
- # [v1.18.0] proot/Termux 兼容: 可能需要额外的安全参数
782
+ # [v1.18.0] proot/Termux 兼容
745
783
  cmd.append("-nobell")
746
- # 跳过 Xinerama 检查(proot 环境下可能失败)
747
784
  env["X11VNC_NO_UNIXPW"] = "1"
748
785
 
749
786
  logger.info(f"启动 x11vnc: {' '.join(cmd)}")
@@ -826,10 +863,11 @@ class VNCManager:
826
863
  except Exception:
827
864
  pass
828
865
 
829
- # 第二次尝试使用更保守的参数(去掉 -scale、-noxfixes 等可能的问题参数)
866
+ # 第二次尝试使用更保守的参数(去掉 -scale 等可能的问题参数,保留 -noshm)
830
867
  cmd = [
831
868
  "x11vnc",
832
869
  "-display", self.display,
870
+ "-noshm", # [v1.20.5] Termux proot-distro 必需
833
871
  "-forever",
834
872
  "-shared",
835
873
  "-nopw",
@@ -601,13 +601,16 @@ class WebControlManager:
601
601
  # 仅改写 HTML 属性中的 URL (不影响内联脚本)
602
602
  def rewrite_attr(match):
603
603
  attr_name = match.group(1).lower()
604
- q_char = match.group(2)
605
- url_val = match.group(3)
604
+ eq = match.group(2) # "=" (可能带空格)
605
+ q_open = match.group(3) # 开引号 " 或 '
606
+ url_val = match.group(4) # URL 值
607
+ q_close = match.group(5) # 闭引号 (同 q_open)
606
608
 
607
- if not url_val or url_val.startswith('data:') or url_val.startswith('blob:') or url_val.startswith('#') or url_val.startswith('javascript:'):
609
+ # 跳过特殊协议
610
+ if not url_val or url_val.startswith('data:') or url_val.startswith('blob:') or url_val.startswith('#') or url_val.startswith('javascript:') or url_val.startswith('mailto:') or url_val.startswith('tel:'):
608
611
  return match.group(0)
609
612
 
610
- # 已经是代理 URL
613
+ # 已经是代理 URL — 跳过
611
614
  if '/api/web_control/proxy' in url_val:
612
615
  return match.group(0)
613
616
 
@@ -619,23 +622,22 @@ class WebControlManager:
619
622
  elif not url_val.startswith('http://') and not url_val.startswith('https://'):
620
623
  url_val = urljoin(original_url, url_val)
621
624
 
622
- # 非同源资源: 某些 CDN 资源直接访问, 不走代理
625
+ # 非同源资源: CDN 图片/脚本/样式直接访问, 不走代理
623
626
  url_parsed = urlparse(url_val)
624
627
  if url_parsed.netloc and url_parsed.netloc != parsed.netloc:
625
- # 对于 src 属性的资源 (图片/脚本/样式/字体), 直接访问即可
626
628
  if attr_name in ('src', 'data-src', 'data-original'):
627
629
  return match.group(0)
628
- # 对于 href 属性的 CSS 字体等, 直接访问
629
- if attr_name == 'href' and any(url_val.endswith(ext) for ext in ('.css', '.woff', '.woff2', '.ttf', '.eot', '.svg')):
630
+ if attr_name == 'href' and any(url_val.lower().endswith(ext) for ext in ('.css', '.woff', '.woff2', '.ttf', '.eot', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico')):
630
631
  return match.group(0)
631
632
  # 其他外链 (导航链接) 走代理
632
633
  if attr_name == 'href':
633
- return f'{attr_name}={q_char}{proxy_base}{url_quote(url_val)}{q_char}'
634
+ return f'{attr_name}{eq}{q_open}{proxy_base}{url_quote(url_val)}{q_close}'
634
635
 
635
636
  # 同源资源走代理
636
- return f'{attr_name}={q_char}{proxy_base}{url_quote(url_val)}{q_char}'
637
+ return f'{attr_name}{eq}{q_open}{proxy_base}{url_quote(url_val)}{q_close}'
637
638
 
638
639
  # 匹配常见 URL 属性
640
+ # group1=属性名, group2="=", group3=开引号, group4=URL值, group5=闭引号
639
641
  url_pattern = re.compile(
640
642
  r'((?:src|href|action|data-src|data-original|poster|formaction|content|cite|background))'
641
643
  r'(\s*=\s*)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.20.5",
3
+ "version": "1.20.7",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -43,4 +43,4 @@
43
43
  "python": ">=3.10",
44
44
  "node": ">=18"
45
45
  }
46
- }
46
+ }