myagent-ai 1.26.6 → 1.26.8
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 +35 -26
- package/main.py +3 -1
- package/package.json +1 -1
- package/skills/registry.py +2 -1
- package/web/api_server.py +27 -7
- package/web/ui/admin/admin-sessions.js +11 -1
- package/web/ui/chat/chat_main.js +22 -6
- package/web/ui/chat/flow_engine.js +30 -8
- package/web/ui/chat/groupchat.js +63 -0
package/agents/main_agent.py
CHANGED
|
@@ -52,7 +52,7 @@ class MainAgent(BaseAgent):
|
|
|
52
52
|
<mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
|
|
53
53
|
<usersays_correct>通过修正识别错误、调整标点,结合上下文,将用户语音转写文本"usersays"修正为更准确的文本,但尽量少改动。如"usersays"为空,则此处为空。</usersays_correct>
|
|
54
54
|
<response><reply>展示给用户的文本,格式上尽量使用md格式,直观形象展示,甚至可以包括超链接、表格等。内容上,针对用户问题,直接回应问题;针对任务,开始的时候,告诉用户,为完成任务,准备如何开展工作;执行过程中,根据工具调用结果,简单展示任务进展;任务完成后的详细最终总结。注意:这是给用户展示信息的最重要标签,尽量不要跟上次回复重复,执行过程展示内容尽量简洁,执行总结可以丰富一点。</reply><toolstocal>
|
|
55
|
-
<tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的
|
|
55
|
+
<tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的XML格式参数,每个参数用独立子标签表示,如: <command>要执行的命令</command> 或 <query>搜索关键词</query><num>5</num></parms><timeout>最多给它执行多久(秒),工具调用超过这个时限会立即回调大语言模型,方便调整工具使用</timeout></tool>
|
|
56
56
|
</toolstocal>
|
|
57
57
|
</response>
|
|
58
58
|
<task_plan>若"context"包含非空"task_plan",则更新它:若任务条数已超8,则精简为3条,若主题发生明显变化,重新设计任务列表。若"context"包含空"task_plan",则先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过2步,则此处输出为空,不创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过2步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记,排序按已完成在前。</task_plan>
|
|
@@ -78,7 +78,7 @@ class MainAgent(BaseAgent):
|
|
|
78
78
|
4. 文件自动发送与下载链接: docx-create、xlsx-create、ppt-create、pdf-create 等命令执行成功后会**自动**将文件发送给用户(无需手动调用 send-file)。系统会返回包含下载链接的文件卡片,LLM 不需要自行拼接文件链接。仅在需要发送其他独立文件(如已存在的文件)时,才使用 myagent-ai send-file <文件路径> [描述]。向用户展示下载链接时,务必使用 Markdown 超链接格式: [文件名](完整URL)。
|
|
79
79
|
|
|
80
80
|
**command**(执行命令行,所有操作都通过它完成):
|
|
81
|
-
<tool><toolname>command</toolname><parms
|
|
81
|
+
<tool><toolname>command</toolname><parms><command>要执行的命令</command></parms><timeout>超时秒数</timeout></tool>
|
|
82
82
|
|
|
83
83
|
【重要】命令执行规则:
|
|
84
84
|
- Shell 原生命令(ls/cat/grep/ps/df/uname/python3/pip/npm/git/curl/wget 等)直接执行,不要加任何前缀
|
|
@@ -123,30 +123,30 @@ GUI桌面 (仅Windows/macOS):
|
|
|
123
123
|
- 播放音频/视频: myagent-ai playaudio/playvideo --url URL [--title 标题]
|
|
124
124
|
|
|
125
125
|
调用示例:
|
|
126
|
-
<tool><toolname>command</toolname><parms>
|
|
127
|
-
<tool><toolname>command</toolname><parms>
|
|
128
|
-
<tool><toolname>command</toolname><parms>
|
|
129
|
-
<tool><toolname>command</toolname><parms>
|
|
126
|
+
<tool><toolname>command</toolname><parms><command>ls -la /tmp && df -h && python3 --version</command></parms><timeout>10</timeout></tool>
|
|
127
|
+
<tool><toolname>command</toolname><parms><command>myagent-ai search 人工智能最新进展</command></parms><timeout>15</timeout></tool>
|
|
128
|
+
<tool><toolname>command</toolname><parms><command>myagent-ai docx-create -c '{"title": "报告", "sections": [{"heading": "摘要", "body": "内容"}]}' -t 周报</command></parms><timeout>30</timeout></tool>
|
|
129
|
+
<tool><toolname>command</toolname><parms><command>cat /etc/os-release && uname -a && free -h</command></parms><timeout>10</timeout></tool>
|
|
130
130
|
|
|
131
131
|
**file_send**(向用户发送文件,文件会以卡片形式显示在聊天中):
|
|
132
|
-
<tool><toolname>file_send</toolname><parms
|
|
132
|
+
<tool><toolname>file_send</toolname><parms><file_path>文件的绝对路径</file_path><description>文件描述(可选)</description></parms><timeout>30</timeout></tool>
|
|
133
133
|
- 当你需要把生成的文件(PDF、Excel、图片、脚本等)发送给用户时,直接使用此工具
|
|
134
134
|
- 当你需要发送一个已存在的文件时,直接使用此工具
|
|
135
135
|
- 不要把文件路径当成文本展示给用户,而是用 file_send 工具发送文件卡片
|
|
136
136
|
|
|
137
137
|
**web_control**(网页控制器,在聊天中打开可操作的浏览器面板):
|
|
138
|
-
<tool><toolname>web_control</toolname><parms>
|
|
139
|
-
- 打开:
|
|
140
|
-
- 导航:
|
|
141
|
-
- 获取内容:
|
|
142
|
-
- 点击:
|
|
143
|
-
- 填写:
|
|
144
|
-
- 滚动:
|
|
145
|
-
- 执行JS:
|
|
146
|
-
- 截图:
|
|
147
|
-
- 等待:
|
|
148
|
-
- Cookie: {"
|
|
149
|
-
- 关闭:
|
|
138
|
+
<tool><toolname>web_control</toolname><parms><action>open</action><url>https://example.com</url></parms><timeout>30</timeout></tool>
|
|
139
|
+
- 打开: <parms><action>open</action><url>URL</url></parms>
|
|
140
|
+
- 导航: <parms><action>navigate</action><url>URL</url><session_id>xxx</session_id></parms>
|
|
141
|
+
- 获取内容: <parms><action>get_content</action><what>text|html|url|title|links|images|forms|inputs</what><session_id>xxx</session_id></parms>
|
|
142
|
+
- 点击: <parms><action>click</action><selector>CSS选择器</selector><session_id>xxx</session_id></parms>
|
|
143
|
+
- 填写: <parms><action>fill</action><selector>CSS选择器</selector><value>内容</value><session_id>xxx</session_id></parms>
|
|
144
|
+
- 滚动: <parms><action>scroll</action><direction>up|down|top|bottom</direction><distance>300</distance><session_id>xxx</session_id></parms>
|
|
145
|
+
- 执行JS: <parms><action>evaluate</action><script>JS代码</script><session_id>xxx</session_id></parms>
|
|
146
|
+
- 截图: <parms><action>screenshot</action><session_id>xxx</session_id></parms>
|
|
147
|
+
- 等待: <parms><action>wait</action><time>1000</time></parms> 或 <parms><action>wait</action><selector>.result</selector><timeout>10</timeout></parms>
|
|
148
|
+
- Cookie: <parms><action>set_cookies</action><cookies>[{"name":"key","value":"val"}]</cookies><session_id>xxx</session_id></parms> 或 <parms><action>get_cookies</action><session_id>xxx</session_id></parms>
|
|
149
|
+
- 关闭: <parms><action>close</action><session_id>xxx</session_id></parms>
|
|
150
150
|
|
|
151
151
|
专业技能指令: 系统内置了丰富的专业技能指南(PDF/DOCX/XLSX/PPT 生成、图表绘制、前端开发等),通过 <get_knowledge> 请求相关技能指令。
|
|
152
152
|
"""
|
|
@@ -206,7 +206,7 @@ GUI桌面 (仅Windows/macOS):
|
|
|
206
206
|
self._execution_events = []
|
|
207
207
|
self._exec_event_counter = 0
|
|
208
208
|
|
|
209
|
-
async def process(self, context: AgentContext) -> AgentContext:
|
|
209
|
+
async def process(self, context: AgentContext, stream_callback=None) -> AgentContext:
|
|
210
210
|
"""
|
|
211
211
|
主处理循环。
|
|
212
212
|
|
|
@@ -250,6 +250,7 @@ GUI桌面 (仅Windows/macOS):
|
|
|
250
250
|
agent_name=_injected_name or self.name,
|
|
251
251
|
agent_description=_injected_desc or self.description,
|
|
252
252
|
agent_override_prompt=_override_prompt,
|
|
253
|
+
stream_callback=stream_callback,
|
|
253
254
|
agent_path=getattr(self, '_agent_override_path', None),
|
|
254
255
|
)
|
|
255
256
|
finally:
|
|
@@ -1650,16 +1651,24 @@ GUI桌面 (仅Windows/macOS):
|
|
|
1650
1651
|
stream_callback: Optional[Callable] = None,
|
|
1651
1652
|
sent_files: Optional[List[Dict[str, Any]]] = None,
|
|
1652
1653
|
) -> Dict[str, Any]:
|
|
1653
|
-
"""[v1.22.0] V2 工具执行 — 统一分发到 ToolDispatcher
|
|
1654
|
+
"""[v1.22.0] V2 工具执行 — 统一分发到 ToolDispatcher
|
|
1655
|
+
[v1.27.0] parms 使用 XML 子标签格式:
|
|
1656
|
+
<command>ls -la</command> → {"command": "ls -la"}
|
|
1657
|
+
<file_path>/tmp/a.txt</file_path><description>描述</description> → {"file_path": "/tmp/a.txt", "description": "描述"}
|
|
1658
|
+
"""
|
|
1654
1659
|
try:
|
|
1655
|
-
import
|
|
1660
|
+
import re as _re
|
|
1656
1661
|
import html as _html
|
|
1657
1662
|
try:
|
|
1658
|
-
# [v1.23.38] 防御性修复: 解码 HTML 实体 (& → & 等)
|
|
1659
|
-
# LLM 可能在 XML 输出中使用 & 代替 &,导致 bash 命令失败
|
|
1660
1663
|
_clean_parms = _html.unescape(parms_str) if parms_str else ""
|
|
1661
|
-
|
|
1662
|
-
|
|
1664
|
+
# XML 子标签解析: <key>value</key> → {"key": "value"}
|
|
1665
|
+
params = {}
|
|
1666
|
+
for _tag_name, _tag_value in _re.findall(
|
|
1667
|
+
r"<([a-zA-Z_][a-zA-Z0-9_]*)\s*>([\s\S]*?)</\1\s*>",
|
|
1668
|
+
_clean_parms,
|
|
1669
|
+
):
|
|
1670
|
+
params[_tag_name] = _tag_value.strip()
|
|
1671
|
+
except Exception:
|
|
1663
1672
|
params = {"raw_input": parms_str}
|
|
1664
1673
|
|
|
1665
1674
|
if self.dispatcher:
|
package/main.py
CHANGED
|
@@ -488,6 +488,7 @@ class MyAgentApp:
|
|
|
488
488
|
self,
|
|
489
489
|
user_message: str,
|
|
490
490
|
session_id: str = "",
|
|
491
|
+
stream_callback=None,
|
|
491
492
|
) -> str:
|
|
492
493
|
"""
|
|
493
494
|
处理用户消息并返回回复。
|
|
@@ -495,6 +496,7 @@ class MyAgentApp:
|
|
|
495
496
|
Args:
|
|
496
497
|
user_message: 用户消息
|
|
497
498
|
session_id: 会话 ID
|
|
499
|
+
stream_callback: SSE 流式回调(群聊等场景需要,用于推送 v2_file 等事件)
|
|
498
500
|
|
|
499
501
|
Returns:
|
|
500
502
|
助手回复文本
|
|
@@ -511,7 +513,7 @@ class MyAgentApp:
|
|
|
511
513
|
)
|
|
512
514
|
|
|
513
515
|
try:
|
|
514
|
-
result_context = await self.main_agent.process(context)
|
|
516
|
+
result_context = await self.main_agent.process(context, stream_callback=stream_callback)
|
|
515
517
|
response = result_context.working_memory.get(
|
|
516
518
|
"final_response", "⚠️ 未能生成回复"
|
|
517
519
|
)
|
package/package.json
CHANGED
package/skills/registry.py
CHANGED
|
@@ -116,7 +116,8 @@ INTERNAL_SERVICES: List[Dict[str, Any]] = [
|
|
|
116
116
|
# CLI 子命令元数据 — 通过 command 工具间接调用
|
|
117
117
|
# ==============================================================================
|
|
118
118
|
# [v1.23.0] 这些命令由 scripts/cli.py 实现,LLM 通过 command 工具调用:
|
|
119
|
-
# <toolname>command</toolname><parms>
|
|
119
|
+
# <toolname>command</toolname><parms><command>myagent-ai <cmd> [args...]</command></parms>
|
|
120
|
+
# [v1.27.0] parms 从 JSON 改为 XML 子标签格式
|
|
120
121
|
|
|
121
122
|
CLI_COMMANDS: List[Dict[str, Any]] = [
|
|
122
123
|
# ── 感知 ──
|
package/web/api_server.py
CHANGED
|
@@ -5496,22 +5496,22 @@ window.addEventListener('beforeunload', function() {{
|
|
|
5496
5496
|
|
|
5497
5497
|
async def _try_model_chain(self, model_chain: list[dict], message: str, session_id: str,
|
|
5498
5498
|
agent_path: str = None, agent_system_prompt: str = None,
|
|
5499
|
-
chat_mode: str = "") -> str:
|
|
5499
|
+
chat_mode: str = "", stream_callback=None) -> str:
|
|
5500
5500
|
"""依次尝试模型链中的模型,直到成功或全部失败
|
|
5501
|
-
|
|
5501
|
+
|
|
5502
5502
|
使用 asyncio.Lock 保护共享的 self.core.llm,防止并发请求互相干扰。
|
|
5503
5503
|
"""
|
|
5504
5504
|
if not model_chain:
|
|
5505
|
-
return await self.core.process_message(message, session_id)
|
|
5505
|
+
return await self.core.process_message(message, session_id, stream_callback=stream_callback)
|
|
5506
5506
|
|
|
5507
5507
|
async with self._model_chain_lock:
|
|
5508
5508
|
return await self._try_model_chain_inner(model_chain, message, session_id,
|
|
5509
5509
|
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
5510
|
-
chat_mode=chat_mode)
|
|
5510
|
+
chat_mode=chat_mode, stream_callback=stream_callback)
|
|
5511
5511
|
|
|
5512
5512
|
async def _try_model_chain_inner(self, model_chain: list[dict], message: str, session_id: str,
|
|
5513
5513
|
agent_path: str = None, agent_system_prompt: str = None,
|
|
5514
|
-
chat_mode: str = "") -> str:
|
|
5514
|
+
chat_mode: str = "", stream_callback=None) -> str:
|
|
5515
5515
|
"""_try_model_chain 的实际执行体(已在 _model_chain_lock 保护下)"""
|
|
5516
5516
|
llm = self.core.llm
|
|
5517
5517
|
last_error = ""
|
|
@@ -5598,7 +5598,7 @@ window.addEventListener('beforeunload', function() {{
|
|
|
5598
5598
|
self.core.main_agent.context_builder.agent_knowledge_dir = str(agent_kb_dir)
|
|
5599
5599
|
|
|
5600
5600
|
try:
|
|
5601
|
-
response = await self.core.process_message(message, session_id)
|
|
5601
|
+
response = await self.core.process_message(message, session_id, stream_callback=stream_callback)
|
|
5602
5602
|
finally:
|
|
5603
5603
|
if self.core.main_agent:
|
|
5604
5604
|
self.core.main_agent._agent_override_prompt = None
|
|
@@ -8324,14 +8324,34 @@ window.addEventListener('beforeunload', function() {{
|
|
|
8324
8324
|
|
|
8325
8325
|
agent_content = content
|
|
8326
8326
|
|
|
8327
|
+
# [v1.26.6] 创建群聊 stream_callback,让 file_send 等工具的 v2_file 事件
|
|
8328
|
+
# 能推送到前端。群聊中 v2_file 需要携带 agent_path 标识发送者。
|
|
8329
|
+
_group_sent_files = []
|
|
8330
|
+
async def _group_stream_callback(event):
|
|
8331
|
+
evt_type = event.get("type", "")
|
|
8332
|
+
if evt_type == "v2_file" and event.get("data"):
|
|
8333
|
+
file_data = event["data"]
|
|
8334
|
+
# 转发为群聊格式的 v2_file 事件(附带 agent_path)
|
|
8335
|
+
await safe_write({
|
|
8336
|
+
"type": "v2_file",
|
|
8337
|
+
"data": file_data,
|
|
8338
|
+
"agent_path": agent_path,
|
|
8339
|
+
"agent_name": display_name,
|
|
8340
|
+
"agent_emoji": avatar,
|
|
8341
|
+
})
|
|
8342
|
+
# 追加到 sent_files 用于持久化
|
|
8343
|
+
if isinstance(file_data, dict) and file_data.get("file_id"):
|
|
8344
|
+
_group_sent_files.append(file_data)
|
|
8345
|
+
|
|
8327
8346
|
# 调用 LLM 获取回复
|
|
8328
8347
|
if model_chain and self.core.llm:
|
|
8329
8348
|
response_text = await self._try_model_chain_inner(
|
|
8330
8349
|
model_chain, agent_content, session_id,
|
|
8331
8350
|
agent_path=agent_path, agent_system_prompt=agent_system_prompt,
|
|
8351
|
+
stream_callback=_group_stream_callback,
|
|
8332
8352
|
)
|
|
8333
8353
|
else:
|
|
8334
|
-
response_text = await self.core.process_message(agent_content, session_id)
|
|
8354
|
+
response_text = await self.core.process_message(agent_content, session_id, stream_callback=_group_stream_callback)
|
|
8335
8355
|
|
|
8336
8356
|
# 流式输出该 agent 的回复文本(逐块发送)
|
|
8337
8357
|
await self._stream_text_chunked(response_text, safe_write, chunk_size=6, delay=0.01)
|
|
@@ -54,7 +54,17 @@ async function _loadSessionMessages(){
|
|
|
54
54
|
const label=isResult?'工具执行结果':(isCall?'工具调用':'工具过程');
|
|
55
55
|
const isOk=isResult&&!((m.content||'').includes('失败'));
|
|
56
56
|
const badge=isResult?`<span class="badge ${isOk?'badge-green':'badge-red'}" style="margin-left:6px">${isOk?'成功':'失败'}</span>`:'';
|
|
57
|
-
|
|
57
|
+
// [v1.27.0] 工具调用: 格式化参数行显示(提取"参数:"行并用统一格式化)
|
|
58
|
+
let tc=(m.content||'');
|
|
59
|
+
if(isCall && tc && typeof window.formatToolParamsPreview==='function'){
|
|
60
|
+
const lines=tc.split('\n');
|
|
61
|
+
const formatted=lines.map(l=>{
|
|
62
|
+
const pm=l.match(/^参数:\s*([\s\S]*)/);
|
|
63
|
+
if(pm){return '参数: '+window.formatToolParamsPreview(pm[1].trim(),500);}
|
|
64
|
+
return l;
|
|
65
|
+
}).join('\n');
|
|
66
|
+
tc=formatted;
|
|
67
|
+
}
|
|
58
68
|
const tcTrunc=tc.length>800;
|
|
59
69
|
html+=`<details style="margin:4px 0;border:1px solid var(--border);border-radius:var(--radius);overflow:hidden" ${tcTrunc?'':'open'}><summary style="padding:8px 12px;cursor:pointer;font-size:13px;color:var(--text2);background:var(--surface2)">${icon} ${label}${badge}</summary><div style="padding:8px 12px;background:var(--surface);font-size:12px;white-space:pre-wrap;word-break:break-all;color:var(--text2)">${escHtml(tcTrunc?tc.slice(0,800)+'... (共'+tc.length+'字符)':'')}</div></details>`;
|
|
60
70
|
} else {
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -3051,6 +3051,7 @@ async function clearCurrentChat() {
|
|
|
3051
3051
|
// 推理思考 → role="assistant", key="reasoning"
|
|
3052
3052
|
|
|
3053
3053
|
// 解析 tool_call 消息内容,兼容新旧格式
|
|
3054
|
+
// [v1.27.0] params 现在支持 XML 子标签和 JSON 两种格式
|
|
3054
3055
|
function parseToolCallContent(content) {
|
|
3055
3056
|
if (!content) return { title: '调用工具', toolName: '', params: '' };
|
|
3056
3057
|
var lines = content.split('\n');
|
|
@@ -3066,7 +3067,12 @@ function parseToolCallContent(content) {
|
|
|
3066
3067
|
var tcIdx = lines.findIndex(function(l) { return l.trim().match(/^调用工具:/); });
|
|
3067
3068
|
var title = tcIdx > 0 ? lines.slice(0, tcIdx).join('\n').trim() : '';
|
|
3068
3069
|
if (!title) title = toolName ? ('调用工具: ' + toolName) : '调用工具';
|
|
3069
|
-
|
|
3070
|
+
// [v1.27.0] 使用统一格式化函数生成显示用 params
|
|
3071
|
+
var displayParams = params;
|
|
3072
|
+
if (typeof window.formatToolParamsPreview === 'function' && params) {
|
|
3073
|
+
displayParams = window.formatToolParamsPreview(params, 300);
|
|
3074
|
+
}
|
|
3075
|
+
return { title: title, toolName: toolName, params: params, displayParams: displayParams };
|
|
3070
3076
|
}
|
|
3071
3077
|
|
|
3072
3078
|
// 解析 tool_result 消息: "[tool_name] 成功/失败\n{output}"
|
|
@@ -3094,7 +3100,7 @@ function groupHistoryMessages(messages) {
|
|
|
3094
3100
|
if (key === 'tool_call') {
|
|
3095
3101
|
var tc = m._parsedToolCall || parseToolCallContent(m.content);
|
|
3096
3102
|
m._parsedToolCall = tc;
|
|
3097
|
-
return { type: 'exec', data: { id: evtId, type: 'tool_call', title: tc.title, tool_name: tc.toolName, params: tc.params || undefined, status: 'done' } };
|
|
3103
|
+
return { type: 'exec', data: { id: evtId, type: 'tool_call', title: tc.title, tool_name: tc.toolName, params: tc.displayParams || tc.params || undefined, status: 'done' } };
|
|
3098
3104
|
}
|
|
3099
3105
|
if (m.role === 'tool') {
|
|
3100
3106
|
var tr = parseToolResultContent(m.content);
|
|
@@ -3236,11 +3242,15 @@ function groupHistoryMessages(messages) {
|
|
|
3236
3242
|
var ep = parts[ei];
|
|
3237
3243
|
if (ep.type === 'exec' && ep.data.tool_name && (ep.data.tool_name === 'playaudio' || ep.data.tool_name === 'playvideo')) {
|
|
3238
3244
|
try {
|
|
3239
|
-
|
|
3240
|
-
var
|
|
3245
|
+
// [v1.27.0] 从 params 中提取 XML 子标签 <url> 和 <title>
|
|
3246
|
+
var _p = ep.data.params || '';
|
|
3247
|
+
var _urlMatch = typeof _p === 'string' ? _p.match(/<url\s*>([\s\S]*?)<\/url\s*>/) : null;
|
|
3248
|
+
var murl = _urlMatch ? _urlMatch[1].trim() : '';
|
|
3249
|
+
var _titleMatch = typeof _p === 'string' ? _p.match(/<title\s*>([\s\S]*?)<\/title\s*>/) : null;
|
|
3250
|
+
var mtitle = _titleMatch ? _titleMatch[1].trim() : '';
|
|
3241
3251
|
if (murl) {
|
|
3242
3252
|
var mtype = ep.data.tool_name === 'playaudio' ? 'audio' : 'video';
|
|
3243
|
-
mediaEmbeds.push({ media_type: mtype, embed_url: murl, title:
|
|
3253
|
+
mediaEmbeds.push({ media_type: mtype, embed_url: murl, title: mtitle, original_url: murl });
|
|
3244
3254
|
}
|
|
3245
3255
|
} catch(mpe) { /* ignore parse errors */ }
|
|
3246
3256
|
}
|
|
@@ -3253,7 +3263,13 @@ function groupHistoryMessages(messages) {
|
|
|
3253
3263
|
var wp = parts[wi];
|
|
3254
3264
|
if (wp.type === 'exec' && wp.data.tool_name === 'web_control') {
|
|
3255
3265
|
try {
|
|
3256
|
-
|
|
3266
|
+
// [v1.27.0] 从 params 中提取 XML 子标签
|
|
3267
|
+
var _p = wp.data.params || '';
|
|
3268
|
+
var wcparams = {};
|
|
3269
|
+
if (typeof _p === 'string') {
|
|
3270
|
+
var _re = /<([a-zA-Z_][a-zA-Z0-9_]*)\s*>([\s\S]*?)<\/\1\s*>/g;
|
|
3271
|
+
var _m; while ((_m = _re.exec(_p)) !== null) { wcparams[_m[1]] = _m[2].trim(); }
|
|
3272
|
+
}
|
|
3257
3273
|
wcEvents.push(wcparams);
|
|
3258
3274
|
} catch(wce) { /* ignore */ }
|
|
3259
3275
|
}
|
|
@@ -1108,6 +1108,31 @@ function _updateToolCardInDOM(msgIdx, partIdx) {
|
|
|
1108
1108
|
targetCard.outerHTML = updatedHtml;
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
|
+
// ══════════════════════════════════════════════════════
|
|
1112
|
+
// ── [v1.27.0] 统一工具参数格式化 — XML 格式显示 ──
|
|
1113
|
+
// 所有页面共用此函数,将 raw parms 字符串格式化为人类可读的简短预览
|
|
1114
|
+
// ══════════════════════════════════════════════════════
|
|
1115
|
+
window.formatToolParamsPreview = function(rawParams, maxLen) {
|
|
1116
|
+
maxLen = maxLen || 200;
|
|
1117
|
+
if (!rawParams) return '';
|
|
1118
|
+
var str = (typeof rawParams === 'string') ? rawParams : JSON.stringify(rawParams);
|
|
1119
|
+
// XML 格式: 提取所有 <key>value</key> 对,格式化为 "key: value"
|
|
1120
|
+
var pairs = [];
|
|
1121
|
+
var re = /<([a-zA-Z_][a-zA-Z0-9_]*)\s*>([\s\S]*?)<\/\1\s*>/g;
|
|
1122
|
+
var m;
|
|
1123
|
+
while ((m = re.exec(str)) !== null) {
|
|
1124
|
+
var val = m[2].trim();
|
|
1125
|
+
if (val.length > 100) val = val.substring(0, 100) + '...';
|
|
1126
|
+
pairs.push(m[1] + ': ' + val);
|
|
1127
|
+
}
|
|
1128
|
+
if (pairs.length > 0) {
|
|
1129
|
+
var result = pairs.join(' | ');
|
|
1130
|
+
return result.length > maxLen ? result.substring(0, maxLen) + '...' : result;
|
|
1131
|
+
}
|
|
1132
|
+
// 非 XML: 直接截断返回
|
|
1133
|
+
return str.length > maxLen ? str.substring(0, maxLen) + '...' : str;
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1111
1136
|
function renderInlineExecEvent(data, msgIdx) {
|
|
1112
1137
|
// V2 Tool Event handling (called with full part: {type:'v2_tool', data:{...}})
|
|
1113
1138
|
if (data.type === 'v2_tool') {
|
|
@@ -1142,10 +1167,9 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
1142
1167
|
if (isResult && inner.result) {
|
|
1143
1168
|
bodyHtml += '<button class="inline-exec-result-btn" onclick="showToolResultModal(' + msgIdx + ', \'' + inner.id + '\')">查看详情</button>';
|
|
1144
1169
|
}
|
|
1145
|
-
// Show params for tool_start
|
|
1170
|
+
// Show params for tool_start — [v1.27.0] 使用统一格式化函数
|
|
1146
1171
|
if (isStart && inner.params) {
|
|
1147
|
-
let paramPreview =
|
|
1148
|
-
if (paramPreview.length > 200) paramPreview = paramPreview.substring(0, 200) + '...';
|
|
1172
|
+
let paramPreview = window.formatToolParamsPreview(inner.params, 200);
|
|
1149
1173
|
bodyHtml += '<div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div>';
|
|
1150
1174
|
}
|
|
1151
1175
|
|
|
@@ -1184,10 +1208,9 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
1184
1208
|
|
|
1185
1209
|
// Build body content
|
|
1186
1210
|
let bodyHtml = '';
|
|
1187
|
-
// Params for tool_call/skill_call (合并卡片中 params 在折叠区域显示)
|
|
1211
|
+
// Params for tool_call/skill_call (合并卡片中 params 在折叠区域显示) — [v1.27.0] 统一格式化
|
|
1188
1212
|
if (data.params && (data.type === 'tool_call' || data.type === 'skill_call') && !data.has_result) {
|
|
1189
|
-
let paramPreview =
|
|
1190
|
-
if (paramPreview.length > 300) paramPreview = paramPreview.substring(0, 300) + '...';
|
|
1213
|
+
let paramPreview = window.formatToolParamsPreview(data.params, 300);
|
|
1191
1214
|
bodyHtml += '<div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div>';
|
|
1192
1215
|
}
|
|
1193
1216
|
// Code preview for code_exec/code_result
|
|
@@ -1198,8 +1221,7 @@ function renderInlineExecEvent(data, msgIdx) {
|
|
|
1198
1221
|
if (data.has_result) {
|
|
1199
1222
|
// 折叠的参数区域
|
|
1200
1223
|
if (data.params) {
|
|
1201
|
-
let paramPreview =
|
|
1202
|
-
if (paramPreview.length > 300) paramPreview = paramPreview.substring(0, 300) + '...';
|
|
1224
|
+
let paramPreview = window.formatToolParamsPreview(data.params, 300);
|
|
1203
1225
|
bodyHtml += '<div class="tool-result-collapsible"><div class="tool-collapsible-toggle" onclick="this.parentElement.classList.toggle(\'collapsed\')">📋 参数</div><div class="tool-collapsible-body"><div class="inline-exec-code">' + escapeHtml(paramPreview) + '</div></div></div>';
|
|
1204
1226
|
}
|
|
1205
1227
|
// 结果摘要
|
package/web/ui/chat/groupchat.js
CHANGED
|
@@ -469,6 +469,16 @@ function startPrivateChatFromGroup(agentPath) {
|
|
|
469
469
|
// ── Render Group Messages ──
|
|
470
470
|
// ══════════════════════════════════════════════════════
|
|
471
471
|
|
|
472
|
+
// [v1.26.6] formatFileSize polyfill(chat_main.js 中已有全局定义,此处作兜底)
|
|
473
|
+
if (typeof formatFileSize !== 'function') {
|
|
474
|
+
function formatFileSize(bytes) {
|
|
475
|
+
if (!bytes || bytes < 0) return '';
|
|
476
|
+
if (bytes < 1024) return bytes + ' B';
|
|
477
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
478
|
+
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
472
482
|
function renderGroupMessages() {
|
|
473
483
|
try {
|
|
474
484
|
_renderGroupMessagesInner();
|
|
@@ -516,6 +526,33 @@ function _renderGroupMessagesInner() {
|
|
|
516
526
|
var _clickAvatar = agentPath ? ' onclick="startPrivateChatFromGroup(\'' + escapeHtml(agentPath) + '\')" style="background:' + agentColor + ';color:#fff;cursor:pointer" title="点击私聊 ' + escapeHtml(agentName) + '"' : ' style="background:' + agentColor + ';color:#fff"';
|
|
517
527
|
// [v1.23.82] 流式输出时显示闪烁光标
|
|
518
528
|
var _streamingCursor = msg._streaming ? '<span class="streaming-cursor" style="display:inline-block;width:2px;height:1em;background:var(--accent);margin-left:2px;animation:blink 1s step-end infinite;vertical-align:text-bottom"></span>' : '';
|
|
529
|
+
// [v1.26.6] 文件附件(群聊 file_send)
|
|
530
|
+
var _filesHtml = '';
|
|
531
|
+
if (msg._files && msg._files.length > 0) {
|
|
532
|
+
for (var _fi2 = 0; _fi2 < msg._files.length; _fi2++) {
|
|
533
|
+
var _ff = msg._files[_fi2];
|
|
534
|
+
var _fId = _ff.id || _ff.file_id || '';
|
|
535
|
+
var _fName = _ff.name || '文件';
|
|
536
|
+
var _fSize = _ff.size ? formatFileSize(_ff.size) : '';
|
|
537
|
+
var _fIcon = _ff.type && _ff.type.startsWith('image/') ? '🖼' : (_ff.type && _ff.type.startsWith('audio/') ? '🎵' : (_ff.type && _ff.type.startsWith('video/') ? '🎬' : '📄'));
|
|
538
|
+
var _fUrl = '/api/file/' + _fId + '?name=' + encodeURIComponent(_fName);
|
|
539
|
+
// 图片类型显示缩略图
|
|
540
|
+
if (_ff.type && _ff.type.startsWith('image/')) {
|
|
541
|
+
_filesHtml += '<div class="group-msg-file-image" style="margin-top:6px">'
|
|
542
|
+
+ '<img src="' + _fUrl + '" style="max-width:300px;max-height:200px;border-radius:8px;cursor:pointer" onclick="window.open(this.src)" onerror="this.style.display=\'none\'">'
|
|
543
|
+
+ '</div>';
|
|
544
|
+
} else {
|
|
545
|
+
_filesHtml += '<div class="group-msg-file-item" style="margin-top:6px;padding:8px 12px;background:var(--bg-tertiary);border-radius:8px;display:flex;align-items:center;gap:8px;cursor:pointer" onclick="window.open(\'' + _fUrl + '\')">'
|
|
546
|
+
+ '<span style="font-size:20px">' + _fIcon + '</span>'
|
|
547
|
+
+ '<span style="flex:1;min-width:0"><span style="display:block;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escapeHtml(_fName) + '</span>'
|
|
548
|
+
+ (_fSize ? '<span style="display:block;font-size:11px;color:var(--text-secondary)">' + _fSize + '</span>' : '')
|
|
549
|
+
+ '</span>'
|
|
550
|
+
+ '<a href="' + _fUrl + '" download="' + escapeHtml(_fName) + '" style="color:var(--text-secondary);text-decoration:none;font-size:16px" onclick="event.stopPropagation()" title="下载">⬇</a>'
|
|
551
|
+
+ '</div>';
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
519
556
|
html += '<div class="group-msg-row">'
|
|
520
557
|
+ '<div class="group-msg-avatar"' + _clickAvatar + '>' + agentEmoji + '</div>'
|
|
521
558
|
+ '<div>'
|
|
@@ -523,6 +560,7 @@ function _renderGroupMessagesInner() {
|
|
|
523
560
|
+ (agentRole ? ' <span class="role-badge">' + escapeHtml(agentRole) + '</span>' : '')
|
|
524
561
|
+ '</div>'
|
|
525
562
|
+ '<div class="group-msg-bubble" style="border-left-color:' + agentColor + '">' + renderMarkdown(msg.content || '') + _streamingCursor + '</div>'
|
|
563
|
+
+ _filesHtml
|
|
526
564
|
+ (msg.time ? '<div class="group-msg-time">' + formatTime(msg.time) + '</div>' : '')
|
|
527
565
|
+ '</div></div>';
|
|
528
566
|
} else if (msg.responses) {
|
|
@@ -1114,6 +1152,31 @@ async function sendGroupChat() {
|
|
|
1114
1152
|
_streamingContent = '';
|
|
1115
1153
|
} else if (evt.type === 'system_message') {
|
|
1116
1154
|
groupMessages.push({type: 'system', content: evt.content, time: new Date().toISOString()});
|
|
1155
|
+
} else if (evt.type === 'v2_file') {
|
|
1156
|
+
// [v1.26.6] 群聊文件卡片 — Agent 通过 file_send 工具发送文件
|
|
1157
|
+
if (evt.data) {
|
|
1158
|
+
var fd = evt.data;
|
|
1159
|
+
// 归一化: 后端用 file_id,渲染代码用 id
|
|
1160
|
+
if (fd.file_id && !fd.id) fd.id = fd.file_id;
|
|
1161
|
+
// 找到当前 agent 的消息(可能是正在流式输出的那条,或最后一条该 agent 的消息)
|
|
1162
|
+
var _targetPath = evt.agent_path || _streamingAgentPath;
|
|
1163
|
+
for (var fi = groupMessages.length - 1; fi >= 0; fi--) {
|
|
1164
|
+
var fm = groupMessages[fi];
|
|
1165
|
+
if (fm.agent === _targetPath || (fm._streaming && fm.agent === _streamingAgentPath)) {
|
|
1166
|
+
if (!fm._files) fm._files = [];
|
|
1167
|
+
// 去重
|
|
1168
|
+
var _fid2 = fd.id || fd.file_id;
|
|
1169
|
+
var _dup2 = _fid2 && fm._files.some(function(f) { return (f.id || f.file_id) === _fid2; });
|
|
1170
|
+
if (!_dup2) {
|
|
1171
|
+
fm._files.push(fd);
|
|
1172
|
+
}
|
|
1173
|
+
break;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// 重新渲染以显示文件卡片
|
|
1177
|
+
_renderGroupMessagesInner();
|
|
1178
|
+
if (typeof scrollToBottom === 'function') scrollToBottom(true);
|
|
1179
|
+
}
|
|
1117
1180
|
} else if (evt.type === 'done') {
|
|
1118
1181
|
// 全部完成
|
|
1119
1182
|
} else if (evt.type === 'error') {
|