myagent-ai 1.23.33 → 1.23.35

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.
@@ -48,7 +48,7 @@ class MainAgent(BaseAgent):
48
48
  <mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
49
49
  <usersays_correct>通过修正识别错误、调整标点,结合上下文,将用户语音转写文本"usersays"修正为更准确的文本,但尽量少改动。如"usersays"为空,则此处为空。</usersays_correct>
50
50
  <response><reply>展示给用户的文本,格式上尽量使用md格式,直观形象展示,甚至可以包括超链接、表格等。内容上,针对用户问题,直接回应问题;针对任务,开始的时候,告诉用户,为完成任务,准备如何开展工作;执行过程中,根据工具调用结果,简单展示任务进展;任务完成后的详细最终总结。注意:这是给用户展示信息的最重要标签,尽量不要跟上次回复重复,执行过程展示内容尽量简洁,执行总结可以丰富一点。</reply><toolstocal>
51
- <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
51
+ <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>最多给它执行多久(秒),工具调用超过这个时限会立即回调大语言模型,方便调整工具使用</timeout></tool>
52
52
  </toolstocal>
53
53
  </response>
54
54
  <task_plan>若"context"包含非空"task_plan",则更新它:若任务条数已超8,则精简为3条,若主题发生明显变化,重新设计任务列表。若"context"包含空"task_plan",则先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过2步,则此处输出为空,不创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过2步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记,排序按已完成在前。</task_plan>
@@ -63,7 +63,7 @@ class MainAgent(BaseAgent):
63
63
  </output>
64
64
 
65
65
  注意事项:
66
- 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool"工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超时了,也会回调大模型,让大模型分析为什么超时,改用其他工具。要求每个工具调用尽快合并多个命令行。注意: docx/xlsx/ppt/pdf-create 命令执行后会自动发送文件给用户,不需要再拼接 send-file。仅在需要发送其他已存在的独立文件时才需要手动调用 send-file。多个独立查询或系统命令才需要用 && 拼接。
66
+ 1. toolstocal标签: 尽量一次性列出所有需执行工具调用的,多个"tool"工具调用只要按顺序重复堆叠tool标签即可,解析器会按顺序执行工具调用,最终全部执行完后,会连同所有结果,回调大语言模型。如果某个工具执行超过预估的时限,也会回调大模型,让大模型分析为什么超时,改用其他工具。要求每个工具调用尽快合并多个命令行。注意: docx/xlsx/ppt/pdf-create 命令执行后会自动发送文件给用户,不需要再拼接 send-file。仅在需要发送其他已存在的独立文件时才需要手动调用 send-file。多个独立查询或系统命令才需要用 && 拼接。
67
67
  2. 上下文中的记忆系统说明
68
68
  - <automemory>: 系统自动根据你通过 <remember> 保存的记忆和当前用户输入,搜索出的 top10 相关记忆。这些是你过去主动记住的内容(包含时间信息),可供参考。
69
69
  - <recall_memory>: 你在上一轮通过 <recall> 指定的记忆搜索结果。系统根据你提供的关键字和时间点搜索了 top5 相关记忆。
@@ -184,7 +184,7 @@ class MainAgent(BaseAgent):
184
184
  <mainsubject>当前对话的6字以内标题(每轮都需输出,系统会每3轮自动更新会话名称)</mainsubject>
185
185
  <usersays_correct>通过修正识别错误、调整标点,结合上下文,将用户语音转写文本"usersays"修正为更准确的文本,但尽量少改动。如"usersays"为空,则此处为空。</usersays_correct>
186
186
  <response><reply>展示给用户的文本,格式上尽量使用md格式,直观形象展示,甚至可以包括超链接、表格等。内容上,针对用户问题,直接回应问题;针对任务,开始的时候,告诉用户,为完成任务,准备如何开展工作;执行过程中,根据工具调用结果,简单展示任务进展;任务完成后的详细最终总结。注意:这是给用户展示信息的最重要标签,尽量不要跟上次回复重复,执行过程展示内容尽量简洁,执行总结可以丰富一点。</reply><toolstocal>
187
- <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>预估调用超时时限(秒),工具调用超时会立即回调大语言模型,方便调整工具使用</timeout></tool>
187
+ <tool><beforecalltext>展示给用户的简单工具调用信息。格式:先使用"接下来、下一步、接着、现在、然后、最后、等下"等连接词➕调用"工具名"。</beforecalltext><toolname>工具名,用于后台解析器解析调用工具</toolname><parms>调用工具的JSON格式参数对象,如格式: {"query": "搜索关键词", "num": 5}</parms><timeout>最多给它执行多久(秒),工具调用超过这个时限会立即回调大语言模型,方便调整工具使用。</timeout></tool>
188
188
  </toolstocal>
189
189
  </response>
190
190
  <task_plan>若"context"包含非空"task_plan",则更新它:若任务条数已超8,则精简为3条,若主题发生明显变化,重新设计任务列表。若"context"包含空"task_plan",则先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过2步,则此处输出为空,不创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过2步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记,排序按已完成在前。</task_plan>
@@ -22,7 +22,6 @@ Expected XML schema produced by the LLM::
22
22
  <toolname>工具名</toolname>
23
23
  <parms>参数JSON或描述</parms>
24
24
  <timeout>预估超时时限(秒)</timeout>
25
- <callback>true/false</callback>
26
25
  </tool>
27
26
  </toolstocal>
28
27
  </response>
@@ -62,7 +61,6 @@ logger = get_logger("myagent.output_parser")
62
61
  # ---------------------------------------------------------------------------
63
62
 
64
63
  _DEFAULT_TIMEOUT: int = 120
65
- _DEFAULT_CALLBACK: bool = True
66
64
 
67
65
  # All top-level tags we recognise inside <output>.
68
66
  KNOWN_TOP_LEVEL_TAGS = [
@@ -89,7 +87,6 @@ TOOL_INNER_TAGS = [
89
87
  "toolname",
90
88
  "parms",
91
89
  "timeout",
92
- "callback",
93
90
  ]
94
91
 
95
92
  # Inner tags inside <remember>.
@@ -590,10 +587,6 @@ def _parse_toolstocal(toolstocal_content: str, *, conservative: bool = False) ->
590
587
  _extract_tag_content(block, "timeout", TOOL_INNER_TAGS, conservative=conservative),
591
588
  _DEFAULT_TIMEOUT,
592
589
  ),
593
- "callback": _parse_bool(
594
- _extract_tag_content(block, "callback", TOOL_INNER_TAGS, conservative=conservative),
595
- _DEFAULT_CALLBACK,
596
- ),
597
590
  }
598
591
  # Only add if toolname is present
599
592
  if tool["toolname"]:
@@ -300,16 +300,15 @@ class ToolDispatcher:
300
300
  ) -> Dict:
301
301
  """发送文件给用户 — 后端推送 v2_file SSE 事件 + 持久化到聊天记录"""
302
302
  try:
303
- from skills.file_send import FileSendSkill
304
- fskill = FileSendSkill()
303
+ from skills.file_send import UPLOADS_DIR
305
304
  fpath = params.get("file_path", "")
306
305
  fdesc = params.get("description", "")
307
306
  if not fpath:
308
307
  logger.warning(f"[{task_id}] file_send: 缺少 file_path 参数")
309
308
  return {"success": False, "error": "缺少 file_path 参数,请提供要发送的文件路径"}
310
309
  logger.info(f"[{task_id}] file_send: 发送文件 {fpath}")
311
-
312
- # [v1.23.29] 先复制文件(不依赖 file_send.execute 的 SSE 发送)
310
+
311
+ # [v1.23.35] 先复制文件(不依赖 file_send.execute 的 SSE 发送)
313
312
  from pathlib import Path as _P
314
313
  import shutil, uuid as _uuid, time as _time
315
314
  fpath_resolved = _P(fpath.strip().strip("'\"")).expanduser()
@@ -319,7 +318,8 @@ class ToolDispatcher:
319
318
  return {"success": False, "error": f"不是文件: {fpath}"}
320
319
 
321
320
  file_id = str(_uuid.uuid4())[:12]
322
- date_dir = fskill.UPLOADS_DIR / _time.strftime("%Y-%m")
321
+ # [v1.23.35] 直接使用模块级 UPLOADS_DIR,不依赖 FileSendSkill 实例属性
322
+ date_dir = UPLOADS_DIR / _time.strftime("%Y-%m")
323
323
  date_dir.mkdir(parents=True, exist_ok=True)
324
324
  stored_name = f"{file_id}_{fpath_resolved.name}"
325
325
  stored_path = date_dir / stored_name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.33",
3
+ "version": "1.23.35",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -1810,12 +1810,9 @@ window.addEventListener('beforeunload', function() {{
1810
1810
  agent_name = data.get("agent_name", "default") or "default"
1811
1811
  # 支持 path 格式 (如 "coder/python-expert")
1812
1812
  agent_path = data.get("agent_path", agent_name)
1813
- raw_session_id = data.get("session_id", "") or "web_default"
1814
- # Avoid double-prefixing: if the session_id already starts with agent_path_, use it directly
1815
- if raw_session_id.startswith(f"{agent_path}_"):
1816
- session_id = raw_session_id
1817
- else:
1818
- session_id = f"{agent_path}_{raw_session_id}"
1813
+ # [v1.23.35] 直接使用前端传来的 session_id,不再拼接 agent_path 前缀
1814
+ # 新格式(sess_uuid)和旧格式(agent_path_web_xxx)均兼容
1815
+ session_id = data.get("session_id", "") or "web_default"
1819
1816
  chat_mode = data.get("mode", "") # "exec" = 执行模式
1820
1817
  escalated = data.get("escalated", False) # 临时提权到 local
1821
1818
 
@@ -1970,12 +1967,8 @@ window.addEventListener('beforeunload', function() {{
1970
1967
  return web.Response(text="data: " + json.dumps({"error": "message is required"}) + "\n\n", content_type="text/event-stream")
1971
1968
 
1972
1969
  agent_path = data.get("agent_path", data.get("agent_name", "default")) or "default"
1973
- raw_session_id = data.get("session_id", "") or "web_default"
1974
- # Avoid double-prefixing: if the session_id already starts with agent_path_, use it directly
1975
- if raw_session_id.startswith(f"{agent_path}_"):
1976
- session_id = raw_session_id
1977
- else:
1978
- session_id = f"{agent_path}_{raw_session_id}"
1970
+ # [v1.23.35] 直接使用前端传来的 session_id,不再拼接 agent_path 前缀
1971
+ session_id = data.get("session_id", "") or "web_default"
1979
1972
  chat_mode = data.get("mode", "")
1980
1973
  escalated = data.get("escalated", False)
1981
1974
  voice_text = data.get("voice_text", "").strip() # 语音转文字原始文本(用于 usersays_correct)
@@ -2255,12 +2248,8 @@ window.addEventListener('beforeunload', function() {{
2255
2248
  return web.json_response({"error": "message is required"}, status=400)
2256
2249
 
2257
2250
  agent_path = data.get("agent_path", "default")
2258
- raw_session_id = data.get("session_id", "web_default")
2259
- # Avoid double-prefixing: if the session_id already starts with agent_path_, use it directly
2260
- if raw_session_id.startswith(f"{agent_path}_"):
2261
- session_id = raw_session_id
2262
- else:
2263
- session_id = f"{agent_path}_{raw_session_id}"
2251
+ # [v1.23.35] 直接使用前端传来的 session_id,不再拼接 agent_path 前缀
2252
+ session_id = data.get("session_id", "web_default")
2264
2253
  choice = data.get("choice", "queue") # "continue" (插入后继续) 或 "queue" (排队)
2265
2254
 
2266
2255
  # 检查会话是否正在运行
@@ -7807,8 +7796,10 @@ window.addEventListener('beforeunload', function() {{
7807
7796
  # 构建最终消息(用户原文不包含任何注入内容)
7808
7797
  agent_content = content
7809
7798
 
7799
+ # [v1.23.34] 群聊场景直接调用 _try_model_chain_inner(绕过全局锁)
7800
+ # 全局锁 _model_chain_lock 在并行场景下会导致死锁/阻塞
7810
7801
  if model_chain and self.core.llm:
7811
- response = await self._try_model_chain(
7802
+ response = await self._try_model_chain_inner(
7812
7803
  model_chain, agent_content, session_id,
7813
7804
  agent_path=agent_path, agent_system_prompt=agent_system_prompt,
7814
7805
  )
@@ -7841,26 +7832,21 @@ window.addEventListener('beforeunload', function() {{
7841
7832
  "response": f"处理失败: {str(e)}",
7842
7833
  }
7843
7834
 
7844
- # 并行调用目标成员agent
7845
- tasks = [process_agent_member(m) for m in target_members]
7846
- try:
7847
- gather_results = await asyncio.gather(*tasks, return_exceptions=True)
7848
- except Exception as e:
7849
- logger.error(f"群消息广播异常: {e}")
7850
- tp.update_task_status(task_id, "failed", last_message=f"广播异常: {str(e)}")
7851
- return web.json_response({"error": f"群消息广播异常: {str(e)}"}, status=500)
7852
-
7853
- # 处理异常结果并按原始成员顺序排序
7835
+ # [v1.23.34] 串行调用目标成员agent(避免并行时 LLM 全局状态互相覆盖导致卡死)
7836
+ # 原因: _try_model_chain_inner 会修改 self.core.llm provider/model/api_key 等全局属性
7837
+ # 并行执行会导致 Agent 之间互相覆盖模型配置,甚至死锁
7854
7838
  raw_responses = []
7855
- for r in gather_results:
7856
- if isinstance(r, Exception):
7839
+ for member in target_members:
7840
+ try:
7841
+ result = await process_agent_member(member)
7842
+ raw_responses.append(result)
7843
+ except Exception as e:
7844
+ logger.error(f"群消息处理异常 ({member.agent_path}): {e}")
7857
7845
  raw_responses.append({
7858
- "ok": False, "agent_path": "unknown",
7859
- "name": "unknown", "avatar": "❌",
7860
- "response": f"异常: {str(r)}",
7846
+ "ok": False, "agent_path": member.agent_path,
7847
+ "name": member.agent_path, "avatar": "❌",
7848
+ "response": f"异常: {str(e)}",
7861
7849
  })
7862
- else:
7863
- raw_responses.append(r)
7864
7850
 
7865
7851
  # Sort by original member order to ensure deterministic message ordering
7866
7852
  final_responses = sorted(
@@ -1797,6 +1797,7 @@ input,textarea,select{font:inherit}
1797
1797
  border:1px solid var(--border-light);border-bottom-left-radius:4px;
1798
1798
  border-left:3px solid var(--accent);
1799
1799
  overflow-wrap:break-word;overflow:hidden;position:relative;
1800
+ user-select:text;-webkit-user-select:text;
1800
1801
  }
1801
1802
  .group-msg-bubble p{margin-bottom:8px}
1802
1803
  .group-msg-bubble p:last-child{margin-bottom:0}
@@ -328,6 +328,14 @@ const StatePersistence = {
328
328
  if (savedSessionId) {
329
329
  state._pendingSessionRestore = savedSessionId;
330
330
  }
331
+ // [v1.23.34] 恢复群聊状态
332
+ var savedView = StatePersistence.load('currentView', 'chat');
333
+ var savedGroupId = StatePersistence.load('currentGroupId', null);
334
+ if (savedView === 'group' && savedGroupId) {
335
+ currentView = 'group';
336
+ currentGroupId = savedGroupId;
337
+ state._pendingGroupRestore = savedGroupId;
338
+ }
331
339
  },
332
340
  /** 标记 setup 已完成(避免每次刷新弹出向导) */
333
341
  markSetupDone() { StatePersistence.save('setupDone', true); },
@@ -445,8 +453,12 @@ async function initChat() {
445
453
  selectAgent(resolved);
446
454
  }
447
455
  } else if (urlSession) {
448
- // 只有 session 没有 agent,尝试从 session ID 推断 agent
449
- const targetAgent = urlSession.split('_web_')[0] || 'default';
456
+ // 只有 session 没有 agent,尝试从 session ID 推断 agent(兼容旧格式 agent_web_xxx)
457
+ var targetAgent = 'default';
458
+ if (urlSession.indexOf('_web_') >= 0) {
459
+ targetAgent = urlSession.split('_web_')[0] || 'default';
460
+ }
461
+ // 新格式 sess_xxx 无法推断 agent,回退 default
450
462
  var target = targetAgent;
451
463
  if (!findAgentByPath(target)) {
452
464
  console.warn('Session-inferred agent not found, fallback to default:', target);
@@ -465,6 +477,14 @@ async function initChat() {
465
477
  }
466
478
  // 如果 agent 一致,loadSessions() 内部已通过 _pendingSessionRestore 自动处理了
467
479
  }
480
+
481
+ // [v1.23.34] 恢复群聊视图(在所有初始化完成后)
482
+ if (state._pendingGroupRestore && typeof selectGroup === 'function') {
483
+ var _gid = state._pendingGroupRestore;
484
+ state._pendingGroupRestore = null;
485
+ // 等待 fetchGroups 完成(initChat 中已调用),延迟恢复
486
+ setTimeout(function() { selectGroup(_gid); }, 300);
487
+ }
468
488
  }
469
489
 
470
490
  // Run init: if DOMContentLoaded already fired (dynamic script load), run immediately
@@ -2062,7 +2082,8 @@ function formatSessionName(id) {
2062
2082
  if (!id) return '新会话';
2063
2083
  if (id.startsWith('web_')) return id.replace('web_', '').replace(/_/g, ' ');
2064
2084
  if (id.startsWith('cli_')) return 'CLI: ' + id.replace('cli_', '');
2065
- // Strip agent prefix if present (e.g., "default_web_2024..." -> "web_2024...")
2085
+ // [v1.23.35] 新格式 sess_xxx 直接显示"新对话",由后端自动命名
2086
+ if (id.startsWith('sess_')) return '新对话';
2066
2087
  return id.length > 20 ? id.slice(0, 20) + '...' : id;
2067
2088
  }
2068
2089
 
@@ -3053,9 +3074,9 @@ window.buildMessageHtml = function(msg, idx, agent) {
3053
3074
  taskPlanHtml +
3054
3075
  finishReasonHtml +
3055
3076
  imageAttachmentHtml +
3056
- mediaEmbedHtml +
3057
3077
  wcHtml +
3058
3078
  bubbleHtml +
3079
+ mediaEmbedHtml +
3059
3080
  fileAttachmentHtml +
3060
3081
  streamingIndicator +
3061
3082
  (msg.time ? '<div class="message-time">' + formatTime(msg.time) + '</div>' : '') +
@@ -692,16 +692,16 @@ function updateStreamingMessage(msgIdx) {
692
692
  }
693
693
 
694
694
  // ── [v1.23.19] 增量渲染在线媒体嵌入播放器(v2_media 事件) ──
695
- // [v1.23.20] 媒体容器放在 bubbleWrapper 外部(和 buildMessageHtml 结构一致)
695
+ // [v1.23.35] 媒体容器放在 bubbleWrapper 之后(对话底部),不再放在前面
696
696
  if (msg._media && msg._media.length > 0) {
697
697
  var existingMedia = contentArea.querySelectorAll(':scope > .msg-attachments-media');
698
698
  var mediaContainer = existingMedia.length > 0 ? existingMedia[0] : null;
699
699
  if (!mediaContainer) {
700
700
  mediaContainer = document.createElement('div');
701
701
  mediaContainer.className = 'msg-attachments msg-attachments-media';
702
- // 插入到 bubbleWrapper 之前(和 buildMessageHtml 顺序一致:media 先,bubble 后)
702
+ // [v1.23.35] 插入到 bubbleWrapper 之后(对话末尾)
703
703
  if (bubbleWrapper && bubbleWrapper.parentNode) {
704
- bubbleWrapper.parentNode.insertBefore(mediaContainer, bubbleWrapper);
704
+ bubbleWrapper.parentNode.insertBefore(mediaContainer, bubbleWrapper.nextSibling);
705
705
  } else {
706
706
  contentArea.appendChild(mediaContainer);
707
707
  }
@@ -1592,7 +1592,7 @@ async function sendMessage(opts) {
1592
1592
  return;
1593
1593
  }
1594
1594
 
1595
- // Create session if needed (incorporate agent name)
1595
+ // [v1.23.35] Create session if needed 使用 UUID 防止 ID 重复/乱串
1596
1596
  let sessionId = state.activeSessionId;
1597
1597
  if (!sessionId || sessionId === '__new__') {
1598
1598
  // 防御:如果当前已有消息(说明有活跃会话但 ID 丢失),尝试复用已有会话
@@ -1601,8 +1601,8 @@ async function sendMessage(opts) {
1601
1601
  state.activeSessionId = sessionId;
1602
1602
  console.warn('[sendMessage] activeSessionId was empty but messages exist, reusing session:', sessionId);
1603
1603
  } else {
1604
- const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
1605
- sessionId = `${state.activeAgent}_web_${ts}`;
1604
+ // 使用 UUID v4 保证全局唯一性,不再暴露 agent 名明文
1605
+ sessionId = 'sess_' + crypto.randomUUID().replace(/-/g, '').slice(0, 20);
1606
1606
  state.activeSessionId = sessionId;
1607
1607
  document.getElementById('headerTitle').textContent = formatSessionName(sessionId);
1608
1608
  // ── 立即在左侧边栏添加新会话条目(不等后端返回) ──
@@ -138,6 +138,11 @@ async function selectGroup(gid) {
138
138
  }
139
139
  currentView = 'group';
140
140
  currentGroupId = gid;
141
+ // [v1.23.34] 持久化群聊状态,刷新页面后恢复
142
+ if (typeof StatePersistence !== 'undefined') {
143
+ StatePersistence.save('currentView', 'group');
144
+ StatePersistence.save('currentGroupId', gid);
145
+ }
141
146
 
142
147
  // Update sidebar
143
148
  renderSessions();
@@ -194,6 +199,11 @@ function exitGroupChat() {
194
199
  currentView = 'chat';
195
200
  currentGroupId = null;
196
201
  groupMessages = [];
202
+ // [v1.23.34] 清除持久化的群聊状态
203
+ if (typeof StatePersistence !== 'undefined') {
204
+ StatePersistence.save('currentView', 'chat');
205
+ StatePersistence.remove('currentGroupId');
206
+ }
197
207
  // 清空残留的聊天状态,避免切回个人聊天时显示群聊消息
198
208
  state.messages = [];
199
209
  state.activeSessionId = null;
@@ -267,8 +277,10 @@ function _renderGroupMessagesInner() {
267
277
  var agentEmoji = msg.agent_emoji || msg.sender_avatar || msg.avatar || '🤖';
268
278
  var agentColor = msg.agent_color || msg.color || 'var(--accent)';
269
279
  var agentRole = msg.agent_role || msg.role_detail || '';
280
+ var agentPath = msg.agent || '';
281
+ var _clickAvatar = agentPath ? ' onclick="showAgentProfile(\'' + escapeHtml(agentPath) + '\')" style="background:' + agentColor + ';color:#fff;cursor:pointer" title="点击查看简介"' : ' style="background:' + agentColor + ';color:#fff"';
270
282
  html += '<div class="group-msg-row">'
271
- + '<div class="group-msg-avatar" style="background:' + agentColor + ';color:#fff">' + agentEmoji + '</div>'
283
+ + '<div class="group-msg-avatar"' + _clickAvatar + '>' + agentEmoji + '</div>'
272
284
  + '<div>'
273
285
  + '<div class="group-msg-name">' + escapeHtml(agentName)
274
286
  + (agentRole ? ' <span class="role-badge">' + escapeHtml(agentRole) + '</span>' : '')
@@ -283,8 +295,10 @@ function _renderGroupMessagesInner() {
283
295
  var rName = r.agent_name || r.sender_name || r.name || r.agent || 'Agent';
284
296
  var rEmoji = r.agent_emoji || r.sender_avatar || r.avatar || '🤖';
285
297
  var rColor = r.agent_color || r.color || 'var(--accent)';
298
+ var rPath = r.agent_path || '';
299
+ var _rClickAvatar = rPath ? ' onclick="showAgentProfile(\'' + escapeHtml(rPath) + '\')" style="background:' + rColor + ';color:#fff;cursor:pointer" title="点击查看简介"' : ' style="background:' + rColor + ';color:#fff"';
286
300
  html += '<div class="group-msg-row">'
287
- + '<div class="group-msg-avatar" style="background:' + rColor + ';color:#fff">' + rEmoji + '</div>'
301
+ + '<div class="group-msg-avatar"' + _rClickAvatar + '>' + rEmoji + '</div>'
288
302
  + '<div>'
289
303
  + '<div class="group-msg-name">' + escapeHtml(rName) + '</div>'
290
304
  + '<div class="group-msg-bubble" style="border-left-color:' + rColor + '">' + renderMarkdown(r.content || r.response || '') + '</div>'
@@ -294,6 +308,17 @@ function _renderGroupMessagesInner() {
294
308
  }
295
309
 
296
310
  container.innerHTML = html;
311
+ // [v1.23.34] 为所有群聊气泡添加双击复制功能
312
+ container.querySelectorAll('.group-msg-bubble').forEach(function(bubble) {
313
+ bubble.addEventListener('dblclick', function() {
314
+ var text = bubble.innerText || bubble.textContent || '';
315
+ if (text) {
316
+ navigator.clipboard.writeText(text).then(function() {
317
+ if (typeof toast === 'function') toast('已复制消息', 'success');
318
+ }).catch(function() {});
319
+ }
320
+ });
321
+ });
297
322
  scrollToBottom();
298
323
  }
299
324
 
@@ -798,3 +823,38 @@ async function sendGroupChat() {
798
823
  input.focus();
799
824
  }
800
825
  }
826
+
827
+ // ══════════════════════════════════════════════════════
828
+ // ── Agent Profile Popup (点击头像显示简介) ──
829
+ // ══════════════════════════════════════════════════════
830
+
831
+ function showAgentProfile(agentPath) {
832
+ var agent = (state.agentsFlat || []).find(function(a) { return a.path === agentPath; });
833
+ if (!agent) {
834
+ if (typeof toast === 'function') toast('未找到 Agent 信息', 'error');
835
+ return;
836
+ }
837
+ var name = agent.name || agentPath;
838
+ var emoji = agent.avatar_emoji || '🤖';
839
+ var color = agent.avatar_color || getAgentGradient(name);
840
+ var desc = agent.description || '暂无描述';
841
+ var tools = agent.tools || [];
842
+ var toolsStr = tools.length > 0 ? tools.join(', ') : '默认工具集';
843
+
844
+ var container = document.getElementById('groupModalContainer');
845
+ // [v1.23.35] 添加"对话"按钮 — 点击后退出群聊,切换到该 Agent 的个人对话
846
+ var chatBtnHtml = typeof selectAgent === 'function'
847
+ ? '<button class="agent-modal-btn agent-modal-btn-primary" onclick="closeGroupModal();selectAgent(\'' + escapeHtml(agentPath) + '\')">💬 对话</button>'
848
+ : '';
849
+ container.innerHTML = '<div class="modal-overlay" onclick="if(event.target===this)closeGroupModal()"><div class="group-modal" style="max-width:380px">'
850
+ + '<div style="display:flex;align-items:center;gap:14px;margin-bottom:16px">'
851
+ + '<div class="group-msg-avatar" style="background:' + color + ';color:#fff;width:48px;height:48px;border-radius:12px;font-size:22px">' + emoji + '</div>'
852
+ + '<div><div style="font-size:18px;font-weight:700;color:var(--text)">' + escapeHtml(name) + '</div>'
853
+ + '<div style="font-size:12px;color:var(--text3)">' + escapeHtml(agentPath) + '</div></div></div>'
854
+ + '<div style="font-size:14px;color:var(--text2);line-height:1.6;margin-bottom:14px;padding:12px;background:var(--bg);border-radius:var(--radius-sm);border:1px solid var(--border-light)">' + escapeHtml(desc) + '</div>'
855
+ + (tools.length > 0 ? '<div style="font-size:12px;color:var(--text3);margin-bottom:14px"><span style="font-weight:600">工具:</span> ' + escapeHtml(toolsStr) + '</div>' : '')
856
+ + '<div class="agent-modal-actions">'
857
+ + chatBtnHtml
858
+ + '<button class="agent-modal-btn agent-modal-btn-cancel" onclick="closeGroupModal()">关闭</button>'
859
+ + '</div></div></div>';
860
+ }