myagent-ai 1.47.19 → 1.47.21
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 +37 -259
- package/aiskills/browser_stealth.py +201 -25
- package/aiskills/chromedev_mcp.py +20 -0
- package/package.json +1 -1
- package/web/api_server.py +3 -95
- package/web/ui/chat/chat_main.js +4 -7
- package/web/ui/chat/flow_engine.js +8 -34
- package/worklog.md +27 -0
- package/core/output_parser.py +0 -730
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -7901,11 +7901,10 @@ 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"
|
|
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 长度
|
|
7909
7908
|
}
|
|
7910
7909
|
|
|
7911
7910
|
# 需要回退(hold back)的最大字符数,用于检测 ```action 或 ```tasklist 标记
|
|
@@ -7914,30 +7913,12 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7914
7913
|
_MAX_HOLD = 12
|
|
7915
7914
|
|
|
7916
7915
|
async def _text_delta_callback(full_text_so_far: str, delta_text: str):
|
|
7917
|
-
"""智能流式过滤器:文本正常推送,JSON action
|
|
7916
|
+
"""[v1.47.21] 智能流式过滤器:文本正常推送,JSON action 块拦截"""
|
|
7918
7917
|
st = _stream_state
|
|
7919
7918
|
remaining = full_text_so_far[st["processed_pos"]:]
|
|
7920
7919
|
|
|
7921
7920
|
while remaining:
|
|
7922
7921
|
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
|
-
|
|
7941
7922
|
# ── 文本模式:寻找 ```action 或 ```tasklist 标记 ──
|
|
7942
7923
|
action_marker = remaining.find("```action")
|
|
7943
7924
|
tasklist_marker = remaining.find("```tasklist")
|
|
@@ -8056,63 +8037,9 @@ window.addEventListener('beforeunload', function() {{
|
|
|
8056
8037
|
remaining = ""
|
|
8057
8038
|
break
|
|
8058
8039
|
|
|
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
|
-
|
|
8113
8040
|
# Stream 结束后的 flush:推送所有 hold 住的文本
|
|
8114
8041
|
async def _flush_remaining_text(full_text: str):
|
|
8115
|
-
"""流结束后,推送所有剩余的文本(处理 hold back 的部分)"""
|
|
8042
|
+
"""[v1.47.21] 流结束后,推送所有剩余的文本(处理 hold back 的部分)"""
|
|
8116
8043
|
st = _stream_state
|
|
8117
8044
|
remaining = full_text[st["processed_pos"]:]
|
|
8118
8045
|
if remaining.strip() and st["mode"] == "text":
|
|
@@ -8123,25 +8050,6 @@ window.addEventListener('beforeunload', function() {{
|
|
|
8123
8050
|
await _write_sse({"type": "text_delta", "content": remaining})
|
|
8124
8051
|
_all_streamed_text_parts.append(remaining)
|
|
8125
8052
|
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)
|
|
8145
8053
|
|
|
8146
8054
|
# Call LLM with streaming — tokens are filtered through _text_delta_callback
|
|
8147
8055
|
# Call LLM with streaming + frequency_penalty to reduce repetition
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -2999,12 +2999,10 @@ 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
|
-
// [v1.47.
|
|
3002
|
+
// [v1.47.21] 清理意外输出的 XML 标签(完全依赖 tool_calling)
|
|
3003
3003
|
var mkey = (m.key || '').toLowerCase();
|
|
3004
3004
|
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
3005
|
-
|
|
3006
|
-
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
3007
|
-
}
|
|
3005
|
+
content = content.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
3008
3006
|
}
|
|
3009
3007
|
var mapped = {
|
|
3010
3008
|
role: m.role || 'assistant',
|
|
@@ -3110,10 +3108,9 @@ async function loadMoreMessages() {
|
|
|
3110
3108
|
}).map(function(m) {
|
|
3111
3109
|
var content = (m.content != null) ? String(m.content) : '';
|
|
3112
3110
|
var mkey = (m.key || '').toLowerCase();
|
|
3111
|
+
// [v1.47.21] 清理意外输出的 XML 标签
|
|
3113
3112
|
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
3114
|
-
|
|
3115
|
-
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
3116
|
-
}
|
|
3113
|
+
content = content.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
3117
3114
|
}
|
|
3118
3115
|
var mapped = {
|
|
3119
3116
|
role: m.role || 'assistant',
|
|
@@ -398,12 +398,9 @@ 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
|
-
// [v1.47.
|
|
402
|
-
// 有 key 的消息(reasoning/reply/tool_call)一般已是纯内容,但部分模型仍会输出 XML
|
|
401
|
+
// [v1.47.21] 清理意外输出的 XML 标签(完全依赖 tool_calling,不再解析 XML)
|
|
403
402
|
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
404
|
-
|
|
405
|
-
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
406
|
-
}
|
|
403
|
+
content = content.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
407
404
|
}
|
|
408
405
|
var mapped = {
|
|
409
406
|
role: m.role || 'assistant',
|
|
@@ -475,11 +472,9 @@ async function forceRefreshHistory() {
|
|
|
475
472
|
}).map(function(m) {
|
|
476
473
|
var content = (m.content != null) ? String(m.content) : '';
|
|
477
474
|
var mkey = (m.key || '').toLowerCase();
|
|
478
|
-
// [v1.47.
|
|
475
|
+
// [v1.47.21] 清理意外输出的 XML 标签
|
|
479
476
|
if (m.role === 'assistant' && content && content.trim().startsWith('<')) {
|
|
480
|
-
|
|
481
|
-
content = (typeof _stripXmlTags === 'function') ? _stripXmlTags(content) : content;
|
|
482
|
-
}
|
|
477
|
+
content = content.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
483
478
|
}
|
|
484
479
|
var mapped = {
|
|
485
480
|
role: m.role || 'assistant',
|
|
@@ -1120,34 +1115,13 @@ function _showFinishNotification(text) {
|
|
|
1120
1115
|
}
|
|
1121
1116
|
|
|
1122
1117
|
/**
|
|
1123
|
-
* Strip XML tags from text
|
|
1124
|
-
*
|
|
1118
|
+
* [v1.47.21] Strip XML tags from text — simple regex cleanup for accidental XML output.
|
|
1119
|
+
* No longer parses <output>/<reply>/<toolstocal> — fully relies on native tool_calling.
|
|
1125
1120
|
*/
|
|
1126
1121
|
function _stripXmlTags(xml) {
|
|
1127
1122
|
if (!xml) return '';
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
// 当只有开始标签没有闭合标签时,将开始标签到文本末尾的内容完全移除
|
|
1131
|
-
text = text.replace(/<task_plan[^>]*>[\s\S]*?<\/task_plan>/g, ''); // 已闭合的完整 task_plan
|
|
1132
|
-
text = text.replace(/<task_plan[^>]*>[\s\S]*$/g, ''); // 未闭合的 task_plan(流式中标签已打开但未关闭)
|
|
1133
|
-
// [v1.37] 优先提取 <reply> 标签内容,不再兜底 <response>
|
|
1134
|
-
var replyMatch = text.match(/<reply[^>]*>([\s\S]*?)<\/reply>/i);
|
|
1135
|
-
if (replyMatch && replyMatch[1] && replyMatch[1].trim()) {
|
|
1136
|
-
return replyMatch[1].trim();
|
|
1137
|
-
}
|
|
1138
|
-
// [v1.37] 移除 <response> 包裹(不再作为兜底提取,直接剥离标签)
|
|
1139
|
-
text = text.replace(/<response[^>]*>|<\/response>/gi, '');
|
|
1140
|
-
// 兜底:去除所有XML标签
|
|
1141
|
-
return text
|
|
1142
|
-
.replace(/<[^>]+>/g, ' ') // Replace tags with space
|
|
1143
|
-
.replace(/</g, '<')
|
|
1144
|
-
.replace(/>/g, '>')
|
|
1145
|
-
.replace(/&/g, '&')
|
|
1146
|
-
.replace(/"/g, '"')
|
|
1147
|
-
.replace(/'/g, "'")
|
|
1148
|
-
.replace(/'/g, "'")
|
|
1149
|
-
.replace(/\s{3,}/g, ' ') // Collapse 3+ whitespace to single space
|
|
1150
|
-
.trim();
|
|
1123
|
+
return xml.replace(/<[^>]+>/g, ' ').replace(/</g, '<').replace(/>/g, '>')
|
|
1124
|
+
.replace(/&/g, '&').replace(/\s{2,}/g, ' ').trim();
|
|
1151
1125
|
}
|
|
1152
1126
|
|
|
1153
1127
|
// ══════════════════════════════════════════════════════
|
package/worklog.md
CHANGED
|
@@ -117,3 +117,30 @@ Stage Summary:
|
|
|
117
117
|
- Streaming filter extracts only `<reply>` content for real-time display
|
|
118
118
|
- Frontend strips XML from both keyless and key="reply" assistant messages
|
|
119
119
|
- VNC mode Firefox support fully functional
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
Task ID: 2
|
|
123
|
+
Agent: Main
|
|
124
|
+
Task: Fix Firefox+VNC browser_stealth: content/close/evaluate/wait_for + browser_open/web_control VNC fallback
|
|
125
|
+
|
|
126
|
+
Work Log:
|
|
127
|
+
- Analyzed logs: stealth_browser_navigate now works (Popen non-blocking), but stealth_browser_content returns "不支持" and agent falls back to web_control/browser_open which also fail in VNC mode
|
|
128
|
+
- Added `_firefox_read_sessionstore()` method: reads Firefox's recovery.jsonlz4 (mozLz4 format) to get current tab URL/title
|
|
129
|
+
- Added `_firefox_get_content()` method: screenshot + sessionstore → returns screenshot path, URL, title, tabs list
|
|
130
|
+
- Changed `get_content()` Firefox mode: calls `_firefox_get_content()` instead of returning error
|
|
131
|
+
- Changed `get_html()` Firefox mode: calls `_firefox_get_content()` instead of returning error
|
|
132
|
+
- Changed `close()` Firefox mode: VNC mode only clears internal state, does NOT kill Firefox (managed by vnc_manager)
|
|
133
|
+
- Changed `StealthBrowserCloseSkill.execute()`: VNC mode returns "会话已释放" instead of "浏览器已关闭"
|
|
134
|
+
- Changed `evaluate()` Firefox mode: better error message suggesting stealth_browser alternatives
|
|
135
|
+
- Changed `wait_for()` Firefox mode: sleep + sessionstore read instead of returning error
|
|
136
|
+
- Changed `browser_open` in chromedev_mcp.py: VNC mode without Chromium → returns error suggesting stealth_browser
|
|
137
|
+
- Added VNC mode hint injection in main_agent.py system prompt: tells agent to use stealth_browser_* tools in VNC mode
|
|
138
|
+
- Published v1.47.20 to npm
|
|
139
|
+
|
|
140
|
+
Stage Summary:
|
|
141
|
+
- Firefox+VNC mode: stealth_browser_content now returns screenshot + tab info (URL/title/tabs)
|
|
142
|
+
- Firefox+VNC mode: close() no longer kills VNC browser process
|
|
143
|
+
- Firefox+VNC mode: wait_for() works (sleep + sessionstore), evaluate() has actionable error message
|
|
144
|
+
- browser_open: VNC mode without Chromium → clear error suggesting stealth_browser
|
|
145
|
+
- main_agent: VNC mode system prompt tells agent to prefer stealth_browser over browser_open/web_control
|
|
146
|
+
- All syntax checks passed
|