myagent-ai 1.47.23 → 1.47.25
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/aiskills/browser_stealth.py +145 -19
- package/aiskills/chromedev_mcp.py +19 -67
- package/core/output_parser.py +730 -0
- package/core/vnc_manager.py +51 -1111
- package/main.py +1 -19
- package/package.json +3 -3
- package/requirements.txt +1 -0
- package/start.js +1 -5
- package/tray_manager.py +2 -20
- package/web/ui/admin/admin-sites.js +15 -6
- package/web/ui/chat/chat_container.html +2 -2
- package/worklog.md +0 -75
|
@@ -2124,6 +2124,10 @@ class StealthBrowser:
|
|
|
2124
2124
|
message=f"Firefox+VNC 截图已保存",
|
|
2125
2125
|
data={"path": save_path},
|
|
2126
2126
|
)
|
|
2127
|
+
logger.warning(
|
|
2128
|
+
f"[_firefox_screenshot] import 截图失败 (rc={result.returncode}): "
|
|
2129
|
+
f"{result.stderr.decode(errors='ignore')[:200]}"
|
|
2130
|
+
)
|
|
2127
2131
|
|
|
2128
2132
|
# 方法2: 使用 xdotool + scrot
|
|
2129
2133
|
scrot_cmd = shutil.which("scrot")
|
|
@@ -2140,6 +2144,10 @@ class StealthBrowser:
|
|
|
2140
2144
|
message=f"Firefox+VNC 截图已保存 (scrot)",
|
|
2141
2145
|
data={"path": save_path},
|
|
2142
2146
|
)
|
|
2147
|
+
logger.warning(
|
|
2148
|
+
f"[_firefox_screenshot] scrot 截图失败 (rc={result.returncode}): "
|
|
2149
|
+
f"{result.stderr.decode(errors='ignore')[:200]}"
|
|
2150
|
+
)
|
|
2143
2151
|
|
|
2144
2152
|
# 方法3: 使用 xwd + convert
|
|
2145
2153
|
xwd_cmd = shutil.which("xwd")
|
|
@@ -2168,21 +2176,93 @@ class StealthBrowser:
|
|
|
2168
2176
|
message=f"Firefox+VNC 截图已保存",
|
|
2169
2177
|
data={"path": save_path},
|
|
2170
2178
|
)
|
|
2179
|
+
logger.warning(
|
|
2180
|
+
f"[_firefox_screenshot] xwd+convert 截图失败 (rc={result.returncode}): "
|
|
2181
|
+
f"{result.stderr.decode(errors='ignore')[:200]}"
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
# 方法4: 使用 xwd + ffmpeg (proot 兼容)
|
|
2185
|
+
ffmpeg_cmd = shutil.which("ffmpeg")
|
|
2186
|
+
if xwd_cmd and ffmpeg_cmd:
|
|
2187
|
+
xwd_tmp = save_path + ".xwd"
|
|
2188
|
+
result = subprocess.run(
|
|
2189
|
+
[xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
|
|
2190
|
+
capture_output=True, timeout=10,
|
|
2191
|
+
env=env, start_new_session=True,
|
|
2192
|
+
)
|
|
2193
|
+
if result.returncode == 0 and os.path.isfile(xwd_tmp):
|
|
2194
|
+
result2 = subprocess.run(
|
|
2195
|
+
[ffmpeg_cmd, "-y", "-i", xwd_tmp, save_path],
|
|
2196
|
+
capture_output=True, timeout=10,
|
|
2197
|
+
env=env, start_new_session=True,
|
|
2198
|
+
)
|
|
2199
|
+
try:
|
|
2200
|
+
os.unlink(xwd_tmp)
|
|
2201
|
+
except Exception:
|
|
2202
|
+
pass
|
|
2203
|
+
if result2.returncode == 0 and os.path.isfile(save_path):
|
|
2204
|
+
logger.info(f"Firefox 截图已保存 (xwd+ffmpeg): {save_path}")
|
|
2205
|
+
return SkillResult(
|
|
2206
|
+
success=True,
|
|
2207
|
+
message=f"Firefox+VNC 截图已保存",
|
|
2208
|
+
data={"path": save_path},
|
|
2209
|
+
)
|
|
2210
|
+
logger.warning(
|
|
2211
|
+
f"[_firefox_screenshot] xwd+ffmpeg 截图失败: xwd_rc={result.returncode}"
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
# 方法5: 使用 Python Pillow + xwd
|
|
2215
|
+
try:
|
|
2216
|
+
from PIL import Image
|
|
2217
|
+
if xwd_cmd:
|
|
2218
|
+
xwd_tmp = save_path + ".xwd"
|
|
2219
|
+
result = subprocess.run(
|
|
2220
|
+
[xwd_cmd, "-root", "-display", display, "-out", xwd_tmp],
|
|
2221
|
+
capture_output=True, timeout=10,
|
|
2222
|
+
env=env, start_new_session=True,
|
|
2223
|
+
)
|
|
2224
|
+
if result.returncode == 0 and os.path.isfile(xwd_tmp):
|
|
2225
|
+
try:
|
|
2226
|
+
img = Image.open(xwd_tmp)
|
|
2227
|
+
img.save(save_path, "PNG")
|
|
2228
|
+
os.unlink(xwd_tmp)
|
|
2229
|
+
logger.info(f"Firefox 截图已保存 (xwd+Pillow): {save_path}")
|
|
2230
|
+
return SkillResult(
|
|
2231
|
+
success=True,
|
|
2232
|
+
message=f"Firefox+VNC 截图已保存",
|
|
2233
|
+
data={"path": save_path},
|
|
2234
|
+
)
|
|
2235
|
+
except Exception as pil_err:
|
|
2236
|
+
logger.warning(f"[_firefox_screenshot] Pillow 转换失败: {pil_err}")
|
|
2237
|
+
try: os.unlink(xwd_tmp)
|
|
2238
|
+
except Exception: pass
|
|
2239
|
+
except ImportError:
|
|
2240
|
+
pass
|
|
2241
|
+
|
|
2242
|
+
_available = []
|
|
2243
|
+
if import_cmd: _available.append("import")
|
|
2244
|
+
if scrot_cmd: _available.append("scrot")
|
|
2245
|
+
if xwd_cmd and convert_cmd: _available.append("xwd+convert")
|
|
2246
|
+
if xwd_cmd and ffmpeg_cmd: _available.append("xwd+ffmpeg")
|
|
2247
|
+
_tried = ", ".join(_available) if _available else "无"
|
|
2171
2248
|
|
|
2172
2249
|
return SkillResult(
|
|
2173
2250
|
success=False,
|
|
2174
|
-
error="Firefox+VNC
|
|
2175
|
-
"
|
|
2251
|
+
error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
|
|
2252
|
+
f"— 需要 ImageMagick / scrot / xwd+convert/ffmpeg",
|
|
2176
2253
|
)
|
|
2177
2254
|
except Exception as e:
|
|
2178
2255
|
return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
|
|
2179
2256
|
|
|
2180
|
-
def _firefox_read_sessionstore(self) -> dict:
|
|
2257
|
+
def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
|
|
2181
2258
|
"""[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
|
|
2182
2259
|
|
|
2183
2260
|
Firefox 运行时会将当前会话信息写入 sessionstore-backups/recovery.jsonlz4。
|
|
2184
2261
|
该文件使用 mozLz4 格式(8字节自定义头 + LZ4 压缩数据)。
|
|
2185
2262
|
|
|
2263
|
+
Args:
|
|
2264
|
+
max_wait: 最长等待时间(秒),给 Firefox 时间写入 sessionstore
|
|
2265
|
+
|
|
2186
2266
|
Returns:
|
|
2187
2267
|
dict: {"url": str, "title": str, "tabs": [{"url": str, "title": str}]}
|
|
2188
2268
|
"""
|
|
@@ -2193,30 +2273,56 @@ class StealthBrowser:
|
|
|
2193
2273
|
if self._firefox_profile_dir:
|
|
2194
2274
|
search_dirs.append(self._firefox_profile_dir)
|
|
2195
2275
|
# 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
|
|
2276
|
+
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2196
2277
|
for extra in [
|
|
2197
|
-
os.path.
|
|
2198
|
-
|
|
2278
|
+
os.path.join(ff_base, "default"),
|
|
2279
|
+
ff_base,
|
|
2199
2280
|
]:
|
|
2200
2281
|
if os.path.isdir(extra) and extra not in search_dirs:
|
|
2201
2282
|
search_dirs.append(extra)
|
|
2283
|
+
# [v1.47.24] 搜索所有 Firefox profile 子目录(如 xxx.default-release)
|
|
2284
|
+
try:
|
|
2285
|
+
if os.path.isdir(ff_base):
|
|
2286
|
+
for entry in os.listdir(ff_base):
|
|
2287
|
+
entry_path = os.path.join(ff_base, entry)
|
|
2288
|
+
if os.path.isdir(entry_path) and entry_path not in search_dirs:
|
|
2289
|
+
# Firefox profile 目录通常包含 user.js 或 prefs.js
|
|
2290
|
+
if os.path.isfile(os.path.join(entry_path, "prefs.js")) or \
|
|
2291
|
+
os.path.isfile(os.path.join(entry_path, "user.js")):
|
|
2292
|
+
search_dirs.append(entry_path)
|
|
2293
|
+
except Exception:
|
|
2294
|
+
pass
|
|
2202
2295
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2296
|
+
# [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
|
|
2297
|
+
_start_time = time.time()
|
|
2298
|
+
while True:
|
|
2299
|
+
recovery_files = []
|
|
2300
|
+
for base_dir in search_dirs:
|
|
2301
|
+
ss_dir = os.path.join(base_dir, "sessionstore-backups")
|
|
2302
|
+
if os.path.isdir(ss_dir):
|
|
2303
|
+
for fname in ("recovery.jsonlz4", "recovery.baklz4",
|
|
2304
|
+
"previous.jsonlz4"):
|
|
2305
|
+
fpath = os.path.join(ss_dir, fname)
|
|
2306
|
+
if os.path.isfile(fpath):
|
|
2307
|
+
recovery_files.append(fpath)
|
|
2308
|
+
# 也检查 base_dir 本身(有些 Firefox 版本)
|
|
2309
|
+
for fname in ("sessionstore.jsonlz4", "sessionstore-backups/recovery.jsonlz4"):
|
|
2310
|
+
fpath = os.path.join(base_dir, fname)
|
|
2210
2311
|
if os.path.isfile(fpath):
|
|
2211
2312
|
recovery_files.append(fpath)
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2313
|
+
|
|
2314
|
+
# 如果找到了文件,或者超时了,就跳出
|
|
2315
|
+
if recovery_files or (time.time() - _start_time) >= max_wait:
|
|
2316
|
+
break
|
|
2317
|
+
|
|
2318
|
+
# 等 0.5 秒再试
|
|
2319
|
+
time.sleep(0.5)
|
|
2217
2320
|
|
|
2218
2321
|
if not recovery_files:
|
|
2219
|
-
logger.
|
|
2322
|
+
logger.warning(
|
|
2323
|
+
f"[_firefox_read_sessionstore] 未找到 sessionstore 文件 "
|
|
2324
|
+
f"(搜索了 {len(search_dirs)} 个目录: {[d.replace(os.path.expanduser('~'), '~') for d in search_dirs]})"
|
|
2325
|
+
)
|
|
2220
2326
|
return result_info
|
|
2221
2327
|
|
|
2222
2328
|
# 读取 mozLz4 格式
|
|
@@ -2290,9 +2396,19 @@ class StealthBrowser:
|
|
|
2290
2396
|
screenshot_path = ""
|
|
2291
2397
|
if screenshot_result.success and screenshot_result.data:
|
|
2292
2398
|
screenshot_path = screenshot_result.data.get("path", "")
|
|
2399
|
+
logger.info(f"[_firefox_get_content] 截图成功: {screenshot_path}")
|
|
2400
|
+
else:
|
|
2401
|
+
logger.warning(
|
|
2402
|
+
f"[_firefox_get_content] 截图失败: "
|
|
2403
|
+
f"{screenshot_result.error or '无数据'}"
|
|
2404
|
+
)
|
|
2293
2405
|
|
|
2294
2406
|
# 2. 读取 sessionstore
|
|
2295
2407
|
session_info = self._firefox_read_sessionstore()
|
|
2408
|
+
logger.info(
|
|
2409
|
+
f"[_firefox_get_content] sessionstore: url={session_info.get('url', '')}, "
|
|
2410
|
+
f"title={session_info.get('title', '')}, tabs={len(session_info.get('tabs', []))}"
|
|
2411
|
+
)
|
|
2296
2412
|
|
|
2297
2413
|
# 3. 组合返回信息
|
|
2298
2414
|
url = session_info.get("url", "")
|
|
@@ -2315,7 +2431,17 @@ class StealthBrowser:
|
|
|
2315
2431
|
if screenshot_path:
|
|
2316
2432
|
content_parts.append(f"截图: {screenshot_path}")
|
|
2317
2433
|
|
|
2318
|
-
|
|
2434
|
+
if not content_parts:
|
|
2435
|
+
# 全部失败 → 至少给截图失败原因
|
|
2436
|
+
fallback_msg = "Firefox+VNC 无法获取页面内容"
|
|
2437
|
+
if not screenshot_result.success:
|
|
2438
|
+
fallback_msg += f" (截图失败: {screenshot_result.error})"
|
|
2439
|
+
if not session_info.get("url"):
|
|
2440
|
+
fallback_msg += " (sessionstore 未读取到 URL)"
|
|
2441
|
+
logger.error(f"[_firefox_get_content] {fallback_msg}")
|
|
2442
|
+
content_text = fallback_msg
|
|
2443
|
+
else:
|
|
2444
|
+
content_text = "\n".join(content_parts)
|
|
2319
2445
|
|
|
2320
2446
|
return SkillResult(
|
|
2321
2447
|
success=True,
|
|
@@ -189,55 +189,27 @@ def _ensure_display() -> Optional[str]:
|
|
|
189
189
|
logger.info(f"VNC 远程桌面已在运行,复用显示: {display}")
|
|
190
190
|
return display
|
|
191
191
|
|
|
192
|
-
#
|
|
193
|
-
# 修复: 不能在 ThreadPoolExecutor + asyncio.run 中直接调用 vnc.start()!
|
|
194
|
-
# 因为 vnc.start() 内部用 asyncio.Lock(),跨 event loop 会导致死锁。
|
|
195
|
-
# 正确做法:在单独线程中用 asyncio.run 启动,然后轮询等待 VNC 就绪。
|
|
192
|
+
# VNC 未运行 → 尝试自动启动
|
|
196
193
|
try:
|
|
197
194
|
import asyncio
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
logger.info(f"VNC 远程桌面已启动,复用显示: {display}")
|
|
217
|
-
return display
|
|
218
|
-
|
|
219
|
-
try:
|
|
220
|
-
result = future.result(timeout=0.1)
|
|
221
|
-
if result.get("success") or vnc.is_running:
|
|
222
|
-
display = vnc.display
|
|
223
|
-
os.environ["DISPLAY"] = display
|
|
224
|
-
logger.info(f"VNC 远程桌面已启动,显示: {display}")
|
|
225
|
-
return display
|
|
226
|
-
else:
|
|
227
|
-
logger.warning(f"VNC 启动失败: {result.get('message', '')}")
|
|
228
|
-
break
|
|
229
|
-
except concurrent.futures.TimeoutError:
|
|
230
|
-
pass
|
|
231
|
-
|
|
232
|
-
import time
|
|
233
|
-
time.sleep(check_interval)
|
|
234
|
-
elapsed += check_interval
|
|
235
|
-
|
|
236
|
-
if vnc.is_running:
|
|
237
|
-
display = vnc.display
|
|
238
|
-
os.environ["DISPLAY"] = display
|
|
239
|
-
return display
|
|
240
|
-
|
|
195
|
+
try:
|
|
196
|
+
loop = asyncio.get_event_loop()
|
|
197
|
+
if loop.is_running():
|
|
198
|
+
import concurrent.futures
|
|
199
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
200
|
+
result = pool.submit(asyncio.run, vnc.start()).result(timeout=30)
|
|
201
|
+
else:
|
|
202
|
+
result = asyncio.run(vnc.start())
|
|
203
|
+
except RuntimeError:
|
|
204
|
+
result = asyncio.run(vnc.start())
|
|
205
|
+
|
|
206
|
+
if result.get("success"):
|
|
207
|
+
display = vnc.display
|
|
208
|
+
os.environ["DISPLAY"] = display
|
|
209
|
+
logger.info(f"VNC 远程桌面已自动启动,显示: {display}")
|
|
210
|
+
return display
|
|
211
|
+
else:
|
|
212
|
+
logger.warning(f"VNC 自动启动失败: {result.get('message', '')}")
|
|
241
213
|
except Exception as e:
|
|
242
214
|
logger.warning(f"VNC 自动启动异常: {e}")
|
|
243
215
|
|
|
@@ -1372,26 +1344,6 @@ class BrowserOpenSkill(Skill):
|
|
|
1372
1344
|
if not url:
|
|
1373
1345
|
return SkillResult(success=False, error="缺少必需参数: url")
|
|
1374
1346
|
|
|
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
|
-
|
|
1395
1347
|
# 检查依赖
|
|
1396
1348
|
dep_err = await asyncio.get_event_loop().run_in_executor(None, _ensure_node_deps)
|
|
1397
1349
|
if dep_err:
|