myagent-ai 1.47.18 → 1.47.20
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 +18 -0
- package/aiskills/browser_stealth.py +233 -29
- package/aiskills/chromedev_mcp.py +20 -0
- package/core/vnc_manager.py +119 -6
- package/package.json +1 -1
- package/worklog.md +48 -0
package/agents/main_agent.py
CHANGED
|
@@ -924,6 +924,24 @@ class MainAgent(BaseAgent):
|
|
|
924
924
|
|
|
925
925
|
messages.append(Message(role="system", content=_system_content))
|
|
926
926
|
|
|
927
|
+
# [v1.47.20] VNC 模式下注入浏览器工具使用提示
|
|
928
|
+
try:
|
|
929
|
+
from core.vnc_manager import get_vnc_manager
|
|
930
|
+
vnc_mgr = get_vnc_manager()
|
|
931
|
+
if vnc_mgr.is_running:
|
|
932
|
+
vnc_hint = (
|
|
933
|
+
"\n\n## VNC 远程桌面模式提示\n"
|
|
934
|
+
"当前运行在 VNC 远程桌面环境,浏览器为 Firefox(不支持 Chromium/CDP)。\n"
|
|
935
|
+
"- **网页浏览**: 优先使用 stealth_browser_start → stealth_browser_navigate → stealth_browser_content\n"
|
|
936
|
+
"- **获取页面内容**: stealth_browser_content(返回截图+标签页信息),不要使用 browser_open\n"
|
|
937
|
+
"- **交互操作**: stealth_browser_click / stealth_browser_fill / stealth_browser_key\n"
|
|
938
|
+
"- **不要使用**: browser_open(需要 Chromium)、web_control(需要前端面板)\n"
|
|
939
|
+
"- **不要关闭 Firefox**: stealth_browser_close 在 VNC 模式下只释放会话,不关闭浏览器"
|
|
940
|
+
)
|
|
941
|
+
messages[0] = Message(role="system", content=messages[0].content + vnc_hint)
|
|
942
|
+
except (ImportError, Exception):
|
|
943
|
+
pass
|
|
944
|
+
|
|
927
945
|
# 注入对话历史
|
|
928
946
|
if conversation_history:
|
|
929
947
|
_history_budget = int(self.context_builder.context_window * 0.25) if self.context_builder else 50000
|
|
@@ -956,10 +956,15 @@ class StealthBrowser:
|
|
|
956
956
|
"""关闭浏览器"""
|
|
957
957
|
self._started = False
|
|
958
958
|
|
|
959
|
-
# [v1.47.
|
|
959
|
+
# [v1.47.20] Firefox+VNC 模式:VNC 桌面的浏览器不能杀,只清理内部状态
|
|
960
960
|
if self._firefox_mode:
|
|
961
961
|
try:
|
|
962
|
-
if self.
|
|
962
|
+
if self._vnc_used:
|
|
963
|
+
# VNC 模式:Firefox 由 vnc_manager 管理,不能杀进程
|
|
964
|
+
# 只清理本实例的内部状态
|
|
965
|
+
logger.info("Firefox+VNC 模式: 不关闭 VNC 浏览器(由 vnc_manager 管理),仅释放内部状态")
|
|
966
|
+
elif self._firefox_process and self._firefox_process.poll() is None:
|
|
967
|
+
# 非 VNC 模式(独立启动的 Firefox):可以关闭
|
|
963
968
|
self._firefox_process.terminate()
|
|
964
969
|
try:
|
|
965
970
|
self._firefox_process.wait(timeout=5)
|
|
@@ -970,19 +975,15 @@ class StealthBrowser:
|
|
|
970
975
|
pass
|
|
971
976
|
logger.info("Firefox 已关闭")
|
|
972
977
|
else:
|
|
973
|
-
#
|
|
974
|
-
|
|
975
|
-
subprocess.run(["pkill", "-f", "firefox"], capture_output=True, timeout=5)
|
|
976
|
-
logger.info("Firefox 进程已终止 (pkill)")
|
|
977
|
-
except Exception:
|
|
978
|
-
pass
|
|
978
|
+
# 可能是复用的非 VNC Firefox 进程
|
|
979
|
+
logger.info("Firefox 进程非本实例启动,跳过关闭")
|
|
979
980
|
except Exception as e:
|
|
980
981
|
logger.error(f"关闭 Firefox 异常: {e}")
|
|
981
982
|
finally:
|
|
982
983
|
self._firefox_process = None
|
|
983
984
|
self._firefox_mode = False
|
|
984
985
|
self._vnc_used = False
|
|
985
|
-
return SkillResult(success=True, message="
|
|
986
|
+
return SkillResult(success=True, message="浏览器会话已释放(VNC 浏览器保持运行)")
|
|
986
987
|
|
|
987
988
|
try:
|
|
988
989
|
if self._browser:
|
|
@@ -1475,11 +1476,13 @@ class StealthBrowser:
|
|
|
1475
1476
|
if not self._ensure_page():
|
|
1476
1477
|
return SkillResult(success=False, error="浏览器未启动")
|
|
1477
1478
|
|
|
1478
|
-
# [v1.47.
|
|
1479
|
+
# [v1.47.20] Firefox+VNC 模式:无法通过 CDP 执行 JS
|
|
1479
1480
|
if self._firefox_mode:
|
|
1480
1481
|
return SkillResult(
|
|
1481
1482
|
success=False,
|
|
1482
|
-
error="Firefox+VNC 模式下不支持 JS
|
|
1483
|
+
error="Firefox+VNC 模式下不支持 JS 执行。请使用 stealth_browser_navigate "
|
|
1484
|
+
"导航页面,用 stealth_browser_content(截图+标签页信息)获取内容,"
|
|
1485
|
+
"用 xdotool 相关操作(click/fill/key)进行交互。",
|
|
1483
1486
|
)
|
|
1484
1487
|
|
|
1485
1488
|
try:
|
|
@@ -1538,12 +1541,9 @@ class StealthBrowser:
|
|
|
1538
1541
|
if not self._ensure_page():
|
|
1539
1542
|
return SkillResult(success=False, error="浏览器未启动")
|
|
1540
1543
|
|
|
1541
|
-
# [v1.47.
|
|
1544
|
+
# [v1.47.20] Firefox+VNC 模式:截图 + sessionstore 读取
|
|
1542
1545
|
if self._firefox_mode:
|
|
1543
|
-
return
|
|
1544
|
-
success=False,
|
|
1545
|
-
error="Firefox+VNC 模式下不支持获取页面内容。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
|
|
1546
|
-
)
|
|
1546
|
+
return self._firefox_get_content()
|
|
1547
1547
|
|
|
1548
1548
|
try:
|
|
1549
1549
|
# Bug Fix: DrissionPage 没有 page.text 属性
|
|
@@ -1583,12 +1583,9 @@ class StealthBrowser:
|
|
|
1583
1583
|
if not self._ensure_page():
|
|
1584
1584
|
return SkillResult(success=False, error="浏览器未启动")
|
|
1585
1585
|
|
|
1586
|
-
# [v1.47.
|
|
1586
|
+
# [v1.47.20] Firefox+VNC 模式:截图 + sessionstore 替代
|
|
1587
1587
|
if self._firefox_mode:
|
|
1588
|
-
return
|
|
1589
|
-
success=False,
|
|
1590
|
-
error="Firefox+VNC 模式下不支持获取页面 HTML。请在 VNC 中手动查看,或切换到桌面环境使用 Chromium。",
|
|
1591
|
-
)
|
|
1588
|
+
return self._firefox_get_content()
|
|
1592
1589
|
|
|
1593
1590
|
try:
|
|
1594
1591
|
html = self._page.html or ""
|
|
@@ -1614,11 +1611,17 @@ class StealthBrowser:
|
|
|
1614
1611
|
if not self._ensure_page():
|
|
1615
1612
|
return SkillResult(success=False, error="浏览器未启动")
|
|
1616
1613
|
|
|
1617
|
-
# [v1.47.
|
|
1614
|
+
# [v1.47.20] Firefox+VNC 模式:sleep + 截图替代
|
|
1618
1615
|
if self._firefox_mode:
|
|
1616
|
+
await asyncio.sleep(min(timeout, 5))
|
|
1617
|
+
# 等待后截图,确认页面状态
|
|
1618
|
+
session_info = self._firefox_read_sessionstore()
|
|
1619
|
+
url = session_info.get("url", "")
|
|
1620
|
+
title = session_info.get("title", "")
|
|
1619
1621
|
return SkillResult(
|
|
1620
|
-
success=
|
|
1621
|
-
|
|
1622
|
+
success=True,
|
|
1623
|
+
message=f"Firefox+VNC: 已等待 {min(timeout, 5)}秒,当前页面: {title or url}",
|
|
1624
|
+
data={"url": url, "title": title},
|
|
1622
1625
|
)
|
|
1623
1626
|
|
|
1624
1627
|
try:
|
|
@@ -1865,25 +1868,53 @@ class StealthBrowser:
|
|
|
1865
1868
|
"""Firefox+VNC 模式下导航到指定 URL。
|
|
1866
1869
|
|
|
1867
1870
|
Firefox 支持远程打开 URL:firefox <url> 会在已运行的实例中打开新标签页。
|
|
1871
|
+
[v1.47.19] 改用 Popen 非阻塞方式:firefox <url> 在已运行实例中打开新标签页
|
|
1872
|
+
后会阻塞等待窗口关闭,subprocess.run + timeout 会导致超时。
|
|
1873
|
+
改用 Popen 后不等待命令完成,只确认启动即可。
|
|
1868
1874
|
"""
|
|
1869
1875
|
display = os.environ.get("DISPLAY", ":99")
|
|
1870
1876
|
env = {**os.environ, "DISPLAY": display}
|
|
1871
1877
|
if not env.get("G_SLICE"):
|
|
1872
1878
|
env["G_SLICE"] = "always-malloc"
|
|
1879
|
+
if not env.get("GSETTINGS_BACKEND"):
|
|
1880
|
+
env["GSETTINGS_BACKEND"] = "memory"
|
|
1881
|
+
if os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
|
|
1882
|
+
env["DBUS_SESSION_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"]
|
|
1873
1883
|
|
|
1874
1884
|
try:
|
|
1875
1885
|
firefox_path = shutil.which("firefox") or shutil.which("myagent-browser")
|
|
1876
1886
|
if not firefox_path:
|
|
1877
1887
|
return SkillResult(success=False, error="Firefox 未找到")
|
|
1878
1888
|
|
|
1879
|
-
|
|
1889
|
+
# [v1.47.19] 使用 Popen 非阻塞方式启动
|
|
1890
|
+
# firefox <url> 在已有实例运行时会将 URL 发送给已有实例,
|
|
1891
|
+
# 然后阻塞等待(因为 Firefox 的 -no-remote 行为)。
|
|
1892
|
+
# 用 Popen 启动后不等待完成,只等待进程启动成功。
|
|
1893
|
+
process = subprocess.Popen(
|
|
1880
1894
|
[firefox_path, "--profile", self._firefox_profile_dir, url],
|
|
1881
|
-
|
|
1882
|
-
|
|
1895
|
+
stdout=subprocess.DEVNULL,
|
|
1896
|
+
stderr=subprocess.DEVNULL,
|
|
1897
|
+
env=env,
|
|
1898
|
+
start_new_session=True,
|
|
1883
1899
|
)
|
|
1884
|
-
|
|
1900
|
+
# 等待短时间确认进程没有立即崩溃
|
|
1901
|
+
time.sleep(0.5)
|
|
1902
|
+
if process.poll() is not None:
|
|
1903
|
+
# 进程已退出,可能是错误
|
|
1904
|
+
exit_code = process.returncode
|
|
1905
|
+
# Firefox 在已有实例时,转发URL后以 exit code 0 退出(正常行为)
|
|
1906
|
+
if exit_code == 0:
|
|
1907
|
+
logger.info(f"Firefox 已转发 URL 到已有实例: {url}")
|
|
1908
|
+
else:
|
|
1909
|
+
return SkillResult(
|
|
1910
|
+
success=False,
|
|
1911
|
+
error=f"Firefox 打开 URL 失败 (exit code: {exit_code}): {url}",
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1885
1914
|
if wait > 0:
|
|
1886
1915
|
time.sleep(wait)
|
|
1916
|
+
|
|
1917
|
+
logger.info(f"Firefox 已打开 URL: {url}")
|
|
1887
1918
|
return SkillResult(
|
|
1888
1919
|
success=True,
|
|
1889
1920
|
message=f"Firefox 已打开: {url}",
|
|
@@ -2146,6 +2177,162 @@ class StealthBrowser:
|
|
|
2146
2177
|
except Exception as e:
|
|
2147
2178
|
return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
|
|
2148
2179
|
|
|
2180
|
+
def _firefox_read_sessionstore(self) -> dict:
|
|
2181
|
+
"""[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
|
|
2182
|
+
|
|
2183
|
+
Firefox 运行时会将当前会话信息写入 sessionstore-backups/recovery.jsonlz4。
|
|
2184
|
+
该文件使用 mozLz4 格式(8字节自定义头 + LZ4 压缩数据)。
|
|
2185
|
+
|
|
2186
|
+
Returns:
|
|
2187
|
+
dict: {"url": str, "title": str, "tabs": [{"url": str, "title": str}]}
|
|
2188
|
+
"""
|
|
2189
|
+
result_info = {"url": "", "title": "", "tabs": []}
|
|
2190
|
+
|
|
2191
|
+
# 搜索可能的 sessionstore 路径
|
|
2192
|
+
search_dirs = []
|
|
2193
|
+
if self._firefox_profile_dir:
|
|
2194
|
+
search_dirs.append(self._firefox_profile_dir)
|
|
2195
|
+
# 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
|
|
2196
|
+
for extra in [
|
|
2197
|
+
os.path.expanduser("~/.mozilla/firefox/default"),
|
|
2198
|
+
os.path.expanduser("~/.mozilla/firefox"),
|
|
2199
|
+
]:
|
|
2200
|
+
if os.path.isdir(extra) and extra not in search_dirs:
|
|
2201
|
+
search_dirs.append(extra)
|
|
2202
|
+
|
|
2203
|
+
recovery_files = []
|
|
2204
|
+
for base_dir in search_dirs:
|
|
2205
|
+
ss_dir = os.path.join(base_dir, "sessionstore-backups")
|
|
2206
|
+
if os.path.isdir(ss_dir):
|
|
2207
|
+
for fname in ("recovery.jsonlz4", "recovery.baklz4",
|
|
2208
|
+
"previous.jsonlz4"):
|
|
2209
|
+
fpath = os.path.join(ss_dir, fname)
|
|
2210
|
+
if os.path.isfile(fpath):
|
|
2211
|
+
recovery_files.append(fpath)
|
|
2212
|
+
# 也检查 base_dir 本身(有些 Firefox 版本)
|
|
2213
|
+
for fname in ("sessionstore.jsonlz4", "sessionstore-backups/recovery.jsonlz4"):
|
|
2214
|
+
fpath = os.path.join(base_dir, fname)
|
|
2215
|
+
if os.path.isfile(fpath):
|
|
2216
|
+
recovery_files.append(fpath)
|
|
2217
|
+
|
|
2218
|
+
if not recovery_files:
|
|
2219
|
+
logger.debug("[_firefox_read_sessionstore] 未找到 sessionstore 文件")
|
|
2220
|
+
return result_info
|
|
2221
|
+
|
|
2222
|
+
# 读取 mozLz4 格式
|
|
2223
|
+
for fpath in recovery_files:
|
|
2224
|
+
try:
|
|
2225
|
+
import struct
|
|
2226
|
+
with open(fpath, "rb") as f:
|
|
2227
|
+
data = f.read()
|
|
2228
|
+
if len(data) < 8:
|
|
2229
|
+
continue
|
|
2230
|
+
magic = struct.unpack("<I", data[:4])[0]
|
|
2231
|
+
if magic != 0x00080000:
|
|
2232
|
+
continue
|
|
2233
|
+
orig_size = struct.unpack("<I", data[4:8])[0]
|
|
2234
|
+
try:
|
|
2235
|
+
import lz4.block
|
|
2236
|
+
decompressed = lz4.block.decompress(
|
|
2237
|
+
data[8:], uncompressed_size=orig_size
|
|
2238
|
+
)
|
|
2239
|
+
except ImportError:
|
|
2240
|
+
logger.debug("[_firefox_read_sessionstore] lz4 未安装,跳过 mozLz4 解析")
|
|
2241
|
+
continue
|
|
2242
|
+
except Exception as lz4_err:
|
|
2243
|
+
logger.debug(f"[_firefox_read_sessionstore] lz4 解压失败: {lz4_err}")
|
|
2244
|
+
continue
|
|
2245
|
+
|
|
2246
|
+
session = json.loads(decompressed)
|
|
2247
|
+
tabs_info = []
|
|
2248
|
+
for win in session.get("windows", []):
|
|
2249
|
+
for tab in win.get("tabs", []):
|
|
2250
|
+
idx = tab.get("index", 1) - 1
|
|
2251
|
+
entries = tab.get("entries", [])
|
|
2252
|
+
if entries and 0 <= idx < len(entries):
|
|
2253
|
+
entry = entries[idx]
|
|
2254
|
+
tab_info = {
|
|
2255
|
+
"url": entry.get("url", ""),
|
|
2256
|
+
"title": entry.get("title", ""),
|
|
2257
|
+
}
|
|
2258
|
+
tabs_info.append(tab_info)
|
|
2259
|
+
|
|
2260
|
+
result_info["tabs"] = tabs_info
|
|
2261
|
+
if tabs_info:
|
|
2262
|
+
# 取第一个标签页作为当前页面(通常是最活跃的)
|
|
2263
|
+
result_info["url"] = tabs_info[0]["url"]
|
|
2264
|
+
result_info["title"] = tabs_info[0]["title"]
|
|
2265
|
+
|
|
2266
|
+
logger.info(
|
|
2267
|
+
f"[_firefox_read_sessionstore] 读取成功: {len(tabs_info)} 个标签页, "
|
|
2268
|
+
f"当前: {result_info['title'][:50]}"
|
|
2269
|
+
)
|
|
2270
|
+
return result_info
|
|
2271
|
+
|
|
2272
|
+
except Exception as e:
|
|
2273
|
+
logger.debug(f"[_firefox_read_sessionstore] 读取 {fpath} 失败: {e}")
|
|
2274
|
+
continue
|
|
2275
|
+
|
|
2276
|
+
return result_info
|
|
2277
|
+
|
|
2278
|
+
def _firefox_get_content(self) -> SkillResult:
|
|
2279
|
+
"""[v1.47.20] Firefox+VNC 模式下获取页面内容。
|
|
2280
|
+
|
|
2281
|
+
由于无法通过 CDP 获取 Firefox 页面文本,采用以下策略:
|
|
2282
|
+
1. 截取当前屏幕截图(供 VLM 视觉理解)
|
|
2283
|
+
2. 读取 Firefox sessionstore 获取 URL/标题
|
|
2284
|
+
3. 返回截图路径和基本元信息
|
|
2285
|
+
|
|
2286
|
+
Agent 可以使用截图进行视觉理解,或使用 web_search/web_read 获取页面文本。
|
|
2287
|
+
"""
|
|
2288
|
+
# 1. 截图
|
|
2289
|
+
screenshot_result = self._firefox_screenshot()
|
|
2290
|
+
screenshot_path = ""
|
|
2291
|
+
if screenshot_result.success and screenshot_result.data:
|
|
2292
|
+
screenshot_path = screenshot_result.data.get("path", "")
|
|
2293
|
+
|
|
2294
|
+
# 2. 读取 sessionstore
|
|
2295
|
+
session_info = self._firefox_read_sessionstore()
|
|
2296
|
+
|
|
2297
|
+
# 3. 组合返回信息
|
|
2298
|
+
url = session_info.get("url", "")
|
|
2299
|
+
title = session_info.get("title", "")
|
|
2300
|
+
tabs = session_info.get("tabs", [])
|
|
2301
|
+
tabs_summary = ""
|
|
2302
|
+
if tabs:
|
|
2303
|
+
tabs_lines = []
|
|
2304
|
+
for i, tab in enumerate(tabs[:10]): # 最多10个标签页
|
|
2305
|
+
tabs_lines.append(f" [{i+1}] {tab.get('title', '?')} - {tab.get('url', '?')}")
|
|
2306
|
+
tabs_summary = "\n".join(tabs_lines)
|
|
2307
|
+
|
|
2308
|
+
content_parts = []
|
|
2309
|
+
if title:
|
|
2310
|
+
content_parts.append(f"标题: {title}")
|
|
2311
|
+
if url:
|
|
2312
|
+
content_parts.append(f"URL: {url}")
|
|
2313
|
+
if tabs_summary:
|
|
2314
|
+
content_parts.append(f"标签页:\n{tabs_summary}")
|
|
2315
|
+
if screenshot_path:
|
|
2316
|
+
content_parts.append(f"截图: {screenshot_path}")
|
|
2317
|
+
|
|
2318
|
+
content_text = "\n".join(content_parts) if content_parts else "无法获取页面内容"
|
|
2319
|
+
|
|
2320
|
+
return SkillResult(
|
|
2321
|
+
success=True,
|
|
2322
|
+
message=f"Firefox+VNC 页面信息: {title or url or '未知'}",
|
|
2323
|
+
data={
|
|
2324
|
+
"url": url,
|
|
2325
|
+
"title": title,
|
|
2326
|
+
"tabs": tabs,
|
|
2327
|
+
"screenshot_path": screenshot_path,
|
|
2328
|
+
"mode": "firefox_vnc",
|
|
2329
|
+
"note": "Firefox+VNC 模式无法直接获取页面文本,已提供截图和标签页信息。"
|
|
2330
|
+
"请使用截图进行视觉理解,或用 web_search/web_read 获取网页文本。",
|
|
2331
|
+
},
|
|
2332
|
+
files=[screenshot_path] if screenshot_path else [],
|
|
2333
|
+
output=content_text,
|
|
2334
|
+
)
|
|
2335
|
+
|
|
2149
2336
|
def _firefox_get_cookies(self) -> SkillResult:
|
|
2150
2337
|
"""Firefox+VNC 模式下读取 cookies.sqlite。"""
|
|
2151
2338
|
try:
|
|
@@ -3035,8 +3222,25 @@ class StealthBrowserCloseSkill(Skill):
|
|
|
3035
3222
|
]
|
|
3036
3223
|
|
|
3037
3224
|
async def execute(self, profile: str = "", **kw) -> SkillResult:
|
|
3038
|
-
#
|
|
3225
|
+
# [v1.47.20] VNC 模式下不关闭 Firefox,只释放会话状态
|
|
3226
|
+
with _browser_lock:
|
|
3227
|
+
is_vnc = False
|
|
3228
|
+
if profile:
|
|
3229
|
+
for key, browser in _browsers.items():
|
|
3230
|
+
if key == profile or key == f"__system__:{profile}":
|
|
3231
|
+
if browser._vnc_used:
|
|
3232
|
+
is_vnc = True
|
|
3233
|
+
break
|
|
3234
|
+
else:
|
|
3235
|
+
is_vnc = any(b._vnc_used for b in _browsers.values())
|
|
3236
|
+
|
|
3039
3237
|
close_stealth_browser(profile_name=profile)
|
|
3238
|
+
|
|
3239
|
+
if is_vnc:
|
|
3240
|
+
return SkillResult(
|
|
3241
|
+
success=True,
|
|
3242
|
+
message="VNC 模式: 浏览器会话已释放,Firefox 保持运行(VNC 远程桌面需要)",
|
|
3243
|
+
)
|
|
3040
3244
|
return SkillResult(success=True, message="浏览器已关闭")
|
|
3041
3245
|
|
|
3042
3246
|
|
|
@@ -1372,6 +1372,26 @@ class BrowserOpenSkill(Skill):
|
|
|
1372
1372
|
if not url:
|
|
1373
1373
|
return SkillResult(success=False, error="缺少必需参数: url")
|
|
1374
1374
|
|
|
1375
|
+
# [v1.47.20] VNC 模式下:Chromium 不可用时回退到 stealth_browser_navigate
|
|
1376
|
+
try:
|
|
1377
|
+
from core.vnc_manager import get_vnc_manager
|
|
1378
|
+
vnc_mgr = get_vnc_manager()
|
|
1379
|
+
if vnc_mgr.is_running:
|
|
1380
|
+
# 检查是否有可用的 Chromium
|
|
1381
|
+
import shutil
|
|
1382
|
+
has_chrome = bool(shutil.which("chromium-browser") or shutil.which("chromium")
|
|
1383
|
+
or shutil.which("google-chrome"))
|
|
1384
|
+
if not has_chrome:
|
|
1385
|
+
# VNC 模式下无 Chromium,回退到 stealth_browser
|
|
1386
|
+
return SkillResult(
|
|
1387
|
+
success=False,
|
|
1388
|
+
error="VNC 远程桌面模式下没有 Chromium 浏览器,无法使用 browser_open。"
|
|
1389
|
+
"请改用 stealth_browser_start + stealth_browser_navigate 操作 Firefox。"
|
|
1390
|
+
"Firefox 在 VNC 模式下已可用。",
|
|
1391
|
+
)
|
|
1392
|
+
except ImportError:
|
|
1393
|
+
pass # vnc_manager 不可用,跳过检测
|
|
1394
|
+
|
|
1375
1395
|
# 检查依赖
|
|
1376
1396
|
dep_err = await asyncio.get_event_loop().run_in_executor(None, _ensure_node_deps)
|
|
1377
1397
|
if dep_err:
|
package/core/vnc_manager.py
CHANGED
|
@@ -1398,10 +1398,32 @@ class VNCManager:
|
|
|
1398
1398
|
# --session: 会话总线
|
|
1399
1399
|
# --address: 指定 unix socket
|
|
1400
1400
|
env = {**os.environ, "DISPLAY": os.environ.get("DISPLAY", self.display)}
|
|
1401
|
+
|
|
1402
|
+
# [v1.47.19] 创建自定义 dbus 会话配置,禁用 launch-helper
|
|
1403
|
+
# 避免 "error: expected absolute path: --shm-helper" 错误
|
|
1404
|
+
# dbus-daemon 的默认 session.conf 包含 <servicedir> 和
|
|
1405
|
+
# <servicehelper> 指令,后者会调用 dbus-daemon-launch-helper --shm-helper
|
|
1406
|
+
# 在 proot 下路径翻译破坏了参数传递。改为创建不含 servicehelper 的配置。
|
|
1407
|
+
custom_config_path = f"{dbus_socket_dir}.conf"
|
|
1408
|
+
try:
|
|
1409
|
+
custom_config = self._create_dbus_session_config()
|
|
1410
|
+
with open(custom_config_path, "w") as f:
|
|
1411
|
+
f.write(custom_config)
|
|
1412
|
+
logger.debug(f"已创建自定义 D-Bus 会话配置: {custom_config_path}")
|
|
1413
|
+
except Exception as conf_err:
|
|
1414
|
+
logger.debug(f"创建自定义 D-Bus 配置失败,使用默认配置: {conf_err}")
|
|
1415
|
+
custom_config_path = None
|
|
1416
|
+
|
|
1417
|
+
dbus_cmd = [dbus_daemon, "--nofork",
|
|
1418
|
+
f"--address={dbus_socket}",
|
|
1419
|
+
f"--pidfile={dbus_pid_file}"]
|
|
1420
|
+
if custom_config_path:
|
|
1421
|
+
dbus_cmd.extend(["--config-file", custom_config_path])
|
|
1422
|
+
else:
|
|
1423
|
+
dbus_cmd.append("--session")
|
|
1424
|
+
|
|
1401
1425
|
proc = subprocess.Popen(
|
|
1402
|
-
|
|
1403
|
-
f"--address={dbus_socket}",
|
|
1404
|
-
f"--pidfile={dbus_pid_file}"],
|
|
1426
|
+
dbus_cmd,
|
|
1405
1427
|
stdin=subprocess.DEVNULL,
|
|
1406
1428
|
stdout=subprocess.DEVNULL,
|
|
1407
1429
|
stderr=subprocess.DEVNULL,
|
|
@@ -1438,6 +1460,64 @@ class VNCManager:
|
|
|
1438
1460
|
except Exception as e:
|
|
1439
1461
|
logger.warning(f"dbus-daemon 直接启动异常: {e}")
|
|
1440
1462
|
|
|
1463
|
+
def _create_dbus_session_config(self) -> str:
|
|
1464
|
+
"""[v1.47.19] 创建不含 servicehelper 的 D-Bus 会话配置。
|
|
1465
|
+
|
|
1466
|
+
标准 session.conf 包含 <servicehelper> 指向 dbus-daemon-launch-helper,
|
|
1467
|
+
后者会被 dbus-daemon 用 --shm-helper 参数调用。
|
|
1468
|
+
在 proot 下路径翻译破坏了这个调用,导致反复出现:
|
|
1469
|
+
error: expected absolute path: "--shm-helper"
|
|
1470
|
+
|
|
1471
|
+
解决方法: 读取系统默认 session.conf,移除 <servicehelper> 行,
|
|
1472
|
+
保留其他配置(<servicedir>、<policy> 等)。
|
|
1473
|
+
这样 D-Bus 服务仍可被发现,但不会被 launch-helper 激活
|
|
1474
|
+
(需要程序自己启动对应的 D-Bus 服务)。
|
|
1475
|
+
"""
|
|
1476
|
+
import re as _re
|
|
1477
|
+
|
|
1478
|
+
# 查找系统默认 session.conf
|
|
1479
|
+
conf_paths = [
|
|
1480
|
+
"/usr/share/dbus-1/session.conf",
|
|
1481
|
+
"/etc/dbus-1/session.conf",
|
|
1482
|
+
]
|
|
1483
|
+
default_conf = ""
|
|
1484
|
+
for cp in conf_paths:
|
|
1485
|
+
if os.path.isfile(cp):
|
|
1486
|
+
try:
|
|
1487
|
+
with open(cp, "r") as f:
|
|
1488
|
+
default_conf = f.read()
|
|
1489
|
+
break
|
|
1490
|
+
except Exception:
|
|
1491
|
+
pass
|
|
1492
|
+
|
|
1493
|
+
if not default_conf:
|
|
1494
|
+
# 无法读取默认配置,创建最小配置
|
|
1495
|
+
return """<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
|
|
1496
|
+
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
|
1497
|
+
<busconfig>
|
|
1498
|
+
<type>session</type>
|
|
1499
|
+
<listen>unix:tmpdir=/tmp</listen>
|
|
1500
|
+
<servicedir>/usr/share/dbus-1/services</servicedir>
|
|
1501
|
+
<policy context="default">
|
|
1502
|
+
<allow send_destination="*" eavesdrop="true"/>
|
|
1503
|
+
<allow eavesdrop="true"/>
|
|
1504
|
+
<allow own="*"/>
|
|
1505
|
+
</policy>
|
|
1506
|
+
</busconfig>"""
|
|
1507
|
+
|
|
1508
|
+
# 从默认配置中移除 <servicehelper> 行
|
|
1509
|
+
# 匹配 <servicehelper>...</servicehelper> 或 <servicehelper .../>
|
|
1510
|
+
lines = default_conf.split("\n")
|
|
1511
|
+
filtered = []
|
|
1512
|
+
for line in lines:
|
|
1513
|
+
stripped = line.strip()
|
|
1514
|
+
# 跳过 servicehelper 行(这是导致 --shm-helper 错误的根源)
|
|
1515
|
+
if stripped.startswith("<servicehelper"):
|
|
1516
|
+
continue
|
|
1517
|
+
filtered.append(line)
|
|
1518
|
+
|
|
1519
|
+
return "\n".join(filtered)
|
|
1520
|
+
|
|
1441
1521
|
def _start_dbus_launch_standard(self) -> None:
|
|
1442
1522
|
"""[v1.43.3] 非 proot 环境下用 dbus-launch 标准方式启动 D-Bus。"""
|
|
1443
1523
|
dbus_launch = shutil.which("dbus-launch")
|
|
@@ -3316,7 +3396,18 @@ exec {backup_path} "${{args[@]}}"
|
|
|
3316
3396
|
' DBUS_SOCKET_DIR="/tmp/dbus-myagent-$(id -u)"',
|
|
3317
3397
|
' mkdir -p "$DBUS_SOCKET_DIR" 2>/dev/null',
|
|
3318
3398
|
' chmod 700 "$DBUS_SOCKET_DIR" 2>/dev/null',
|
|
3319
|
-
|
|
3399
|
+
# [v1.47.19] 使用自定义配置(移除 servicehelper,避免 --shm-helper 错误)
|
|
3400
|
+
' DBUS_CONF="$DBUS_SOCKET_DIR.conf"',
|
|
3401
|
+
' if [ ! -f "$DBUS_CONF" ]; then',
|
|
3402
|
+
' if [ -f /usr/share/dbus-1/session.conf ]; then',
|
|
3403
|
+
' grep -v "<servicehelper" /usr/share/dbus-1/session.conf > "$DBUS_CONF" 2>/dev/null',
|
|
3404
|
+
' fi',
|
|
3405
|
+
' fi',
|
|
3406
|
+
' if [ -f "$DBUS_CONF" ]; then',
|
|
3407
|
+
' dbus-daemon --nofork --config-file="$DBUS_CONF" --address="unix:path=$DBUS_SOCKET_DIR" &',
|
|
3408
|
+
' else',
|
|
3409
|
+
' dbus-daemon --session --nofork --address="unix:path=$DBUS_SOCKET_DIR" &',
|
|
3410
|
+
' fi',
|
|
3320
3411
|
' export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_SOCKET_DIR"',
|
|
3321
3412
|
' elif [ -x /usr/bin/dbus-launch ]; then',
|
|
3322
3413
|
' # 非 proot: 标准方式',
|
|
@@ -4836,7 +4927,18 @@ if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
|
|
|
4836
4927
|
if [ "$IS_PROOT" = "1" ] && [ -x /usr/bin/dbus-daemon ]; then
|
|
4837
4928
|
DBUS_SOCK="/tmp/dbus-myagent-$(id -u)"
|
|
4838
4929
|
mkdir -p "$DBUS_SOCK" 2>/dev/null && chmod 700 "$DBUS_SOCK" 2>/dev/null
|
|
4839
|
-
|
|
4930
|
+
# [v1.47.19] 使用自定义配置(移除 servicehelper,避免 --shm-helper 错误)
|
|
4931
|
+
DBUS_CONF="$DBUS_SOCK.conf"
|
|
4932
|
+
if [ ! -f "$DBUS_CONF" ]; then
|
|
4933
|
+
if [ -f /usr/share/dbus-1/session.conf ]; then
|
|
4934
|
+
grep -v "<servicehelper" /usr/share/dbus-1/session.conf > "$DBUS_CONF" 2>/dev/null
|
|
4935
|
+
fi
|
|
4936
|
+
fi
|
|
4937
|
+
if [ -f "$DBUS_CONF" ]; then
|
|
4938
|
+
dbus-daemon --nofork --config-file="$DBUS_CONF" --address="unix:path=$DBUS_SOCK" &
|
|
4939
|
+
else
|
|
4940
|
+
dbus-daemon --session --nofork --address="unix:path=$DBUS_SOCK" &
|
|
4941
|
+
fi
|
|
4840
4942
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_SOCK"
|
|
4841
4943
|
elif [ -x /usr/bin/dbus-launch ]; then
|
|
4842
4944
|
eval $(dbus-launch --sh-syntax 2>/dev/null) || true
|
|
@@ -4884,7 +4986,18 @@ if [ -z "$DBUS_SESSION_BUS_ADDRESS" ]; then
|
|
|
4884
4986
|
if [ "$IS_PROOT" = "1" ] && [ -x /usr/bin/dbus-daemon ]; then
|
|
4885
4987
|
DBUS_SOCK="/tmp/dbus-myagent-$(id -u)"
|
|
4886
4988
|
mkdir -p "$DBUS_SOCK" 2>/dev/null && chmod 700 "$DBUS_SOCK" 2>/dev/null
|
|
4887
|
-
|
|
4989
|
+
# [v1.47.19] 使用自定义配置(移除 servicehelper,避免 --shm-helper 错误)
|
|
4990
|
+
DBUS_CONF="$DBUS_SOCK.conf"
|
|
4991
|
+
if [ ! -f "$DBUS_CONF" ]; then
|
|
4992
|
+
if [ -f /usr/share/dbus-1/session.conf ]; then
|
|
4993
|
+
grep -v "<servicehelper" /usr/share/dbus-1/session.conf > "$DBUS_CONF" 2>/dev/null
|
|
4994
|
+
fi
|
|
4995
|
+
fi
|
|
4996
|
+
if [ -f "$DBUS_CONF" ]; then
|
|
4997
|
+
dbus-daemon --nofork --config-file="$DBUS_CONF" --address="unix:path=$DBUS_SOCK" &
|
|
4998
|
+
else
|
|
4999
|
+
dbus-daemon --session --nofork --address="unix:path=$DBUS_SOCK" &
|
|
5000
|
+
fi
|
|
4888
5001
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_SOCK"
|
|
4889
5002
|
elif [ -x /usr/bin/dbus-launch ]; then
|
|
4890
5003
|
eval $(dbus-launch --sh-syntax 2>/dev/null) || true
|
package/package.json
CHANGED
package/worklog.md
CHANGED
|
@@ -69,3 +69,51 @@ Stage Summary:
|
|
|
69
69
|
- Async non-blocking: waiting for lock does NOT block the event loop (other coroutines run normally)
|
|
70
70
|
- New `browser_wait_status` tool: agents can check lock status and optionally wait for browser to become free
|
|
71
71
|
- All 19 tests passed (7 import/signature + 12 async contention)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
Task ID: 3
|
|
75
|
+
Agent: Main
|
|
76
|
+
Task: Fix model outputting `<output>` XML tags visible to users + VNC mode Firefox fix
|
|
77
|
+
|
|
78
|
+
Work Log:
|
|
79
|
+
- Analyzed the full streaming pipeline: LLM → _call_llm_stream → _emit_text_delta → _text_delta_callback → SSE → Frontend
|
|
80
|
+
- Found root cause: v1.38 switched to native tool_calling, but some models still output old `<output>` XML format
|
|
81
|
+
- When models output `<output>` XML without native tool_calls, the XML was treated as plain text and shown to users
|
|
82
|
+
- The frontend `_stripXmlTags` only ran on keyless assistant messages, not key="reply" messages
|
|
83
|
+
|
|
84
|
+
Changes made:
|
|
85
|
+
1. **agents/main_agent.py** (_process_v2_inner):
|
|
86
|
+
- Added `<output>` XML fallback: when response.tool_calls is empty but content contains `<output>` XML, use output_parser to parse
|
|
87
|
+
- Handles mainsubject, remember, task_plan, and tools_to_call from parsed XML
|
|
88
|
+
- Executes tools from `<toolstocal>` and continues the LLM loop
|
|
89
|
+
- Extracts `<reply>` content for user display, strips all XML tags as fallback
|
|
90
|
+
- Saves clean reply as key="reply" and raw XML as key="llm_output"
|
|
91
|
+
|
|
92
|
+
2. **web/api_server.py** (_text_delta_callback):
|
|
93
|
+
- Added "output_xml" mode to streaming filter
|
|
94
|
+
- Detects `<output>` tag and enters output_xml mode, suppressing all non-reply content
|
|
95
|
+
- Extracts and streams `<reply>` content in real-time
|
|
96
|
+
- Handles both closed `<reply>...</reply>` and unclosed `<reply>` during streaming
|
|
97
|
+
- Exits output_xml mode on `</output>` close tag
|
|
98
|
+
- Updated _flush_remaining_text to handle output_xml mode
|
|
99
|
+
|
|
100
|
+
3. **web/ui/chat/flow_engine.js** (pollChatHistory, forceRefreshHistory):
|
|
101
|
+
- Extended XML stripping to also handle key="reply" messages (not just keyless)
|
|
102
|
+
- Condition: `(!mkey || mkey === 'reply')` for assistant messages starting with `<`
|
|
103
|
+
|
|
104
|
+
4. **web/ui/chat/chat_main.js** (two locations):
|
|
105
|
+
- Same fix: extended XML stripping to handle key="reply" messages
|
|
106
|
+
|
|
107
|
+
5. **aiskills/browser_stealth.py** (VNC+Firefox):
|
|
108
|
+
- Already implemented in previous session: VNC mode directly uses Firefox, skipping Chrome/DrissionPage
|
|
109
|
+
- `_start_firefox_in_vnc()` method handles Firefox launch with proper env vars for proot ARM64
|
|
110
|
+
- `_detect_browser(skip_puppeteer=True)` in VNC mode skips Puppeteer Chrome detection
|
|
111
|
+
|
|
112
|
+
6. **package.json**: Bumped version to 1.47.18, published to npm
|
|
113
|
+
|
|
114
|
+
Stage Summary:
|
|
115
|
+
- `<output>` XML tags no longer visible to users in any path (streaming, polling, history)
|
|
116
|
+
- Models that don't support native tool_calling now have their XML output properly parsed and executed
|
|
117
|
+
- Streaming filter extracts only `<reply>` content for real-time display
|
|
118
|
+
- Frontend strips XML from both keyless and key="reply" assistant messages
|
|
119
|
+
- VNC mode Firefox support fully functional
|