myagent-ai 1.47.17 → 1.47.19
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 +194 -2
- package/aiskills/browser_stealth.py +32 -4
- package/core/vnc_manager.py +119 -6
- package/package.json +1 -1
- package/web/api_server.py +94 -2
- package/web/ui/chat/chat_main.js +9 -7
- package/web/ui/chat/flow_engine.js +11 -7
- package/worklog.md +48 -0
package/agents/main_agent.py
CHANGED
|
@@ -1222,8 +1222,200 @@ class MainAgent(BaseAgent):
|
|
|
1222
1222
|
continue
|
|
1223
1223
|
|
|
1224
1224
|
else:
|
|
1225
|
-
#
|
|
1226
|
-
|
|
1225
|
+
# 没有原生工具调用 → 检查是否为旧格式 <output> XML(某些模型不支持 tool_calling)
|
|
1226
|
+
raw_content = (response.content or "").strip()
|
|
1227
|
+
|
|
1228
|
+
# [v1.47.16] 兼容旧格式:当 LLM 输出 <output> XML 时,用 output_parser 解析
|
|
1229
|
+
if raw_content.startswith("<output") or ("<output>" in raw_content and "<toolstocal>" in raw_content):
|
|
1230
|
+
logger.info(f"[{task_id}] 检测到旧格式 <output> XML 输出,启用 output_parser 解析")
|
|
1231
|
+
parsed = parse_output(raw_content)
|
|
1232
|
+
|
|
1233
|
+
if parsed.parse_success:
|
|
1234
|
+
# 1) 处理 mainsubject → 更新会话标题
|
|
1235
|
+
if parsed.mainsubject and self.dispatcher:
|
|
1236
|
+
try:
|
|
1237
|
+
await self.dispatcher.dispatch(
|
|
1238
|
+
tool_name="update_conversation_title",
|
|
1239
|
+
params={"title": parsed.mainsubject, "session_id": context.session_id},
|
|
1240
|
+
timeout=10,
|
|
1241
|
+
)
|
|
1242
|
+
except Exception:
|
|
1243
|
+
pass
|
|
1244
|
+
|
|
1245
|
+
# 2) 处理 remember → 保存记忆
|
|
1246
|
+
if parsed.remember and self.dispatcher:
|
|
1247
|
+
try:
|
|
1248
|
+
await self.dispatcher.dispatch(
|
|
1249
|
+
tool_name="save_memory",
|
|
1250
|
+
params={
|
|
1251
|
+
"content": parsed.remember,
|
|
1252
|
+
"type": parsed.remember_type or "session",
|
|
1253
|
+
"session_id": context.session_id,
|
|
1254
|
+
},
|
|
1255
|
+
timeout=10,
|
|
1256
|
+
)
|
|
1257
|
+
except Exception:
|
|
1258
|
+
pass
|
|
1259
|
+
|
|
1260
|
+
# 3) 处理 task_plan
|
|
1261
|
+
if parsed.task_plan and self.dispatcher:
|
|
1262
|
+
try:
|
|
1263
|
+
await self.dispatcher.dispatch(
|
|
1264
|
+
tool_name="task_plan",
|
|
1265
|
+
params={"action": "create", "plan": parsed.task_plan},
|
|
1266
|
+
timeout=10,
|
|
1267
|
+
)
|
|
1268
|
+
current_task_plan = parsed.task_plan
|
|
1269
|
+
await self._emit_v2_event(
|
|
1270
|
+
"v2_task_plan",
|
|
1271
|
+
{"plan": truncate_str(current_task_plan, 2000)},
|
|
1272
|
+
stream_callback,
|
|
1273
|
+
)
|
|
1274
|
+
except Exception:
|
|
1275
|
+
pass
|
|
1276
|
+
|
|
1277
|
+
# 4) 处理 tools_to_call → 执行工具
|
|
1278
|
+
if parsed.tools_to_call:
|
|
1279
|
+
logger.info(f"[{task_id}] 从 <output> XML 提取到 {len(parsed.tools_to_call)} 个工具调用")
|
|
1280
|
+
|
|
1281
|
+
# 添加 assistant 消息到消息列表
|
|
1282
|
+
messages.append(Message(
|
|
1283
|
+
role="assistant",
|
|
1284
|
+
content=raw_content,
|
|
1285
|
+
))
|
|
1286
|
+
|
|
1287
|
+
# 保存 LLM 原始输出
|
|
1288
|
+
if self.memory:
|
|
1289
|
+
self.memory.add_session(agent_id=_effective_agent_id,
|
|
1290
|
+
session_id=context.session_id,
|
|
1291
|
+
role="assistant",
|
|
1292
|
+
content=raw_content,
|
|
1293
|
+
key="llm_output",
|
|
1294
|
+
importance=0.3,
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
for tool_desc in parsed.tools_to_call:
|
|
1298
|
+
_tc_name = tool_desc.get("toolname", "")
|
|
1299
|
+
_tc_parms = tool_desc.get("parms", "{}")
|
|
1300
|
+
_tc_timeout = int(tool_desc.get("timeout", 120))
|
|
1301
|
+
|
|
1302
|
+
if not _tc_name:
|
|
1303
|
+
continue
|
|
1304
|
+
|
|
1305
|
+
# 注入 session_id
|
|
1306
|
+
if _tc_name in ("save_memory", "recall_memory", "update_conversation_title"):
|
|
1307
|
+
if isinstance(_tc_parms, str):
|
|
1308
|
+
try:
|
|
1309
|
+
_tc_parms_dict = json.loads(_tc_parms)
|
|
1310
|
+
except (json.JSONDecodeError, TypeError):
|
|
1311
|
+
_tc_parms_dict = {"raw_input": _tc_parms}
|
|
1312
|
+
else:
|
|
1313
|
+
_tc_parms_dict = _tc_parms
|
|
1314
|
+
_tc_parms_dict.setdefault("session_id", context.session_id)
|
|
1315
|
+
_tc_parms = json.dumps(_tc_parms_dict, ensure_ascii=False)
|
|
1316
|
+
|
|
1317
|
+
# 发送工具开始事件
|
|
1318
|
+
await self._emit_v2_event(
|
|
1319
|
+
"v2_tool_start",
|
|
1320
|
+
{"tool": {"toolname": _tc_name, "parms": truncate_str(str(_tc_parms), 500)}},
|
|
1321
|
+
stream_callback,
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
self._add_exec_event("tool_call", {
|
|
1325
|
+
"title": f"调用工具: {_tc_name}",
|
|
1326
|
+
"tool_name": _tc_name,
|
|
1327
|
+
"arguments": str(_tc_parms),
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
# 执行工具
|
|
1331
|
+
tool_result = await self._execute_v2_tool(
|
|
1332
|
+
_tc_name, str(_tc_parms), _tc_timeout,
|
|
1333
|
+
context, task_id,
|
|
1334
|
+
stream_callback=stream_callback,
|
|
1335
|
+
sent_files=_sent_files,
|
|
1336
|
+
agent_path=agent_path,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
# 提取输出
|
|
1340
|
+
if tool_result is None:
|
|
1341
|
+
tool_result = {"success": False, "error": "工具返回了空结果"}
|
|
1342
|
+
_output_text = (
|
|
1343
|
+
tool_result.get("output", "")
|
|
1344
|
+
or tool_result.get("message", "")
|
|
1345
|
+
or tool_result.get("stdout", "")
|
|
1346
|
+
or tool_result.get("error", "")
|
|
1347
|
+
)
|
|
1348
|
+
if not _output_text and tool_result.get("data"):
|
|
1349
|
+
try:
|
|
1350
|
+
_output_text = json.dumps(tool_result["data"], ensure_ascii=False, default=str)[:30000]
|
|
1351
|
+
except Exception:
|
|
1352
|
+
_output_text = str(tool_result["data"])[:30000]
|
|
1353
|
+
|
|
1354
|
+
# 发送工具结果事件
|
|
1355
|
+
await self._emit_v2_event(
|
|
1356
|
+
"v2_tool_result",
|
|
1357
|
+
{"tool": {"toolname": _tc_name}, "result": {
|
|
1358
|
+
"success": tool_result.get("success", False),
|
|
1359
|
+
"output": truncate_str(_output_text, 30000),
|
|
1360
|
+
"error": truncate_str(tool_result.get("error", ""), 30000),
|
|
1361
|
+
}},
|
|
1362
|
+
stream_callback,
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
self._add_exec_event("tool_result", {
|
|
1366
|
+
"title": f"工具结果: {_tc_name}",
|
|
1367
|
+
"tool_name": _tc_name,
|
|
1368
|
+
"success": tool_result.get("success", False),
|
|
1369
|
+
"summary": truncate_str(_output_text, 30000),
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
# 添加 tool result 消息
|
|
1373
|
+
messages.append(Message(
|
|
1374
|
+
role="user",
|
|
1375
|
+
content=f"[工具结果: {_tc_name}] {truncate_str(_output_text, 5000)}",
|
|
1376
|
+
))
|
|
1377
|
+
|
|
1378
|
+
# 工具执行完毕 → 继续循环让 LLM 处理结果
|
|
1379
|
+
continue
|
|
1380
|
+
|
|
1381
|
+
# 5) 没有工具但有 reply → 提取纯文本回复
|
|
1382
|
+
if parsed.reply:
|
|
1383
|
+
reply_text = parsed.reply.strip()
|
|
1384
|
+
else:
|
|
1385
|
+
# 兜底:去除所有 XML 标签
|
|
1386
|
+
import re as _re_xml
|
|
1387
|
+
reply_text = _re_xml.sub(r'<[^>]+>', '', raw_content).strip()
|
|
1388
|
+
|
|
1389
|
+
if not reply_text:
|
|
1390
|
+
reply_text = "处理完毕。"
|
|
1391
|
+
|
|
1392
|
+
context.working_memory["final_response"] = reply_text
|
|
1393
|
+
await self._emit_v2_event("v2_reasoning", {"content": truncate_str(reply_text, 3000)}, stream_callback)
|
|
1394
|
+
|
|
1395
|
+
# 保存回复到会话记忆
|
|
1396
|
+
if self.memory:
|
|
1397
|
+
self.memory.add_session(agent_id=_effective_agent_id,
|
|
1398
|
+
session_id=context.session_id,
|
|
1399
|
+
role="assistant",
|
|
1400
|
+
content=reply_text,
|
|
1401
|
+
key="reply",
|
|
1402
|
+
importance=0.5,
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
# 保存 LLM 原始输出
|
|
1406
|
+
if self.memory:
|
|
1407
|
+
self.memory.add_session(agent_id=_effective_agent_id,
|
|
1408
|
+
session_id=context.session_id,
|
|
1409
|
+
role="assistant",
|
|
1410
|
+
content=raw_content,
|
|
1411
|
+
key="llm_output",
|
|
1412
|
+
importance=0.3,
|
|
1413
|
+
)
|
|
1414
|
+
|
|
1415
|
+
break
|
|
1416
|
+
|
|
1417
|
+
# 纯文本回复(非 XML 格式)
|
|
1418
|
+
reply_text = raw_content
|
|
1227
1419
|
logger.info(f"[{task_id}] 无工具调用,任务完成 (reply长度={len(reply_text)})")
|
|
1228
1420
|
|
|
1229
1421
|
if not reply_text:
|
|
@@ -1865,25 +1865,53 @@ class StealthBrowser:
|
|
|
1865
1865
|
"""Firefox+VNC 模式下导航到指定 URL。
|
|
1866
1866
|
|
|
1867
1867
|
Firefox 支持远程打开 URL:firefox <url> 会在已运行的实例中打开新标签页。
|
|
1868
|
+
[v1.47.19] 改用 Popen 非阻塞方式:firefox <url> 在已运行实例中打开新标签页
|
|
1869
|
+
后会阻塞等待窗口关闭,subprocess.run + timeout 会导致超时。
|
|
1870
|
+
改用 Popen 后不等待命令完成,只确认启动即可。
|
|
1868
1871
|
"""
|
|
1869
1872
|
display = os.environ.get("DISPLAY", ":99")
|
|
1870
1873
|
env = {**os.environ, "DISPLAY": display}
|
|
1871
1874
|
if not env.get("G_SLICE"):
|
|
1872
1875
|
env["G_SLICE"] = "always-malloc"
|
|
1876
|
+
if not env.get("GSETTINGS_BACKEND"):
|
|
1877
|
+
env["GSETTINGS_BACKEND"] = "memory"
|
|
1878
|
+
if os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
|
|
1879
|
+
env["DBUS_SESSION_BUS_ADDRESS"] = os.environ["DBUS_SESSION_BUS_ADDRESS"]
|
|
1873
1880
|
|
|
1874
1881
|
try:
|
|
1875
1882
|
firefox_path = shutil.which("firefox") or shutil.which("myagent-browser")
|
|
1876
1883
|
if not firefox_path:
|
|
1877
1884
|
return SkillResult(success=False, error="Firefox 未找到")
|
|
1878
1885
|
|
|
1879
|
-
|
|
1886
|
+
# [v1.47.19] 使用 Popen 非阻塞方式启动
|
|
1887
|
+
# firefox <url> 在已有实例运行时会将 URL 发送给已有实例,
|
|
1888
|
+
# 然后阻塞等待(因为 Firefox 的 -no-remote 行为)。
|
|
1889
|
+
# 用 Popen 启动后不等待完成,只等待进程启动成功。
|
|
1890
|
+
process = subprocess.Popen(
|
|
1880
1891
|
[firefox_path, "--profile", self._firefox_profile_dir, url],
|
|
1881
|
-
|
|
1882
|
-
|
|
1892
|
+
stdout=subprocess.DEVNULL,
|
|
1893
|
+
stderr=subprocess.DEVNULL,
|
|
1894
|
+
env=env,
|
|
1895
|
+
start_new_session=True,
|
|
1883
1896
|
)
|
|
1884
|
-
|
|
1897
|
+
# 等待短时间确认进程没有立即崩溃
|
|
1898
|
+
time.sleep(0.5)
|
|
1899
|
+
if process.poll() is not None:
|
|
1900
|
+
# 进程已退出,可能是错误
|
|
1901
|
+
exit_code = process.returncode
|
|
1902
|
+
# Firefox 在已有实例时,转发URL后以 exit code 0 退出(正常行为)
|
|
1903
|
+
if exit_code == 0:
|
|
1904
|
+
logger.info(f"Firefox 已转发 URL 到已有实例: {url}")
|
|
1905
|
+
else:
|
|
1906
|
+
return SkillResult(
|
|
1907
|
+
success=False,
|
|
1908
|
+
error=f"Firefox 打开 URL 失败 (exit code: {exit_code}): {url}",
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1885
1911
|
if wait > 0:
|
|
1886
1912
|
time.sleep(wait)
|
|
1913
|
+
|
|
1914
|
+
logger.info(f"Firefox 已打开 URL: {url}")
|
|
1887
1915
|
return SkillResult(
|
|
1888
1916
|
success=True,
|
|
1889
1917
|
message=f"Firefox 已打开: {url}",
|
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/web/api_server.py
CHANGED
|
@@ -7901,10 +7901,11 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7901
7901
|
# 4. 检测到裸 JSON(整个回复以 { 开头):进入 action 模式,提取 thought
|
|
7902
7902
|
# 5. 代码块结束后回到文本模式,继续流式推送
|
|
7903
7903
|
_stream_state = {
|
|
7904
|
-
"mode": "text", # "text" | "action_block" | "tasklist_block" | "bare_json"
|
|
7904
|
+
"mode": "text", # "text" | "action_block" | "tasklist_block" | "bare_json" | "output_xml"
|
|
7905
7905
|
"processed_pos": 0, # 已处理到的位置(用于去重 streaming)
|
|
7906
7906
|
"thought_sent": 0, # 已推送的 thought 长度
|
|
7907
7907
|
"action_block_depth": 0, # ``` 嵌套深度
|
|
7908
|
+
"reply_sent": 0, # [v1.47.16] output_xml 模式下已推送的 reply 长度
|
|
7908
7909
|
}
|
|
7909
7910
|
|
|
7910
7911
|
# 需要回退(hold back)的最大字符数,用于检测 ```action 或 ```tasklist 标记
|
|
@@ -7913,12 +7914,30 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7913
7914
|
_MAX_HOLD = 12
|
|
7914
7915
|
|
|
7915
7916
|
async def _text_delta_callback(full_text_so_far: str, delta_text: str):
|
|
7916
|
-
"""智能流式过滤器:文本正常推送,JSON action
|
|
7917
|
+
"""智能流式过滤器:文本正常推送,JSON action 块拦截,<output> XML 拦截"""
|
|
7917
7918
|
st = _stream_state
|
|
7918
7919
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
7919
7920
|
|
|
7920
7921
|
while remaining:
|
|
7921
7922
|
if st["mode"] == "text":
|
|
7923
|
+
# ── [v1.47.16] 检测 <output> XML 标签 → 进入 output_xml 模式 ──
|
|
7924
|
+
output_marker = remaining.find("<output")
|
|
7925
|
+
if output_marker >= 0:
|
|
7926
|
+
# 推送 <output> 之前的文本
|
|
7927
|
+
text_before = remaining[:output_marker]
|
|
7928
|
+
if text_before.strip():
|
|
7929
|
+
await _write_sse({"type": "text_delta", "content": text_before})
|
|
7930
|
+
_all_streamed_text_parts.append(text_before)
|
|
7931
|
+
# 跳过 <output...> 开始标签
|
|
7932
|
+
tag_end = remaining.find(">", output_marker)
|
|
7933
|
+
if tag_end >= 0:
|
|
7934
|
+
st["processed_pos"] += tag_end + 1
|
|
7935
|
+
else:
|
|
7936
|
+
st["processed_pos"] += len(remaining)
|
|
7937
|
+
st["mode"] = "output_xml"
|
|
7938
|
+
remaining = full_text_so_far[st["processed_pos"]:]
|
|
7939
|
+
continue
|
|
7940
|
+
|
|
7922
7941
|
# ── 文本模式:寻找 ```action 或 ```tasklist 标记 ──
|
|
7923
7942
|
action_marker = remaining.find("```action")
|
|
7924
7943
|
tasklist_marker = remaining.find("```tasklist")
|
|
@@ -8037,6 +8056,60 @@ window.addEventListener('beforeunload', function() {{
|
|
|
8037
8056
|
remaining = ""
|
|
8038
8057
|
break
|
|
8039
8058
|
|
|
8059
|
+
elif st["mode"] == "output_xml":
|
|
8060
|
+
# ── [v1.47.16] <output> XML 模式:提取 <reply> 内容流式推送,其余全部拦截 ──
|
|
8061
|
+
# 策略:在 output_xml 模式下,只在检测到 <reply> 内容时推送,其他标签内容全部跳过
|
|
8062
|
+
import re as _re_xml_stream
|
|
8063
|
+
|
|
8064
|
+
# 检查 </output> 闭合标签 → 退出 output_xml 模式
|
|
8065
|
+
close_output = remaining.find("</output>")
|
|
8066
|
+
if close_output >= 0:
|
|
8067
|
+
# 在闭合标签前,检查是否有未推送的 <reply> 内容
|
|
8068
|
+
before_close = full_text_so_far[st["processed_pos"]:st["processed_pos"] + close_output]
|
|
8069
|
+
# 尝试提取 <reply> 内容
|
|
8070
|
+
reply_m = _re_xml_stream.search(r'<reply[^>]*>([\s\S]*?)</reply>', before_close)
|
|
8071
|
+
if reply_m and reply_m.group(1).strip():
|
|
8072
|
+
reply_content = reply_m.group(1).strip()
|
|
8073
|
+
new_part = reply_content[st["reply_sent"]:]
|
|
8074
|
+
if new_part:
|
|
8075
|
+
await _write_sse({"type": "text_delta", "content": new_part})
|
|
8076
|
+
_all_streamed_text_parts.append(new_part)
|
|
8077
|
+
st["reply_sent"] = len(reply_content)
|
|
8078
|
+
# 跳过到 </output> 之后
|
|
8079
|
+
st["processed_pos"] += close_output + len("</output>")
|
|
8080
|
+
st["mode"] = "text"
|
|
8081
|
+
remaining = full_text_so_far[st["processed_pos"]:]
|
|
8082
|
+
continue
|
|
8083
|
+
|
|
8084
|
+
# 尚未闭合:尝试提取已闭合的 <reply>...</reply> 内容并流式推送
|
|
8085
|
+
all_so_far = full_text_so_far[st["processed_pos"]:]
|
|
8086
|
+
reply_m = _re_xml_stream.search(r'<reply[^>]*>([\s\S]*?)</reply>', all_so_far)
|
|
8087
|
+
if reply_m and reply_m.group(1).strip():
|
|
8088
|
+
reply_content = reply_m.group(1).strip()
|
|
8089
|
+
new_part = reply_content[st["reply_sent"]:]
|
|
8090
|
+
if new_part:
|
|
8091
|
+
await _write_sse({"type": "text_delta", "content": new_part})
|
|
8092
|
+
_all_streamed_text_parts.append(new_part)
|
|
8093
|
+
st["reply_sent"] = len(reply_content)
|
|
8094
|
+
|
|
8095
|
+
# 尝试提取未闭合的 <reply> 内容(流式输出中标签可能尚未关闭)
|
|
8096
|
+
elif not reply_m:
|
|
8097
|
+
reply_open_m = _re_xml_stream.search(r'<reply[^>]*>([\s\S]+)$', all_so_far)
|
|
8098
|
+
if reply_open_m and reply_open_m.group(1).strip():
|
|
8099
|
+
partial_reply = reply_open_m.group(1)
|
|
8100
|
+
# 去除尾部可能的不完整标签
|
|
8101
|
+
partial_reply = _re_xml_stream.sub(r'<[^>]*$', '', partial_reply).strip()
|
|
8102
|
+
if partial_reply and len(partial_reply) > st["reply_sent"]:
|
|
8103
|
+
new_part = partial_reply[st["reply_sent"]:]
|
|
8104
|
+
if new_part:
|
|
8105
|
+
await _write_sse({"type": "text_delta", "content": new_part})
|
|
8106
|
+
_all_streamed_text_parts.append(new_part)
|
|
8107
|
+
st["reply_sent"] = len(partial_reply)
|
|
8108
|
+
|
|
8109
|
+
# 等待更多 token
|
|
8110
|
+
remaining = ""
|
|
8111
|
+
break
|
|
8112
|
+
|
|
8040
8113
|
# Stream 结束后的 flush:推送所有 hold 住的文本
|
|
8041
8114
|
async def _flush_remaining_text(full_text: str):
|
|
8042
8115
|
"""流结束后,推送所有剩余的文本(处理 hold back 的部分)"""
|
|
@@ -8050,6 +8123,25 @@ window.addEventListener('beforeunload', function() {{
|
|
|
8050
8123
|
await _write_sse({"type": "text_delta", "content": remaining})
|
|
8051
8124
|
_all_streamed_text_parts.append(remaining)
|
|
8052
8125
|
st["processed_pos"] = len(full_text)
|
|
8126
|
+
elif st["mode"] == "output_xml":
|
|
8127
|
+
# [v1.47.16] output_xml 模式下 flush:尝试提取 <reply> 内容
|
|
8128
|
+
import re as _re_xml_flush
|
|
8129
|
+
reply_m = _re_xml_flush.search(r'<reply[^>]*>([\s\S]*?)(?:</reply>|$)', remaining)
|
|
8130
|
+
if reply_m and reply_m.group(1).strip():
|
|
8131
|
+
reply_content = reply_m.group(1).strip()
|
|
8132
|
+
new_part = reply_content[st["reply_sent"]:]
|
|
8133
|
+
if new_part:
|
|
8134
|
+
await _write_sse({"type": "text_delta", "content": new_part})
|
|
8135
|
+
_all_streamed_text_parts.append(new_part)
|
|
8136
|
+
st["reply_sent"] = len(reply_content)
|
|
8137
|
+
# 检查 </output> 之后是否还有文本
|
|
8138
|
+
close_pos = remaining.find("</output>")
|
|
8139
|
+
if close_pos >= 0:
|
|
8140
|
+
after_output = remaining[close_pos + len("</output>"):].strip()
|
|
8141
|
+
if after_output and st["mode"] == "output_xml":
|
|
8142
|
+
# 不推送(output_xml 模式结束后可能有残余标签文本)
|
|
8143
|
+
pass
|
|
8144
|
+
st["processed_pos"] = len(full_text)
|
|
8053
8145
|
|
|
8054
8146
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
8055
8147
|
# Call LLM with streaming + frequency_penalty to reduce repetition
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2999,12 +2999,12 @@ async function selectSession(id) {
|
|
|
2999
2999
|
return m && (m.role === 'user' || m.role === 'assistant' || m.role === 'tool');
|
|
3000
3000
|
}).map(function(m) {
|
|
3001
3001
|
var content = (m.content != null) ? String(m.content) : '';
|
|
3002
|
-
//
|
|
3003
|
-
// 只有无 key 的旧格式 assistant 消息且内容以 < 开头时才需要剥离 XML
|
|
3004
|
-
// 有 key 的消息(reasoning/reply/tool_call)已经是解析后的纯内容,无需处理
|
|
3002
|
+
// [v1.47.16] 剥离 XML 标签:无 key 的旧格式 + key=reply 但仍含 XML 标签的消息
|
|
3005
3003
|
var mkey = (m.key || '').toLowerCase();
|
|
3006
|
-
if (m.role === 'assistant' &&
|
|
3007
|
-
|
|
3004
|
+
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
3005
|
+
if (!mkey || mkey === 'reply') {
|
|
3006
|
+
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
3007
|
+
}
|
|
3008
3008
|
}
|
|
3009
3009
|
var mapped = {
|
|
3010
3010
|
role: m.role || 'assistant',
|
|
@@ -3110,8 +3110,10 @@ async function loadMoreMessages() {
|
|
|
3110
3110
|
}).map(function(m) {
|
|
3111
3111
|
var content = (m.content != null) ? String(m.content) : '';
|
|
3112
3112
|
var mkey = (m.key || '').toLowerCase();
|
|
3113
|
-
if (m.role === 'assistant' &&
|
|
3114
|
-
|
|
3113
|
+
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
3114
|
+
if (!mkey || mkey === 'reply' || (mkey !== 'tool_call' && mkey !== 'reasoning')) {
|
|
3115
|
+
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
3116
|
+
}
|
|
3115
3117
|
}
|
|
3116
3118
|
var mapped = {
|
|
3117
3119
|
role: m.role || 'assistant',
|
|
@@ -398,10 +398,12 @@ async function pollChatHistory() {
|
|
|
398
398
|
}).map(function(m) {
|
|
399
399
|
var content = (m.content != null) ? String(m.content) : '';
|
|
400
400
|
var mkey = (m.key || '').toLowerCase();
|
|
401
|
-
//
|
|
402
|
-
// 有 key 的消息(reasoning/reply/tool_call
|
|
403
|
-
if (m.role === 'assistant' &&
|
|
404
|
-
|
|
401
|
+
// [v1.47.16] 剥离 XML 标签:无 key 的旧格式 + key=reply 但仍含 XML 标签的消息
|
|
402
|
+
// 有 key 的消息(reasoning/reply/tool_call)一般已是纯内容,但部分模型仍会输出 XML
|
|
403
|
+
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
404
|
+
if (!mkey || mkey === 'reply') {
|
|
405
|
+
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
406
|
+
}
|
|
405
407
|
}
|
|
406
408
|
var mapped = {
|
|
407
409
|
role: m.role || 'assistant',
|
|
@@ -473,9 +475,11 @@ async function forceRefreshHistory() {
|
|
|
473
475
|
}).map(function(m) {
|
|
474
476
|
var content = (m.content != null) ? String(m.content) : '';
|
|
475
477
|
var mkey = (m.key || '').toLowerCase();
|
|
476
|
-
//
|
|
477
|
-
if (m.role === 'assistant' &&
|
|
478
|
-
|
|
478
|
+
// [v1.47.16] 剥离 XML 标签:无 key 的旧格式 + key=reply 但仍含 XML 标签的消息
|
|
479
|
+
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
480
|
+
if (!mkey || mkey === 'reply') {
|
|
481
|
+
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
482
|
+
}
|
|
479
483
|
}
|
|
480
484
|
var mapped = {
|
|
481
485
|
role: m.role || 'assistant',
|
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
|