myagent-ai 1.20.4 → 1.20.6
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.
- package/agents/main_agent.py +157 -23
- package/core/vnc_manager.py +100 -62
- package/core/web_control.py +704 -0
- package/package.json +2 -2
- package/skills/docx_skill.py +10 -2
- package/skills/pdf_skill.py +13 -3
- package/web/api_server.py +545 -0
- package/web/ui/chat/chat.css +26 -4
- package/web/ui/chat/chat_container.html +22 -1
- package/web/ui/chat/chat_main.js +130 -1
- package/web/ui/chat/flow_engine.js +21 -0
- package/web/ui/index.html +42 -7
- package/worklog.md +61 -0
package/agents/main_agent.py
CHANGED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import re
|
|
11
12
|
from typing import Any, Callable, Dict, List, Optional
|
|
12
13
|
|
|
@@ -76,6 +77,18 @@ class MainAgent(BaseAgent):
|
|
|
76
77
|
- **发送文件给用户**: 用 `file_send` 工具(参数: file_path=文件路径, description=说明),当你生成或处理了文件需要返回给用户时使用
|
|
77
78
|
- **播放音频**: 用 `playaudio` 工具(参数: url=音乐链接或file_path=本地文件路径),在聊天中内嵌播放音频(支持QQ音乐、YouTube音乐、本地MP3/WAV等),播放时自动关闭语音合成
|
|
78
79
|
- **播放视频**: 用 `playvideo` 工具(参数: url=视频链接或file_path=本地文件路径),在聊天中内嵌播放视频(支持抖音、YouTube、B站、本地MP4等),播放时自动关闭语音合成
|
|
80
|
+
- **网页控制**: 用 `web_control` 工具在聊天中打开一个可控制的浏览器面板,可浏览网页、点击元素、填写表单、滚动页面、执行JS、管理Cookie等。
|
|
81
|
+
- 打开: `{"action": "open", "url": "https://example.com"}` — 首次使用会自动创建会话,后续可传 session_id 复用
|
|
82
|
+
- 导航: `{"action": "navigate", "url": "https://...", "session_id": "xxx"}`
|
|
83
|
+
- 获取内容: `{"action": "get_content", "what": "text|html|url|title|links|images|forms|inputs", "session_id": "xxx"}`
|
|
84
|
+
- 点击元素: `{"action": "click", "selector": "CSS选择器", "session_id": "xxx"}`
|
|
85
|
+
- 填写输入: `{"action": "fill", "selector": "CSS选择器", "value": "内容", "session_id": "xxx"}`
|
|
86
|
+
- 滚动页面: `{"action": "scroll", "direction": "up|down|top|bottom", "distance": 300, "session_id": "xxx"}`
|
|
87
|
+
- 执行JS: `{"action": "evaluate", "script": "JavaScript代码", "session_id": "xxx"}`
|
|
88
|
+
- 管理Cookie: `{"action": "set_cookies", "cookies": [{"name":"k","value":"v","domain":"example.com"}], "session_id": "xxx"}` 或 `{"action": "get_cookies", "session_id": "xxx"}`
|
|
89
|
+
- 等待: `{"action": "wait", "time": 1000}` 毫秒 或 `{"action": "wait", "selector": ".result", "timeout": 10}` 秒
|
|
90
|
+
- 关闭: `{"action": "close", "session_id": "xxx"}`
|
|
91
|
+
- 注意: 网页通过服务端代理加载,用户可在面板中手动操作。复杂交互建议先用 get_content 查看页面结构再用 click/fill。
|
|
79
92
|
- **主动召回记忆**: 用 `recall_memory` 工具(参数: keyword=关键字, time_point=可选时间点如"2025-01", limit=数量默认5),根据关键字和时间搜索历史记忆
|
|
80
93
|
4. 准备好内容后,最后,再检查输出格式,确保满足以下要求:
|
|
81
94
|
<output>
|
|
@@ -1216,6 +1229,7 @@ class MainAgent(BaseAgent):
|
|
|
1216
1229
|
tool_result = await self._execute_v2_tool(
|
|
1217
1230
|
tool_name, parms, timeout, context, task_id,
|
|
1218
1231
|
stream_callback=stream_callback,
|
|
1232
|
+
sent_files=_sent_files,
|
|
1219
1233
|
)
|
|
1220
1234
|
|
|
1221
1235
|
# 发送工具结果事件
|
|
@@ -1552,6 +1566,7 @@ class MainAgent(BaseAgent):
|
|
|
1552
1566
|
context: AgentContext,
|
|
1553
1567
|
task_id: str,
|
|
1554
1568
|
stream_callback: Optional[Callable] = None,
|
|
1569
|
+
sent_files: Optional[List[Dict[str, Any]]] = None,
|
|
1555
1570
|
) -> Dict[str, Any]:
|
|
1556
1571
|
"""V2 工具执行"""
|
|
1557
1572
|
result = {"success": False, "output": "", "error": ""}
|
|
@@ -1634,18 +1649,14 @@ class MainAgent(BaseAgent):
|
|
|
1634
1649
|
# [v1.16.18] 使用当前作用域的 stream_callback(而非 context._stream_callback)
|
|
1635
1650
|
_fresult = await _fskill.execute(_fpath, _fdesc, stream_callback=stream_callback)
|
|
1636
1651
|
result = {"success": True, "output": json.dumps(_fresult, ensure_ascii=False, indent=2), "data": _fresult}
|
|
1637
|
-
# [v1.18.5] 追踪发送的文件,用于持久化到会话记忆
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
"size": _fresult.get("size", 0),
|
|
1646
|
-
})
|
|
1647
|
-
except NameError:
|
|
1648
|
-
pass # _sent_files 不在当前作用域,跳过文件追踪
|
|
1652
|
+
# [v1.18.5→21.1] 追踪发送的文件,用于持久化到会话记忆
|
|
1653
|
+
if sent_files is not None and _fresult.get("success") and _fresult.get("file_id"):
|
|
1654
|
+
sent_files.append({
|
|
1655
|
+
"id": _fresult["file_id"],
|
|
1656
|
+
"name": _fresult.get("name", ""),
|
|
1657
|
+
"type": _fresult.get("type", ""),
|
|
1658
|
+
"size": _fresult.get("size", 0),
|
|
1659
|
+
})
|
|
1649
1660
|
except Exception as _fse:
|
|
1650
1661
|
result = {"success": False, "error": f"文件发送失败: {_fse}"}
|
|
1651
1662
|
logger.warning(f"[{task_id}] file_send 工具异常: {_fse}")
|
|
@@ -1727,17 +1738,14 @@ class MainAgent(BaseAgent):
|
|
|
1727
1738
|
# 标记为媒体文件,前端渲染内嵌播放器
|
|
1728
1739
|
_fresult["_media_type"] = _media_type
|
|
1729
1740
|
result = {"success": True, "output": f"已发送{_media_type}文件: {_fpath.name}", "data": _fresult}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
})
|
|
1739
|
-
except NameError:
|
|
1740
|
-
pass
|
|
1741
|
+
if sent_files is not None and _fresult.get("file_id"):
|
|
1742
|
+
sent_files.append({
|
|
1743
|
+
"id": _fresult["file_id"],
|
|
1744
|
+
"name": _fresult.get("name", ""),
|
|
1745
|
+
"type": _fresult.get("type", ""),
|
|
1746
|
+
"size": _fresult.get("size", 0),
|
|
1747
|
+
"_media_type": _media_type,
|
|
1748
|
+
})
|
|
1741
1749
|
else:
|
|
1742
1750
|
result = {"success": False, "error": _fresult.get("error", "文件发送失败")}
|
|
1743
1751
|
else:
|
|
@@ -1747,12 +1755,138 @@ class MainAgent(BaseAgent):
|
|
|
1747
1755
|
result = {"success": False, "error": f"播放工具异常: {_me}"}
|
|
1748
1756
|
logger.warning(f"[{task_id}] {tool_name} 工具异常: {_me}")
|
|
1749
1757
|
|
|
1758
|
+
elif tool_name == "web_control":
|
|
1759
|
+
# [v1.21.0] 网页控制器 — 在聊天中打开可控制的浏览器面板
|
|
1760
|
+
try:
|
|
1761
|
+
from core.web_control import get_web_control_manager
|
|
1762
|
+
_wc_mgr = get_web_control_manager()
|
|
1763
|
+
_wc_action = params.get("action", "open")
|
|
1764
|
+
_wc_session_id = params.get("session_id", "").strip()
|
|
1765
|
+
|
|
1766
|
+
# 自动获取或创建会话
|
|
1767
|
+
_wc_session = None
|
|
1768
|
+
if _wc_session_id:
|
|
1769
|
+
_wc_session = _wc_mgr.get_session(_wc_session_id)
|
|
1770
|
+
if not _wc_session:
|
|
1771
|
+
_wc_session = _wc_mgr.create_session()
|
|
1772
|
+
_wc_session_id = _wc_session.session_id
|
|
1773
|
+
|
|
1774
|
+
if _wc_action == "open":
|
|
1775
|
+
# 打开面板 — 发送 SSE 事件让前端弹出控制面板
|
|
1776
|
+
_wc_url = params.get("url", "").strip()
|
|
1777
|
+
if _wc_url:
|
|
1778
|
+
_wc_session.current_url = _wc_url
|
|
1779
|
+
if stream_callback:
|
|
1780
|
+
stream_callback({
|
|
1781
|
+
"type": "v2_web_control",
|
|
1782
|
+
"data": {
|
|
1783
|
+
"action": "open",
|
|
1784
|
+
"session_id": _wc_session_id,
|
|
1785
|
+
"url": _wc_url,
|
|
1786
|
+
"panel_url": f"/api/web_control/panel?sid={_wc_session_id}",
|
|
1787
|
+
}
|
|
1788
|
+
})
|
|
1789
|
+
result = {
|
|
1790
|
+
"success": True,
|
|
1791
|
+
"output": f"已打开网页控制面板 (session: {_wc_session_id})" + (f",URL: {_wc_url}" if _wc_url else ""),
|
|
1792
|
+
"session_id": _wc_session_id,
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
elif _wc_action == "close":
|
|
1796
|
+
_wc_mgr.close_session(_wc_session_id)
|
|
1797
|
+
if stream_callback:
|
|
1798
|
+
stream_callback({
|
|
1799
|
+
"type": "v2_web_control",
|
|
1800
|
+
"data": {"action": "close", "session_id": _wc_session_id}
|
|
1801
|
+
})
|
|
1802
|
+
result = {"success": True, "output": f"已关闭网页控制面板 (session: {_wc_session_id})"}
|
|
1803
|
+
|
|
1804
|
+
elif _wc_action == "navigate":
|
|
1805
|
+
_wc_url = params.get("url", "").strip()
|
|
1806
|
+
if not _wc_url:
|
|
1807
|
+
result = {"success": False, "error": "请提供 url 参数"}
|
|
1808
|
+
else:
|
|
1809
|
+
_wc_session.current_url = _wc_url
|
|
1810
|
+
if stream_callback:
|
|
1811
|
+
stream_callback({
|
|
1812
|
+
"type": "v2_web_control",
|
|
1813
|
+
"data": {"action": "navigate", "session_id": _wc_session_id, "url": _wc_url}
|
|
1814
|
+
})
|
|
1815
|
+
result = {"success": True, "output": f"正在导航到: {_wc_url}"}
|
|
1816
|
+
|
|
1817
|
+
elif _wc_action in ("set_cookies", "get_cookies"):
|
|
1818
|
+
# Cookie 操作 — 服务端直接处理
|
|
1819
|
+
if _wc_action == "set_cookies":
|
|
1820
|
+
_cookies = params.get("cookies", [])
|
|
1821
|
+
if isinstance(_cookies, str):
|
|
1822
|
+
import json as _jc
|
|
1823
|
+
try:
|
|
1824
|
+
_cookies = _jc.loads(_cookies)
|
|
1825
|
+
except:
|
|
1826
|
+
_cookies = []
|
|
1827
|
+
for _c in _cookies:
|
|
1828
|
+
if isinstance(_c, dict):
|
|
1829
|
+
_c_domain = _c.get("domain", "").lstrip(".")
|
|
1830
|
+
_c_name = _c.get("name", "")
|
|
1831
|
+
_c_value = _c.get("value", "")
|
|
1832
|
+
if _c_domain and _c_name:
|
|
1833
|
+
_wc_session.cookies[f"{_c_domain}::{_c_name}"] = _c_value
|
|
1834
|
+
result = {"success": True, "output": f"已设置 {len(_cookies)} 个 cookie", "total_cookies": len(_wc_session.cookies)}
|
|
1835
|
+
else:
|
|
1836
|
+
_cookie_list = []
|
|
1837
|
+
for _ck, _cv in _wc_session.cookies.items():
|
|
1838
|
+
_parts = _ck.split("::", 1)
|
|
1839
|
+
if len(_parts) == 2:
|
|
1840
|
+
_cookie_list.append({"domain": _parts[0], "name": _parts[1], "value": _cv})
|
|
1841
|
+
result = {"success": True, "output": json.dumps(_cookie_list, ensure_ascii=False), "cookies": _cookie_list}
|
|
1842
|
+
|
|
1843
|
+
else:
|
|
1844
|
+
# 其他操作(click, fill, scroll, evaluate, get_content, wait, screenshot)
|
|
1845
|
+
# 通过命令队列下发到客户端执行,阻塞等待结果
|
|
1846
|
+
_wc_params = {k: v for k, v in params.items() if k not in ("action", "session_id")}
|
|
1847
|
+
_wc_timeout = int(params.get("timeout", timeout))
|
|
1848
|
+
_wc_result = await _wc_mgr.queue_command(
|
|
1849
|
+
session_id=_wc_session_id,
|
|
1850
|
+
action=_wc_action,
|
|
1851
|
+
params=_wc_params,
|
|
1852
|
+
timeout=min(_wc_timeout, 60), # 最大 60 秒
|
|
1853
|
+
)
|
|
1854
|
+
result = _wc_result
|
|
1855
|
+
|
|
1856
|
+
except Exception as _wce:
|
|
1857
|
+
result = {"success": False, "error": f"网页控制器异常: {_wce}"}
|
|
1858
|
+
logger.warning(f"[{task_id}] web_control 工具异常: {_wce}")
|
|
1859
|
+
|
|
1750
1860
|
elif self.skills:
|
|
1751
1861
|
exec_result = await self.skills.execute(tool_name, **params)
|
|
1752
1862
|
if exec_result is None:
|
|
1753
1863
|
result["error"] = f"技能 {tool_name} 返回了空结果"
|
|
1754
1864
|
else:
|
|
1755
1865
|
result = exec_result.to_dict()
|
|
1866
|
+
# [v1.21.1] Skill 生成文件后自动通过 file_send 发送给前端
|
|
1867
|
+
if exec_result.success and exec_result.files:
|
|
1868
|
+
try:
|
|
1869
|
+
from skills.file_send import FileSendSkill
|
|
1870
|
+
_auto_fsend = FileSendSkill()
|
|
1871
|
+
for _auto_fpath in exec_result.files:
|
|
1872
|
+
if _auto_fpath and os.path.isfile(_auto_fpath):
|
|
1873
|
+
_auto_fres = await _auto_fsend.execute(
|
|
1874
|
+
_auto_fpath, stream_callback=stream_callback)
|
|
1875
|
+
if _auto_fres.get("success") and _auto_fres.get("file_id"):
|
|
1876
|
+
# 记录发送的 file_id
|
|
1877
|
+
if not result.get("_sent_file_ids"):
|
|
1878
|
+
result["_sent_file_ids"] = []
|
|
1879
|
+
result["_sent_file_ids"].append(_auto_fres["file_id"])
|
|
1880
|
+
# 追踪到 sent_files(持久化到会话记忆)
|
|
1881
|
+
if sent_files is not None:
|
|
1882
|
+
sent_files.append({
|
|
1883
|
+
"id": _auto_fres["file_id"],
|
|
1884
|
+
"name": _auto_fres.get("name", ""),
|
|
1885
|
+
"type": _auto_fres.get("type", ""),
|
|
1886
|
+
"size": _auto_fres.get("size", 0),
|
|
1887
|
+
})
|
|
1888
|
+
except Exception as _afe:
|
|
1889
|
+
logger.warning(f"[{task_id}] 自动 file_send 失败: {_afe}")
|
|
1756
1890
|
else:
|
|
1757
1891
|
result["error"] = f"未知工具: {tool_name}"
|
|
1758
1892
|
|
package/core/vnc_manager.py
CHANGED
|
@@ -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.
|
|
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}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
logger.
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
#
|
|
697
|
-
#
|
|
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
|
-
"-
|
|
718
|
-
"-
|
|
719
|
-
"-
|
|
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",
|
|
723
|
-
"-noxdamage",
|
|
724
|
-
"-nowf",
|
|
725
|
-
"-nowcr",
|
|
764
|
+
"-xkb", # 使用 XKB 扩展
|
|
765
|
+
"-noxdamage", # 禁用 XDAMAGE(兼容性更好)
|
|
766
|
+
"-nowf", # 禁用 wireframe
|
|
767
|
+
"-nowcr", # 禁用 cursor shape updates
|
|
726
768
|
"-nocursorshape",
|
|
727
|
-
"-deferupdate", "5",
|
|
728
|
-
"-scale", "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.
|
|
735
|
-
#
|
|
736
|
-
|
|
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
|
|
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",
|