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.
@@ -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>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>最多给它执行多久(秒),工具调用超过这个时限会立即回调大语言模型,方便调整工具使用</timeout></tool>
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>{"command": "要执行的命令"}</parms><timeout>超时秒数</timeout></tool>
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>{"command": "ls -la /tmp && df -h && python3 --version"}</parms><timeout>10</timeout></tool>
127
- <tool><toolname>command</toolname><parms>{"command": "myagent-ai search 人工智能最新进展"}</parms><timeout>15</timeout></tool>
128
- <tool><toolname>command</toolname><parms>{"command": "myagent-ai docx-create -c '{\"title\": \"报告\", \"sections\": [{\"heading\": \"摘要\", \"body\": \"内容\"}]}' -t 周报"}</parms><timeout>30</timeout></tool>
129
- <tool><toolname>command</toolname><parms>{"command": "cat /etc/os-release && uname -a && free -h"}</parms><timeout>10</timeout></tool>
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>{"file_path": "文件的绝对路径", "description": "文件描述(可选)"}</parms><timeout>30</timeout></tool>
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>{"action": "open", "url": "https://example.com"}</parms><timeout>30</timeout></tool>
139
- - 打开: {"action": "open", "url": "URL"}
140
- - 导航: {"action": "navigate", "url": "URL", "session_id": "xxx"}
141
- - 获取内容: {"action": "get_content", "what": "text|html|url|title|links|images|forms|inputs", "session_id": "xxx"}
142
- - 点击: {"action": "click", "selector": "CSS选择器", "session_id": "xxx"}
143
- - 填写: {"action": "fill", "selector": "CSS选择器", "value": "内容", "session_id": "xxx"}
144
- - 滚动: {"action": "scroll", "direction": "up|down|top|bottom", "distance": 300, "session_id": "xxx"}
145
- - 执行JS: {"action": "evaluate", "script": "JS代码", "session_id": "xxx"}
146
- - 截图: {"action": "screenshot", "session_id": "xxx"}
147
- - 等待: {"action": "wait", "time": 1000}{"action": "wait", "selector": ".result", "timeout": 10}
148
- - Cookie: {"action": "set_cookies", "cookies": [...], "session_id": "xxx"}{"action": "get_cookies", "session_id": "xxx"}
149
- - 关闭: {"action": "close", "session_id": "xxx"}
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 json as _json
1660
+ import re as _re
1656
1661
  import html as _html
1657
1662
  try:
1658
- # [v1.23.38] 防御性修复: 解码 HTML 实体 (&amp; → & 等)
1659
- # LLM 可能在 XML 输出中使用 &amp; 代替 &,导致 bash 命令失败
1660
1663
  _clean_parms = _html.unescape(parms_str) if parms_str else ""
1661
- params = _json.loads(_clean_parms) if _clean_parms else {}
1662
- except (_json.JSONDecodeError, TypeError):
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.26.6",
3
+ "version": "1.26.8",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -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>{"command": "myagent-ai <cmd> [args...]"}</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
- const tc=(m.content||'');
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 {
@@ -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
- return { title: title, toolName: toolName, params: params };
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
- var mparams = typeof ep.data.params === 'string' ? JSON.parse(ep.data.params) : (ep.data.params || {});
3240
- var murl = mparams.url || '';
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: mparams.title || '', original_url: murl });
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
- var wcparams = typeof wp.data.params === 'string' ? JSON.parse(wp.data.params) : (wp.data.params || {});
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 = typeof inner.params === 'string' ? inner.params : JSON.stringify(inner.params);
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 = typeof data.params === 'string' ? data.params : JSON.stringify(data.params);
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 = typeof data.params === 'string' ? data.params : JSON.stringify(data.params);
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
  // 结果摘要
@@ -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') {