myagent-ai 1.47.22 → 1.47.24
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 +73 -12
- package/aiskills/browser_stealth.py +117 -19
- package/package.json +1 -1
package/agents/main_agent.py
CHANGED
|
@@ -943,16 +943,77 @@ class MainAgent(BaseAgent):
|
|
|
943
943
|
_reasoning_parts.append(delta_text)
|
|
944
944
|
await self._emit_v2_event("v2_reasoning", {"content": delta_text}, stream_callback)
|
|
945
945
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
946
|
+
# 可重试的 LLM 调用(最多 5 次)
|
|
947
|
+
_max_llm_retries = 5
|
|
948
|
+
_llm_retry_count = 0
|
|
949
|
+
response = None
|
|
950
|
+
|
|
951
|
+
while _llm_retry_count < _max_llm_retries:
|
|
952
|
+
try:
|
|
953
|
+
if stream_response and self.llm:
|
|
954
|
+
response = await self._call_llm_stream(
|
|
955
|
+
messages,
|
|
956
|
+
tools=tools if tools else None,
|
|
957
|
+
text_delta_callback=text_delta_callback,
|
|
958
|
+
reasoning_delta_callback=_reasoning_delta_cb,
|
|
959
|
+
stream_response=stream_response,
|
|
960
|
+
)
|
|
961
|
+
else:
|
|
962
|
+
response = await self._call_llm(messages, tools=tools if tools else None)
|
|
963
|
+
except Exception as _llm_exc:
|
|
964
|
+
# 异常类错误 → 判断是否可重试
|
|
965
|
+
_llm_exc_str = str(_llm_exc).lower()
|
|
966
|
+
_is_retryable = any(kw in _llm_exc_str for kw in (
|
|
967
|
+
"connection", "timeout", "timed out",
|
|
968
|
+
"429", "500", "502", "503", "504",
|
|
969
|
+
"rate_limit", "rate limit", "overloaded", "capacity",
|
|
970
|
+
"network", "eof",
|
|
971
|
+
))
|
|
972
|
+
_llm_retry_count += 1
|
|
973
|
+
if _is_retryable and _llm_retry_count < _max_llm_retries:
|
|
974
|
+
_delay = 2.0 * (2 ** (_llm_retry_count - 1)) # 2s, 4s, 8s, 16s
|
|
975
|
+
logger.warning(
|
|
976
|
+
f"[{task_id}] LLM 调用异常 (第 {_llm_retry_count}/{_max_llm_retries} 次),"
|
|
977
|
+
f"{_delay:.0f}s 后重试: {_llm_exc}"
|
|
978
|
+
)
|
|
979
|
+
await self._emit_v2_event("v2_reasoning", {
|
|
980
|
+
"content": f"⏳ 网络不稳定,正在重试 ({_llm_retry_count}/{_max_llm_retries})..."
|
|
981
|
+
}, stream_callback)
|
|
982
|
+
await asyncio.sleep(_delay)
|
|
983
|
+
continue
|
|
984
|
+
else:
|
|
985
|
+
# 不可重试 或 已达上限 → 包装为失败 response
|
|
986
|
+
logger.error(f"[{task_id}] LLM 调用异常 (已重试 {_llm_retry_count} 次): {_llm_exc}")
|
|
987
|
+
response = type("LLMResponse", (), {"success": False, "error": str(_llm_exc)})()
|
|
988
|
+
break
|
|
989
|
+
|
|
990
|
+
# 没有异常 → 检查 response.success
|
|
991
|
+
if response.success:
|
|
992
|
+
break # 成功 → 跳出重试循环
|
|
993
|
+
|
|
994
|
+
# response.success == False 但没抛异常 → 判断错误是否可重试
|
|
995
|
+
_llm_error = (response.error or "").lower()
|
|
996
|
+
_is_retryable = any(kw in _llm_error for kw in (
|
|
997
|
+
"connection", "timeout", "timed out",
|
|
998
|
+
"429", "500", "502", "503", "504",
|
|
999
|
+
"rate_limit", "rate limit", "overloaded", "capacity",
|
|
1000
|
+
"network", "eof",
|
|
1001
|
+
))
|
|
1002
|
+
_llm_retry_count += 1
|
|
1003
|
+
if _is_retryable and _llm_retry_count < _max_llm_retries:
|
|
1004
|
+
_delay = 2.0 * (2 ** (_llm_retry_count - 1))
|
|
1005
|
+
logger.warning(
|
|
1006
|
+
f"[{task_id}] LLM 返回失败 (第 {_llm_retry_count}/{_max_llm_retries} 次),"
|
|
1007
|
+
f"{_delay:.0f}s 后重试: {response.error}"
|
|
1008
|
+
)
|
|
1009
|
+
await self._emit_v2_event("v2_reasoning", {
|
|
1010
|
+
"content": f"⏳ 网络不稳定,正在重试 ({_llm_retry_count}/{_max_llm_retries})..."
|
|
1011
|
+
}, stream_callback)
|
|
1012
|
+
await asyncio.sleep(_delay)
|
|
1013
|
+
continue
|
|
1014
|
+
else:
|
|
1015
|
+
# 不可重试 或 已达上限 → 保持失败 response,退出循环
|
|
1016
|
+
break
|
|
956
1017
|
|
|
957
1018
|
# LLM 调用失败处理
|
|
958
1019
|
if not response.success:
|
|
@@ -982,8 +1043,8 @@ class MainAgent(BaseAgent):
|
|
|
982
1043
|
await self._emit_v2_event("v2_reasoning", {"content": _vision_skip_msg}, stream_callback)
|
|
983
1044
|
break
|
|
984
1045
|
|
|
985
|
-
#
|
|
986
|
-
error_msg = f"LLM
|
|
1046
|
+
# 其他错误(含已耗尽重试次数的连接错误)
|
|
1047
|
+
error_msg = f"LLM 调用失败 (已重试 {_llm_retry_count} 次): {_llm_error}"
|
|
987
1048
|
context.working_memory["final_response"] = error_msg
|
|
988
1049
|
await self._emit_v2_event("v2_reasoning", {"content": error_msg}, stream_callback)
|
|
989
1050
|
break
|
|
@@ -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,65 @@ 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: 使用 xdotool + xwd + ffmpeg (proot 兼容)
|
|
2185
|
+
xdotool_cmd = shutil.which("xdotool")
|
|
2186
|
+
ffmpeg_cmd = shutil.which("ffmpeg")
|
|
2187
|
+
if xdotool_cmd and ffmpeg_cmd:
|
|
2188
|
+
xwd_tmp = save_path + ".xwd"
|
|
2189
|
+
# 用 xdotool 获取活动窗口并截图
|
|
2190
|
+
result = subprocess.run(
|
|
2191
|
+
[xwd_cmd or "xwd", "-root", "-display", display, "-out", xwd_tmp],
|
|
2192
|
+
capture_output=True, timeout=10,
|
|
2193
|
+
env=env, start_new_session=True,
|
|
2194
|
+
)
|
|
2195
|
+
if result.returncode == 0 and os.path.isfile(xwd_tmp):
|
|
2196
|
+
# 用 ffmpeg 转换 xwd 到 png
|
|
2197
|
+
result2 = subprocess.run(
|
|
2198
|
+
[ffmpeg_cmd, "-y", "-i", xwd_tmp, save_path],
|
|
2199
|
+
capture_output=True, timeout=10,
|
|
2200
|
+
env=env, start_new_session=True,
|
|
2201
|
+
)
|
|
2202
|
+
try:
|
|
2203
|
+
os.unlink(xwd_tmp)
|
|
2204
|
+
except Exception:
|
|
2205
|
+
pass
|
|
2206
|
+
if result2.returncode == 0 and os.path.isfile(save_path):
|
|
2207
|
+
logger.info(f"Firefox 截图已保存 (xwd+ffmpeg): {save_path}")
|
|
2208
|
+
return SkillResult(
|
|
2209
|
+
success=True,
|
|
2210
|
+
message=f"Firefox+VNC 截图已保存",
|
|
2211
|
+
data={"path": save_path},
|
|
2212
|
+
)
|
|
2213
|
+
|
|
2214
|
+
_available = []
|
|
2215
|
+
if import_cmd: _available.append("import")
|
|
2216
|
+
if scrot_cmd: _available.append("scrot")
|
|
2217
|
+
if xwd_cmd and convert_cmd: _available.append("xwd+convert")
|
|
2218
|
+
if xdotool_cmd and ffmpeg_cmd: _available.append("xdotool+ffmpeg")
|
|
2219
|
+
_tried = ", ".join(_available) if _available else "无"
|
|
2171
2220
|
|
|
2172
2221
|
return SkillResult(
|
|
2173
2222
|
success=False,
|
|
2174
|
-
error="Firefox+VNC
|
|
2175
|
-
"
|
|
2223
|
+
error=f"Firefox+VNC 截图失败 (尝试过: {_tried}) "
|
|
2224
|
+
f"— 需要 ImageMagick import / scrot / xwd+convert",
|
|
2176
2225
|
)
|
|
2177
2226
|
except Exception as e:
|
|
2178
2227
|
return SkillResult(success=False, error=f"Firefox+VNC 截图失败: {e}")
|
|
2179
2228
|
|
|
2180
|
-
def _firefox_read_sessionstore(self) -> dict:
|
|
2229
|
+
def _firefox_read_sessionstore(self, max_wait: float = 5.0) -> dict:
|
|
2181
2230
|
"""[v1.47.20] 读取 Firefox sessionstore 获取当前标签页 URL/标题。
|
|
2182
2231
|
|
|
2183
2232
|
Firefox 运行时会将当前会话信息写入 sessionstore-backups/recovery.jsonlz4。
|
|
2184
2233
|
该文件使用 mozLz4 格式(8字节自定义头 + LZ4 压缩数据)。
|
|
2185
2234
|
|
|
2235
|
+
Args:
|
|
2236
|
+
max_wait: 最长等待时间(秒),给 Firefox 时间写入 sessionstore
|
|
2237
|
+
|
|
2186
2238
|
Returns:
|
|
2187
2239
|
dict: {"url": str, "title": str, "tabs": [{"url": str, "title": str}]}
|
|
2188
2240
|
"""
|
|
@@ -2193,30 +2245,56 @@ class StealthBrowser:
|
|
|
2193
2245
|
if self._firefox_profile_dir:
|
|
2194
2246
|
search_dirs.append(self._firefox_profile_dir)
|
|
2195
2247
|
# 也搜索 Firefox 默认 profile 和 vnc_manager 启动的 profile
|
|
2248
|
+
ff_base = os.path.expanduser("~/.mozilla/firefox")
|
|
2196
2249
|
for extra in [
|
|
2197
|
-
os.path.
|
|
2198
|
-
|
|
2250
|
+
os.path.join(ff_base, "default"),
|
|
2251
|
+
ff_base,
|
|
2199
2252
|
]:
|
|
2200
2253
|
if os.path.isdir(extra) and extra not in search_dirs:
|
|
2201
2254
|
search_dirs.append(extra)
|
|
2255
|
+
# [v1.47.24] 搜索所有 Firefox profile 子目录(如 xxx.default-release)
|
|
2256
|
+
try:
|
|
2257
|
+
if os.path.isdir(ff_base):
|
|
2258
|
+
for entry in os.listdir(ff_base):
|
|
2259
|
+
entry_path = os.path.join(ff_base, entry)
|
|
2260
|
+
if os.path.isdir(entry_path) and entry_path not in search_dirs:
|
|
2261
|
+
# Firefox profile 目录通常包含 user.js 或 prefs.js
|
|
2262
|
+
if os.path.isfile(os.path.join(entry_path, "prefs.js")) or \
|
|
2263
|
+
os.path.isfile(os.path.join(entry_path, "user.js")):
|
|
2264
|
+
search_dirs.append(entry_path)
|
|
2265
|
+
except Exception:
|
|
2266
|
+
pass
|
|
2202
2267
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2268
|
+
# [v1.47.24] 等待 Firefox 写入 sessionstore(刚导航完可能还没写入)
|
|
2269
|
+
_start_time = time.time()
|
|
2270
|
+
while True:
|
|
2271
|
+
recovery_files = []
|
|
2272
|
+
for base_dir in search_dirs:
|
|
2273
|
+
ss_dir = os.path.join(base_dir, "sessionstore-backups")
|
|
2274
|
+
if os.path.isdir(ss_dir):
|
|
2275
|
+
for fname in ("recovery.jsonlz4", "recovery.baklz4",
|
|
2276
|
+
"previous.jsonlz4"):
|
|
2277
|
+
fpath = os.path.join(ss_dir, fname)
|
|
2278
|
+
if os.path.isfile(fpath):
|
|
2279
|
+
recovery_files.append(fpath)
|
|
2280
|
+
# 也检查 base_dir 本身(有些 Firefox 版本)
|
|
2281
|
+
for fname in ("sessionstore.jsonlz4", "sessionstore-backups/recovery.jsonlz4"):
|
|
2282
|
+
fpath = os.path.join(base_dir, fname)
|
|
2210
2283
|
if os.path.isfile(fpath):
|
|
2211
2284
|
recovery_files.append(fpath)
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2285
|
+
|
|
2286
|
+
# 如果找到了文件,或者超时了,就跳出
|
|
2287
|
+
if recovery_files or (time.time() - _start_time) >= max_wait:
|
|
2288
|
+
break
|
|
2289
|
+
|
|
2290
|
+
# 等 0.5 秒再试
|
|
2291
|
+
time.sleep(0.5)
|
|
2217
2292
|
|
|
2218
2293
|
if not recovery_files:
|
|
2219
|
-
logger.
|
|
2294
|
+
logger.warning(
|
|
2295
|
+
f"[_firefox_read_sessionstore] 未找到 sessionstore 文件 "
|
|
2296
|
+
f"(搜索了 {len(search_dirs)} 个目录: {[d.replace(os.path.expanduser('~'), '~') for d in search_dirs]})"
|
|
2297
|
+
)
|
|
2220
2298
|
return result_info
|
|
2221
2299
|
|
|
2222
2300
|
# 读取 mozLz4 格式
|
|
@@ -2290,9 +2368,19 @@ class StealthBrowser:
|
|
|
2290
2368
|
screenshot_path = ""
|
|
2291
2369
|
if screenshot_result.success and screenshot_result.data:
|
|
2292
2370
|
screenshot_path = screenshot_result.data.get("path", "")
|
|
2371
|
+
logger.info(f"[_firefox_get_content] 截图成功: {screenshot_path}")
|
|
2372
|
+
else:
|
|
2373
|
+
logger.warning(
|
|
2374
|
+
f"[_firefox_get_content] 截图失败: "
|
|
2375
|
+
f"{screenshot_result.error or '无数据'}"
|
|
2376
|
+
)
|
|
2293
2377
|
|
|
2294
2378
|
# 2. 读取 sessionstore
|
|
2295
2379
|
session_info = self._firefox_read_sessionstore()
|
|
2380
|
+
logger.info(
|
|
2381
|
+
f"[_firefox_get_content] sessionstore: url={session_info.get('url', '')}, "
|
|
2382
|
+
f"title={session_info.get('title', '')}, tabs={len(session_info.get('tabs', []))}"
|
|
2383
|
+
)
|
|
2296
2384
|
|
|
2297
2385
|
# 3. 组合返回信息
|
|
2298
2386
|
url = session_info.get("url", "")
|
|
@@ -2315,7 +2403,17 @@ class StealthBrowser:
|
|
|
2315
2403
|
if screenshot_path:
|
|
2316
2404
|
content_parts.append(f"截图: {screenshot_path}")
|
|
2317
2405
|
|
|
2318
|
-
|
|
2406
|
+
if not content_parts:
|
|
2407
|
+
# 全部失败 → 至少给截图失败原因
|
|
2408
|
+
fallback_msg = "Firefox+VNC 无法获取页面内容"
|
|
2409
|
+
if not screenshot_result.success:
|
|
2410
|
+
fallback_msg += f" (截图失败: {screenshot_result.error})"
|
|
2411
|
+
if not session_info.get("url"):
|
|
2412
|
+
fallback_msg += " (sessionstore 未读取到 URL)"
|
|
2413
|
+
logger.error(f"[_firefox_get_content] {fallback_msg}")
|
|
2414
|
+
content_text = fallback_msg
|
|
2415
|
+
else:
|
|
2416
|
+
content_text = "\n".join(content_parts)
|
|
2319
2417
|
|
|
2320
2418
|
return SkillResult(
|
|
2321
2419
|
success=True,
|