myagent-ai 1.23.28 → 1.23.30
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 +10 -0
- package/core/tool_dispatcher.py +95 -29
- package/package.json +1 -1
- package/scripts/cli.py +32 -0
- package/web/api_server.py +112 -17
- package/web/ui/chat/chat.css +56 -0
- package/web/ui/chat/chat_main.js +20 -0
- package/web/ui/chat/groupchat.js +152 -1
package/agents/main_agent.py
CHANGED
|
@@ -134,6 +134,9 @@ class MainAgent(BaseAgent):
|
|
|
134
134
|
【记忆】
|
|
135
135
|
- 搜索记忆: myagent-ai memory [--keyword 关键词] [--limit N]
|
|
136
136
|
|
|
137
|
+
【Agent间通信】
|
|
138
|
+
- 向其他Agent发送消息: myagent-ai chat --agent <Agent路径> --message "消息内容"
|
|
139
|
+
|
|
137
140
|
【媒体播放】
|
|
138
141
|
- 播放音频: myagent-ai playaudio --url 音频URL [--title 标题] 或 myagent-ai playaudio --file 本地路径
|
|
139
142
|
- 播放视频: myagent-ai playvideo --url 视频URL [--title 标题] 或 myagent-ai playvideo --file 本地路径
|
|
@@ -149,11 +152,18 @@ class MainAgent(BaseAgent):
|
|
|
149
152
|
<tool><toolname>command</toolname><parms>{"command": "myagent-ai ppt-create -s '{JSON幻灯片}'"}</parms><timeout>30</timeout></tool>
|
|
150
153
|
<tool><toolname>command</toolname><parms>{"command": "myagent-ai playaudio --url https://music.163.com/song?id=123 --title 歌曲名"}</parms><timeout>10</timeout></tool>
|
|
151
154
|
<tool><toolname>command</toolname><parms>{"command": "myagent-ai playvideo --url https://www.bilibili.com/video/BV123 --title 视频名"}</parms><timeout>10</timeout></tool>
|
|
155
|
+
<tool><toolname>command</toolname><parms>{"command": "myagent-ai chat --agent default/coder --message \"请帮我分析这段代码的时间复杂度\""}</parms><timeout>10</timeout></tool>
|
|
152
156
|
|
|
153
157
|
多个命令可用 && 连接一次执行(强烈推荐,减少LLM回调次数):
|
|
154
158
|
<tool><toolname>command</toolname><parms>{"command": "myagent-ai search xxx && myagent-ai read-url https://..."}</parms><timeout>30</timeout></tool>
|
|
155
159
|
<tool><toolname>command</toolname><parms>{"command": "myagent-ai sysinfo && myagent-ai ps --filter python"}</parms><timeout>15</timeout></tool>
|
|
156
160
|
|
|
161
|
+
**file_send**(向用户发送文件,文件会以卡片形式显示在聊天中):
|
|
162
|
+
<tool><toolname>file_send</toolname><parms>{"file_path": "文件的绝对路径", "description": "文件描述(可选)"}</parms><timeout>30</timeout></tool>
|
|
163
|
+
- 当你需要把生成的文件(PDF、Excel、图片、脚本等)发送给用户时,直接使用此工具
|
|
164
|
+
- 当你需要发送一个已存在的文件时,直接使用此工具
|
|
165
|
+
- 不要把文件路径当成文本展示给用户,而是用 file_send 工具发送文件卡片
|
|
166
|
+
|
|
157
167
|
**web_control**(网页控制器,在聊天中打开可操作的浏览器面板):
|
|
158
168
|
<tool><toolname>web_control</toolname><parms>{"action": "open", "url": "https://example.com"}</parms><timeout>30</timeout></tool>
|
|
159
169
|
- 打开: {"action": "open", "url": "URL"}
|
package/core/tool_dispatcher.py
CHANGED
|
@@ -197,23 +197,13 @@ class ToolDispatcher:
|
|
|
197
197
|
try:
|
|
198
198
|
p = _P(send_path)
|
|
199
199
|
if p.exists():
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
stream_callback=stream_callback,
|
|
200
|
+
# [v1.23.29] 直接通过 _exec_file_send 发送(统一入口,确保 v2_file 推送)
|
|
201
|
+
file_result = await self._exec_file_send(
|
|
202
|
+
{"file_path": send_path, "description": send_desc},
|
|
203
|
+
task_id, stream_callback, sent_files,
|
|
205
204
|
)
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
if sent_files is not None and fresult.get("file_id"):
|
|
209
|
-
sent_files.append({
|
|
210
|
-
"id": fresult["file_id"],
|
|
211
|
-
"name": fresult.get("name", ""),
|
|
212
|
-
"size": fresult.get("size", 0),
|
|
213
|
-
"url": fresult.get("url", ""),
|
|
214
|
-
})
|
|
215
|
-
else:
|
|
216
|
-
result["output"] += f"\n[文件发送失败: {fresult.get('error', '')}]"
|
|
205
|
+
if not file_result.get("success"):
|
|
206
|
+
result["output"] += f"\n[文件发送失败: {file_result.get('error', '')}]"
|
|
217
207
|
except Exception as e:
|
|
218
208
|
logger.warning(f"[{task_id}] CLI 文件发送异常: {e}")
|
|
219
209
|
result["output"] += f"\n[文件发送异常: {e}]"
|
|
@@ -246,6 +236,31 @@ class ToolDispatcher:
|
|
|
246
236
|
if not media_result.get("success"):
|
|
247
237
|
result["output"] += f"\n[视频播放失败: {media_result.get('error', '')}]"
|
|
248
238
|
|
|
239
|
+
# [v1.23.29] 检测 __CHAT_AGENT__ 标记 — CLI chat 命令输出此标记
|
|
240
|
+
# 格式: __CHAT_AGENT__agent_path|agent_name|message__END__
|
|
241
|
+
chat_markers = _re.findall(r'__CHAT_AGENT__(.+?)\|(.+?)\|(.+?)__END__', clean_output)
|
|
242
|
+
if chat_markers:
|
|
243
|
+
clean_output = _re.sub(r'__CHAT_AGENT__.+?__END__\n?', '', clean_output).strip()
|
|
244
|
+
result["output"] = clean_output
|
|
245
|
+
for chat_agent_path, chat_agent_name, chat_msg in chat_markers:
|
|
246
|
+
# Store as a chat agent event in sent_files for persistence
|
|
247
|
+
if sent_files is not None:
|
|
248
|
+
sent_files.append({
|
|
249
|
+
"_type": "chat_agent",
|
|
250
|
+
"target_agent": chat_agent_path.strip(),
|
|
251
|
+
"target_name": chat_agent_name.strip(),
|
|
252
|
+
"message": chat_msg.strip(),
|
|
253
|
+
})
|
|
254
|
+
# Emit SSE event for frontend display
|
|
255
|
+
try:
|
|
256
|
+
await self._emit_sse("v2_chat_agent", {
|
|
257
|
+
"target_agent": chat_agent_path.strip(),
|
|
258
|
+
"target_name": chat_agent_name.strip(),
|
|
259
|
+
"message": chat_msg.strip(),
|
|
260
|
+
}, stream_callback)
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
|
|
249
264
|
return result
|
|
250
265
|
|
|
251
266
|
async def _exec_recall_memory(self, params: Dict, task_id: str) -> Dict:
|
|
@@ -278,7 +293,7 @@ class ToolDispatcher:
|
|
|
278
293
|
stream_callback: Optional[Callable] = None,
|
|
279
294
|
sent_files: Optional[List[Dict]] = None,
|
|
280
295
|
) -> Dict:
|
|
281
|
-
"""发送文件给用户"""
|
|
296
|
+
"""发送文件给用户 — 后端推送 v2_file SSE 事件 + 持久化到聊天记录"""
|
|
282
297
|
try:
|
|
283
298
|
from skills.file_send import FileSendSkill
|
|
284
299
|
fskill = FileSendSkill()
|
|
@@ -288,20 +303,71 @@ class ToolDispatcher:
|
|
|
288
303
|
logger.warning(f"[{task_id}] file_send: 缺少 file_path 参数")
|
|
289
304
|
return {"success": False, "error": "缺少 file_path 参数,请提供要发送的文件路径"}
|
|
290
305
|
logger.info(f"[{task_id}] file_send: 发送文件 {fpath}")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
|
|
307
|
+
# [v1.23.29] 先复制文件(不依赖 file_send.execute 的 SSE 发送)
|
|
308
|
+
from pathlib import Path as _P
|
|
309
|
+
import shutil, uuid as _uuid, time as _time
|
|
310
|
+
fpath_resolved = _P(fpath.strip().strip("'\"")).expanduser()
|
|
311
|
+
if not fpath_resolved.exists():
|
|
312
|
+
return {"success": False, "error": f"文件不存在: {fpath}"}
|
|
313
|
+
if not fpath_resolved.is_file():
|
|
314
|
+
return {"success": False, "error": f"不是文件: {fpath}"}
|
|
315
|
+
|
|
316
|
+
file_id = str(_uuid.uuid4())[:12]
|
|
317
|
+
date_dir = fskill.UPLOADS_DIR / _time.strftime("%Y-%m")
|
|
318
|
+
date_dir.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
stored_name = f"{file_id}_{fpath_resolved.name}"
|
|
320
|
+
stored_path = date_dir / stored_name
|
|
321
|
+
shutil.copy2(str(fpath_resolved), str(stored_path))
|
|
322
|
+
|
|
323
|
+
mime_map = {
|
|
324
|
+
".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg",
|
|
325
|
+
".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp",
|
|
326
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
327
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
328
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
329
|
+
".txt": "text/plain", ".csv": "text/csv", ".md": "text/markdown",
|
|
330
|
+
".json": "application/json", ".html": "text/html",
|
|
331
|
+
".mp3": "audio/mpeg", ".mp4": "video/mp4", ".wav": "audio/wav",
|
|
332
|
+
".zip": "application/zip", ".tar.gz": "application/gzip",
|
|
333
|
+
}
|
|
334
|
+
mime = mime_map.get(fpath_resolved.suffix.lower(), "application/octet-stream")
|
|
335
|
+
size = stored_path.stat().st_size
|
|
336
|
+
|
|
337
|
+
file_data = {
|
|
338
|
+
"id": file_id,
|
|
339
|
+
"file_id": file_id,
|
|
340
|
+
"name": fpath_resolved.name,
|
|
341
|
+
"type": mime,
|
|
342
|
+
"size": size,
|
|
343
|
+
"description": fdesc or f"文件: {fpath_resolved.name}",
|
|
344
|
+
"url": f"/api/file/{file_id}?name={fpath_resolved.name}",
|
|
345
|
+
"download_url": f"/api/file/{file_id}/download?name={fpath_resolved.name}",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# [v1.23.29] 关键:通过 _emit_sse 后端推送 v2_file 事件到前端
|
|
349
|
+
# 这是文件卡片显示的核心机制 — 不依赖 file_send.execute 内部的 SSE 发送
|
|
350
|
+
await self._emit_sse("v2_file", file_data, stream_callback)
|
|
351
|
+
logger.info(f"[{task_id}] file_send: v2_file 已推送 → {file_id} ({fpath_resolved.name})")
|
|
352
|
+
|
|
353
|
+
# 持久化到 sent_files(写入聊天记录数据库)
|
|
354
|
+
if sent_files is not None:
|
|
298
355
|
sent_files.append({
|
|
299
|
-
"id":
|
|
300
|
-
"
|
|
301
|
-
"
|
|
302
|
-
"
|
|
356
|
+
"id": file_id,
|
|
357
|
+
"file_id": file_id,
|
|
358
|
+
"name": fpath_resolved.name,
|
|
359
|
+
"type": mime,
|
|
360
|
+
"size": size,
|
|
361
|
+
"description": fdesc or f"文件: {fpath_resolved.name}",
|
|
362
|
+
"url": file_data["url"],
|
|
363
|
+
"download_url": file_data["download_url"],
|
|
303
364
|
})
|
|
304
|
-
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"success": True,
|
|
368
|
+
"output": f"文件已发送: {fpath_resolved.name} (ID: {file_id}, 大小: {size} bytes)",
|
|
369
|
+
"data": file_data,
|
|
370
|
+
}
|
|
305
371
|
except Exception as e:
|
|
306
372
|
logger.error(f"[{task_id}] file_send: 异常 - {e}", exc_info=True)
|
|
307
373
|
return {"success": False, "error": f"文件发送失败: {e}"}
|
package/package.json
CHANGED
package/scripts/cli.py
CHANGED
|
@@ -18,6 +18,7 @@ scripts/cli.py - MyAgent CLI 工具集
|
|
|
18
18
|
GUI: screenshot, mouse-click, mouse-drag, type-text, hotkey,
|
|
19
19
|
window-list, window-focus
|
|
20
20
|
记忆: memory
|
|
21
|
+
通信: chat
|
|
21
22
|
帮助: help, -h, --help
|
|
22
23
|
"""
|
|
23
24
|
from __future__ import annotations
|
|
@@ -135,6 +136,8 @@ async def _run():
|
|
|
135
136
|
# 媒体播放
|
|
136
137
|
"playaudio": cmd_playaudio,
|
|
137
138
|
"playvideo": cmd_playvideo,
|
|
139
|
+
# 通信
|
|
140
|
+
"chat": cmd_chat,
|
|
138
141
|
# 帮助
|
|
139
142
|
"help": cmd_help,
|
|
140
143
|
"-h": cmd_help,
|
|
@@ -990,6 +993,32 @@ async def cmd_playvideo(args):
|
|
|
990
993
|
print(f"__EMBED_VIDEO__{source}|{title}__END__")
|
|
991
994
|
|
|
992
995
|
|
|
996
|
+
# =============================================================================
|
|
997
|
+
# 通信命令
|
|
998
|
+
# =============================================================================
|
|
999
|
+
|
|
1000
|
+
async def cmd_chat(args):
|
|
1001
|
+
"""Agent间通信 — 向群内其他Agent发送消息"""
|
|
1002
|
+
import argparse
|
|
1003
|
+
p = argparse.ArgumentParser(prog="myagent-ai chat", description="向群内其他Agent发送消息,消息会出现在群聊记录中")
|
|
1004
|
+
p.add_argument("--agent", "-a", required=True, help="目标Agent路径,如 default/coder")
|
|
1005
|
+
p.add_argument("--message", "-m", required=True, help="要发送的消息内容")
|
|
1006
|
+
p.add_argument("--group", "-g", help="群ID(可选,默认使用最近活跃的群)")
|
|
1007
|
+
a = p.parse_args(args)
|
|
1008
|
+
|
|
1009
|
+
# Find agent config
|
|
1010
|
+
agent_dir = Path(__file__).parent.parent / "data" / "agents" / a.agent
|
|
1011
|
+
cfg_file = agent_dir / "config.json"
|
|
1012
|
+
if not cfg_file.exists():
|
|
1013
|
+
print(f"错误: Agent 不存在: {a.agent}", file=sys.stderr)
|
|
1014
|
+
sys.exit(1)
|
|
1015
|
+
cfg = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
1016
|
+
agent_name = cfg.get("name", a.agent)
|
|
1017
|
+
|
|
1018
|
+
# Output chat marker (similar to __SEND_FILE__ pattern)
|
|
1019
|
+
print(f"__CHAT_AGENT__{a.agent}|{agent_name}|{a.message}__END__")
|
|
1020
|
+
|
|
1021
|
+
|
|
993
1022
|
# =============================================================================
|
|
994
1023
|
# 帮助命令
|
|
995
1024
|
# =============================================================================
|
|
@@ -1058,6 +1087,9 @@ GUI (仅 Windows/macOS):
|
|
|
1058
1087
|
记忆:
|
|
1059
1088
|
myagent-ai memory [--keyword 关键词] [--limit N] 搜索历史记忆
|
|
1060
1089
|
|
|
1090
|
+
通信:
|
|
1091
|
+
myagent-ai chat --agent <路径> --message "消息" 向其他Agent发送消息
|
|
1092
|
+
|
|
1061
1093
|
媒体播放:
|
|
1062
1094
|
myagent-ai playaudio --url <URL> [--title] 播放在线音频
|
|
1063
1095
|
myagent-ai playaudio --file <路径> [--title] 播放本地音频
|
package/web/api_server.py
CHANGED
|
@@ -7630,6 +7630,9 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7630
7630
|
|
|
7631
7631
|
async def handle_send_group_message(self, request):
|
|
7632
7632
|
"""POST /api/groups/{gid}/messages - 发送群消息(广播到所有成员agent)"""
|
|
7633
|
+
import re as _re
|
|
7634
|
+
import time as _time
|
|
7635
|
+
|
|
7633
7636
|
gid = request.match_info["gid"]
|
|
7634
7637
|
data = await request.json()
|
|
7635
7638
|
content = data.get("message", "").strip()
|
|
@@ -7666,12 +7669,39 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7666
7669
|
metadata={"source": "group_chat", "group_name": group.name},
|
|
7667
7670
|
)
|
|
7668
7671
|
|
|
7669
|
-
# 3.
|
|
7672
|
+
# 3. [v1.23.29] 解析 @提及
|
|
7673
|
+
at_pattern = _re.compile(r'@(\S+)')
|
|
7674
|
+
at_targets = at_pattern.findall(content)
|
|
7675
|
+
mentioned_agents = set()
|
|
7676
|
+
mentioned_all = False
|
|
7677
|
+
|
|
7678
|
+
for target in at_targets:
|
|
7679
|
+
target_lower = target.lower()
|
|
7680
|
+
if target_lower in ('all', '所有人', '大家'):
|
|
7681
|
+
mentioned_all = True
|
|
7682
|
+
break
|
|
7683
|
+
for m in group.members:
|
|
7684
|
+
agent_cfg = self._read_agent_config(m.agent_path)
|
|
7685
|
+
agent_name = agent_cfg.get("name", "") if agent_cfg else ""
|
|
7686
|
+
if (target_lower == m.agent_path.lower()
|
|
7687
|
+
or target_lower == agent_name.lower()
|
|
7688
|
+
or (m.nickname and target_lower == m.nickname.lower())):
|
|
7689
|
+
mentioned_agents.add(m.agent_path)
|
|
7690
|
+
|
|
7691
|
+
# 4. 广播到所有非禁言成员agent,并行处理
|
|
7670
7692
|
active_members = [m for m in group.members if not m.muted]
|
|
7671
7693
|
|
|
7694
|
+
# [v1.23.29] 确定哪些 Agent 应该回复
|
|
7695
|
+
if mentioned_all:
|
|
7696
|
+
target_members = active_members
|
|
7697
|
+
elif mentioned_agents:
|
|
7698
|
+
target_members = [m for m in active_members if m.agent_path in mentioned_agents]
|
|
7699
|
+
else:
|
|
7700
|
+
target_members = active_members # 无@=广播给所有(向后兼容)
|
|
7701
|
+
|
|
7672
7702
|
import asyncio
|
|
7673
7703
|
# Build a member_order map for deterministic sorting after gather
|
|
7674
|
-
member_order = {m.agent_path: i for i, m in enumerate(
|
|
7704
|
+
member_order = {m.agent_path: i for i, m in enumerate(target_members)}
|
|
7675
7705
|
|
|
7676
7706
|
async def process_agent_member(member):
|
|
7677
7707
|
"""Process a single member's response (DO NOT save messages here)"""
|
|
@@ -7704,19 +7734,64 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7704
7734
|
my_role = {"owner": "群主", "admin": "管理员"}.get(member.role, "成员")
|
|
7705
7735
|
my_desc = agent_cfg.get("description", "") if agent_cfg else ""
|
|
7706
7736
|
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
f"
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7737
|
+
# [v1.23.29] 构建近期群聊记录
|
|
7738
|
+
recent_msgs = mgr.get_messages(gid, limit=10)
|
|
7739
|
+
history_lines = []
|
|
7740
|
+
for rm in recent_msgs:
|
|
7741
|
+
rm_name = rm.sender_name or ("用户" if rm.sender == "user" else rm.agent_path)
|
|
7742
|
+
rm_time_str = _time.strftime("%H:%M", _time.localtime(rm.timestamp)) if rm.timestamp else ""
|
|
7743
|
+
history_lines.append(f"[{rm_time_str}] {rm_name}: {rm.content[:200]}")
|
|
7744
|
+
chat_history = "\n".join(reversed(history_lines))
|
|
7745
|
+
|
|
7746
|
+
# [v1.23.29] 确定 @提及信息
|
|
7747
|
+
is_mentioned = mentioned_all or member.agent_path in mentioned_agents
|
|
7748
|
+
at_info = ""
|
|
7749
|
+
if mentioned_all:
|
|
7750
|
+
at_info = "- 本次消息使用了 @所有人,请务必回复。\n"
|
|
7751
|
+
elif mentioned_agents:
|
|
7752
|
+
if is_mentioned:
|
|
7753
|
+
at_info = f"- 本次消息使用了 @{my_name},请务必回复。\n"
|
|
7754
|
+
else:
|
|
7755
|
+
at_info = "- 本次消息使用了 @其他成员,与你无关,请不要回复。\n"
|
|
7756
|
+
else:
|
|
7757
|
+
at_info = "- 本次消息没有 @任何人(广播消息),你可以根据内容决定是否回复。\n"
|
|
7758
|
+
|
|
7759
|
+
group_context = f"""## 群聊上下文
|
|
7760
|
+
|
|
7761
|
+
你正在参与一个群聊,以下是群的详细信息:
|
|
7762
|
+
|
|
7763
|
+
### 群信息
|
|
7764
|
+
- 群名称: {group.name}
|
|
7765
|
+
- 群描述: {group.description or '无'}
|
|
7766
|
+
- 当前发言者: 用户(群聊中的真人用户)
|
|
7767
|
+
|
|
7768
|
+
### 你的身份
|
|
7769
|
+
- 名称: {my_name}
|
|
7770
|
+
- 路径: {agent_path}
|
|
7771
|
+
- 角色: {my_role}{"(已禁言,但仍可接收消息)" if member.muted else ""}
|
|
7772
|
+
|
|
7773
|
+
### 群成员列表(共{len(group.members)}人)
|
|
7774
|
+
{chr(10).join(member_lines)}
|
|
7775
|
+
|
|
7776
|
+
### 沟通规则(重要)
|
|
7777
|
+
1. **@提及机制**: 用户发送消息时可以使用 @名称 来指定回复者
|
|
7778
|
+
- @某个Agent: 只有被@的Agent需要回复
|
|
7779
|
+
- @所有人 / @all: 所有成员都需要回复
|
|
7780
|
+
- 不@任何人(广播): 你自行判断是否需要回复
|
|
7781
|
+
2. **跨Agent沟通**: 你可以使用 `myagent-ai chat --agent <路径> --message "消息"` 命令向群内其他Agent发送消息。消息会出现在群聊中,对方下次被@时会看到。
|
|
7782
|
+
3. **回复方式**:
|
|
7783
|
+
- 直接回复用户的消息
|
|
7784
|
+
- 需要其他Agent协助时,使用 chat 命令沟通,不要假装代替其他Agent回答
|
|
7785
|
+
- 如需协调多个Agent,建议用户使用 @所有人
|
|
7786
|
+
{at_info}
|
|
7787
|
+
### 近期群聊记录(最近10条)
|
|
7788
|
+
{chat_history if chat_history else '(暂无历史消息)'}
|
|
7789
|
+
|
|
7790
|
+
### 重要提醒
|
|
7791
|
+
- 你只代表你自己发言,使用第一人称
|
|
7792
|
+
- 不要假装是其他Agent或代替其他Agent回答
|
|
7793
|
+
- 如果问题超出你的能力范围,建议用户@相关专家Agent
|
|
7794
|
+
- 如果需要其他Agent的信息,使用 `myagent-ai chat` 命令沟通"""
|
|
7720
7795
|
|
|
7721
7796
|
# 将群聊上下文追加到 agent_system_prompt
|
|
7722
7797
|
if agent_system_prompt:
|
|
@@ -7766,8 +7841,8 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7766
7841
|
"response": f"处理失败: {str(e)}",
|
|
7767
7842
|
}
|
|
7768
7843
|
|
|
7769
|
-
#
|
|
7770
|
-
tasks = [process_agent_member(m) for m in
|
|
7844
|
+
# 并行调用目标成员agent
|
|
7845
|
+
tasks = [process_agent_member(m) for m in target_members]
|
|
7771
7846
|
try:
|
|
7772
7847
|
gather_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
7773
7848
|
except Exception as e:
|
|
@@ -7793,6 +7868,26 @@ window.addEventListener('beforeunload', function() {{
|
|
|
7793
7868
|
key=lambda r: member_order.get(r.get("agent_path", ""), 999999)
|
|
7794
7869
|
)
|
|
7795
7870
|
|
|
7871
|
+
# [v1.23.29] 从 Agent 响应中提取 __CHAT_AGENT__ 标记,清理并保存跨Agent通信消息
|
|
7872
|
+
_chat_msg_pattern = _re.compile(r'__CHAT_AGENT__(.+?)\|(.+?)\|(.+?)__END__')
|
|
7873
|
+
for resp in final_responses:
|
|
7874
|
+
resp_text = resp.get("response", "")
|
|
7875
|
+
chat_matches = _chat_msg_pattern.findall(resp_text)
|
|
7876
|
+
if chat_matches:
|
|
7877
|
+
# 清理标记文本
|
|
7878
|
+
resp["response"] = _chat_msg_pattern.sub('', resp_text).strip()
|
|
7879
|
+
# 保存跨Agent通信消息到群聊
|
|
7880
|
+
for c_path, c_name, c_msg in chat_matches:
|
|
7881
|
+
chat_sys_msg = GroupMessage(
|
|
7882
|
+
group_id=gid,
|
|
7883
|
+
sender=resp.get("agent_path", ""),
|
|
7884
|
+
sender_name=resp.get("name", ""),
|
|
7885
|
+
sender_avatar=resp.get("avatar", "🤖"),
|
|
7886
|
+
content=f"💬 对 {c_name.strip()} 说: {c_msg.strip()}",
|
|
7887
|
+
msg_type="text",
|
|
7888
|
+
)
|
|
7889
|
+
mgr.add_message(chat_sys_msg)
|
|
7890
|
+
|
|
7796
7891
|
# Save agent messages sequentially in sorted order
|
|
7797
7892
|
for resp in final_responses:
|
|
7798
7893
|
agent_msg = GroupMessage(
|
package/web/ui/chat/chat.css
CHANGED
|
@@ -2671,3 +2671,59 @@ body.popout-mode #popoutBtn{display:none !important}
|
|
|
2671
2671
|
.md-table-wrapper td{padding:7px 12px;border:1px solid var(--border-light);color:var(--text);vertical-align:top;word-break:break-word}
|
|
2672
2672
|
.md-table-wrapper tr:nth-child(even){background:var(--bg3)}
|
|
2673
2673
|
.md-table-wrapper tr:hover{background:var(--accent-light)}
|
|
2674
|
+
|
|
2675
|
+
/* ══════════════════════════════════════════════════════
|
|
2676
|
+
── @ Mention Panel (v1.23.29) ──
|
|
2677
|
+
══════════════════════════════════════════════════════ */
|
|
2678
|
+
#atMentionPanel{
|
|
2679
|
+
background:var(--bg-secondary,var(--bg2));
|
|
2680
|
+
border:1px solid var(--border-color,var(--border));
|
|
2681
|
+
border-radius:8px;
|
|
2682
|
+
box-shadow:0 4px 16px rgba(0,0,0,.4);
|
|
2683
|
+
min-width:200px;
|
|
2684
|
+
max-width:320px;
|
|
2685
|
+
font-size:13px;
|
|
2686
|
+
padding:4px 0;
|
|
2687
|
+
}
|
|
2688
|
+
.at-mention-item{
|
|
2689
|
+
display:flex;
|
|
2690
|
+
align-items:center;
|
|
2691
|
+
gap:8px;
|
|
2692
|
+
padding:8px 12px;
|
|
2693
|
+
cursor:pointer;
|
|
2694
|
+
transition:background .15s;
|
|
2695
|
+
}
|
|
2696
|
+
.at-mention-item:hover,.at-mention-item.at-item-active{
|
|
2697
|
+
background:var(--bg-hover,rgba(255,255,255,.08));
|
|
2698
|
+
}
|
|
2699
|
+
.at-emoji{
|
|
2700
|
+
font-size:16px;
|
|
2701
|
+
width:24px;
|
|
2702
|
+
text-align:center;
|
|
2703
|
+
flex-shrink:0;
|
|
2704
|
+
}
|
|
2705
|
+
.at-name{
|
|
2706
|
+
font-weight:500;
|
|
2707
|
+
color:var(--text-primary,var(--text));
|
|
2708
|
+
}
|
|
2709
|
+
.at-path{
|
|
2710
|
+
font-size:11px;
|
|
2711
|
+
color:var(--text-secondary,var(--text3));
|
|
2712
|
+
margin-left:auto;
|
|
2713
|
+
}
|
|
2714
|
+
.at-all-btn{
|
|
2715
|
+
background:transparent;
|
|
2716
|
+
border:1px solid var(--accent);
|
|
2717
|
+
color:var(--accent);
|
|
2718
|
+
border-radius:6px;
|
|
2719
|
+
padding:4px 10px;
|
|
2720
|
+
font-size:12px;
|
|
2721
|
+
cursor:pointer;
|
|
2722
|
+
white-space:nowrap;
|
|
2723
|
+
transition:all .2s;
|
|
2724
|
+
flex-shrink:0;
|
|
2725
|
+
}
|
|
2726
|
+
.at-all-btn:hover{
|
|
2727
|
+
background:var(--accent);
|
|
2728
|
+
color:#fff;
|
|
2729
|
+
}
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -391,6 +391,26 @@ async function initChat() {
|
|
|
391
391
|
document.getElementById('sendBtn').disabled = !this.value.trim();
|
|
392
392
|
saveDraft();
|
|
393
393
|
});
|
|
394
|
+
// [v1.23.29] Initialize @ mention panel for group chat
|
|
395
|
+
if (typeof initAtMention === 'function') initAtMention();
|
|
396
|
+
// [v1.23.29] Create @all button (hidden by default, shown in group chat)
|
|
397
|
+
var sendBtn = document.getElementById('sendBtn');
|
|
398
|
+
if (sendBtn && sendBtn.parentElement && !document.getElementById('atAllBtn')) {
|
|
399
|
+
var atAllBtn = document.createElement('button');
|
|
400
|
+
atAllBtn.id = 'atAllBtn';
|
|
401
|
+
atAllBtn.className = 'at-all-btn';
|
|
402
|
+
atAllBtn.textContent = '@所有人';
|
|
403
|
+
atAllBtn.style.display = 'none';
|
|
404
|
+
atAllBtn.onclick = function() {
|
|
405
|
+
var input = document.getElementById('userInput');
|
|
406
|
+
if (input) {
|
|
407
|
+
input.value = input.value + '@所有人 ';
|
|
408
|
+
input.focus();
|
|
409
|
+
document.getElementById('sendBtn').disabled = false;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
sendBtn.parentElement.insertBefore(atAllBtn, sendBtn);
|
|
413
|
+
}
|
|
394
414
|
// Load task plan if in exec mode (panel stays hidden until tasks exist)
|
|
395
415
|
if (state.chatMode === 'exec') {
|
|
396
416
|
// Don't show task panel by default - it will appear when the LLM creates a task list
|
package/web/ui/chat/groupchat.js
CHANGED
|
@@ -154,6 +154,10 @@ async function selectGroup(gid) {
|
|
|
154
154
|
document.getElementById('activeAgentBadge').style.display = 'none';
|
|
155
155
|
document.getElementById('groupBackBtn').style.display = '';
|
|
156
156
|
document.getElementById('groupSettingsBtn').style.display = '';
|
|
157
|
+
|
|
158
|
+
// Show @all button for group chat
|
|
159
|
+
var atAllBtn = document.getElementById('atAllBtn');
|
|
160
|
+
if (atAllBtn) atAllBtn.style.display = '';
|
|
157
161
|
var _ccb = document.getElementById('clearChatBtn'); if (_ccb) _ccb.style.display = 'none';
|
|
158
162
|
var dot = document.querySelector('.main-title .dot');
|
|
159
163
|
if (dot) dot.style.display = 'none';
|
|
@@ -199,6 +203,8 @@ function exitGroupChat() {
|
|
|
199
203
|
document.getElementById('activeAgentBadge').style.display = '';
|
|
200
204
|
document.getElementById('groupBackBtn').style.display = 'none';
|
|
201
205
|
document.getElementById('groupSettingsBtn').style.display = 'none';
|
|
206
|
+
var atAllBtn = document.getElementById('atAllBtn');
|
|
207
|
+
if (atAllBtn) atAllBtn.style.display = 'none';
|
|
202
208
|
var _ccb2 = document.getElementById('clearChatBtn'); if (_ccb2) _ccb2.style.display = '';
|
|
203
209
|
var dot = document.querySelector('.main-title .dot');
|
|
204
210
|
if (dot) dot.style.display = '';
|
|
@@ -239,7 +245,8 @@ function _renderGroupMessagesInner() {
|
|
|
239
245
|
html = '<div class="welcome-card">'
|
|
240
246
|
+ '<h2><span class="emoji">' + escapeHtml((group && (group.avatar_emoji || group.emoji)) || '👥') + '</span>'
|
|
241
247
|
+ ' <span>群聊: ' + escapeHtml((group && group.name) || '') + '</span></h2>'
|
|
242
|
-
+ '<p class="subtitle"
|
|
248
|
+
+ '<p class="subtitle">输入 @ 选择要回复的 Agent,或直接发送消息广播给所有人</p>'
|
|
249
|
+
+ '<p class="subtitle" style="font-size:12px;color:var(--text-secondary)">支持 @Agent名 指定回复 | @所有人 全部回复 | myagent-ai chat 跨Agent沟通</p>'
|
|
243
250
|
+ '</div>';
|
|
244
251
|
container.innerHTML = html;
|
|
245
252
|
return;
|
|
@@ -593,6 +600,150 @@ async function confirmAddMembers() {
|
|
|
593
600
|
}
|
|
594
601
|
}
|
|
595
602
|
|
|
603
|
+
// ══════════════════════════════════════════════════════
|
|
604
|
+
// ── @ Mention Panel (@提及面板) ──
|
|
605
|
+
// ══════════════════════════════════════════════════════
|
|
606
|
+
|
|
607
|
+
var _atPanelVisible = false;
|
|
608
|
+
var _atPanelFiltered = [];
|
|
609
|
+
|
|
610
|
+
function initAtMention() {
|
|
611
|
+
var input = document.getElementById('userInput');
|
|
612
|
+
if (!input) return;
|
|
613
|
+
input.addEventListener('input', _onInputChangeForAt);
|
|
614
|
+
input.addEventListener('keydown', _onInputKeydownForAt);
|
|
615
|
+
// Close panel on click outside
|
|
616
|
+
document.addEventListener('click', function(e) {
|
|
617
|
+
if (!e.target.closest('#atMentionPanel') && !e.target.closest('#userInput')) {
|
|
618
|
+
_hideAtPanel();
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function _onInputChangeForAt(e) {
|
|
624
|
+
if (currentView !== 'group') { _hideAtPanel(); return; }
|
|
625
|
+
var val = e.target.value;
|
|
626
|
+
var pos = val.lastIndexOf('@');
|
|
627
|
+
if (pos < 0 || (pos > 0 && val[pos - 1] !== ' ' && val[pos - 1] !== '\n')) {
|
|
628
|
+
_hideAtPanel();
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
var query = val.substring(pos + 1).toLowerCase().split(' ')[0];
|
|
632
|
+
if (query.length > 20) { _hideAtPanel(); return; }
|
|
633
|
+
|
|
634
|
+
var group = groups.find(function(g) { return g.id === currentGroupId; });
|
|
635
|
+
if (!group || !group.members) { _hideAtPanel(); return; }
|
|
636
|
+
|
|
637
|
+
_atPanelFiltered = [];
|
|
638
|
+
// Add "所有人" option
|
|
639
|
+
var allMatch = !query || 'all'.indexOf(query) >= 0 || '所有人'.indexOf(query) >= 0 || '大家'.indexOf(query) >= 0;
|
|
640
|
+
if (allMatch) {
|
|
641
|
+
_atPanelFiltered.push({name: '所有人', path: '@all', emoji: '📢', isAll: true});
|
|
642
|
+
}
|
|
643
|
+
// Add members
|
|
644
|
+
for (var i = 0; i < group.members.length; i++) {
|
|
645
|
+
var m = group.members[i];
|
|
646
|
+
var mc = (state.agentsFlat || []).find(function(a) { return a.path === m.agent_path; });
|
|
647
|
+
var mName = (mc && mc.name) || m.agent_path;
|
|
648
|
+
var mEmoji = (mc && mc.avatar_emoji) || '🤖';
|
|
649
|
+
if (!query || mName.toLowerCase().indexOf(query) >= 0 || m.agent_path.toLowerCase().indexOf(query) >= 0) {
|
|
650
|
+
_atPanelFiltered.push({name: mName, path: m.agent_path, emoji: mEmoji});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (_atPanelFiltered.length === 0) { _hideAtPanel(); return; }
|
|
655
|
+
_showAtPanel(pos);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function _showAtPanel(atPos) {
|
|
659
|
+
var existing = document.getElementById('atMentionPanel');
|
|
660
|
+
if (!existing) {
|
|
661
|
+
existing = document.createElement('div');
|
|
662
|
+
existing.id = 'atMentionPanel';
|
|
663
|
+
document.body.appendChild(existing);
|
|
664
|
+
}
|
|
665
|
+
var html = '';
|
|
666
|
+
for (var i = 0; i < _atPanelFiltered.length; i++) {
|
|
667
|
+
var item = _atPanelFiltered[i];
|
|
668
|
+
var cls = i === 0 ? ' at-item-active' : '';
|
|
669
|
+
html += '<div class="at-mention-item' + cls + '" data-index="' + i + '" data-name="' + escapeHtml(item.name) + '" data-path="' + escapeHtml(item.path) + '">'
|
|
670
|
+
+ '<span class="at-emoji">' + item.emoji + '</span>'
|
|
671
|
+
+ '<span class="at-name">' + escapeHtml(item.name) + '</span>'
|
|
672
|
+
+ (item.isAll ? '' : '<span class="at-path">' + escapeHtml(item.path) + '</span>')
|
|
673
|
+
+ '</div>';
|
|
674
|
+
}
|
|
675
|
+
existing.innerHTML = html;
|
|
676
|
+
existing.style.display = 'block';
|
|
677
|
+
existing._atIndex = 0;
|
|
678
|
+
|
|
679
|
+
// Position near input
|
|
680
|
+
var input = document.getElementById('userInput');
|
|
681
|
+
var rect = input.getBoundingClientRect();
|
|
682
|
+
existing.style.position = 'fixed';
|
|
683
|
+
existing.style.left = Math.max(8, rect.left) + 'px';
|
|
684
|
+
existing.style.bottom = (window.innerHeight - rect.top + 4) + 'px';
|
|
685
|
+
existing.style.maxHeight = '200px';
|
|
686
|
+
existing.style.overflowY = 'auto';
|
|
687
|
+
existing.style.zIndex = '99999';
|
|
688
|
+
|
|
689
|
+
// Click handlers
|
|
690
|
+
var items = existing.querySelectorAll('.at-mention-item');
|
|
691
|
+
items.forEach(function(el) {
|
|
692
|
+
el.addEventListener('mousedown', function(e) {
|
|
693
|
+
e.preventDefault();
|
|
694
|
+
_selectAtItem(parseInt(el.dataset.index));
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
_atPanelVisible = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function _hideAtPanel() {
|
|
701
|
+
var panel = document.getElementById('atMentionPanel');
|
|
702
|
+
if (panel) panel.style.display = 'none';
|
|
703
|
+
_atPanelVisible = false;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function _selectAtItem(index) {
|
|
707
|
+
var item = _atPanelFiltered[index];
|
|
708
|
+
if (!item) return;
|
|
709
|
+
var input = document.getElementById('userInput');
|
|
710
|
+
var val = input.value;
|
|
711
|
+
var pos = val.lastIndexOf('@');
|
|
712
|
+
var mention = item.isAll ? '@所有人 ' : '@' + item.name + ' ';
|
|
713
|
+
input.value = val.substring(0, pos) + mention + val.substring(pos + 1 + (val.substring(pos + 1).split(' ')[0].length));
|
|
714
|
+
input.focus();
|
|
715
|
+
_hideAtPanel();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function _onInputKeydownForAt(e) {
|
|
719
|
+
if (!_atPanelVisible || !_atPanelFiltered.length) return;
|
|
720
|
+
var panel = document.getElementById('atMentionPanel');
|
|
721
|
+
if (e.key === 'ArrowDown') {
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
panel._atIndex = Math.min(panel._atIndex + 1, _atPanelFiltered.length - 1);
|
|
724
|
+
_highlightAtItem(panel);
|
|
725
|
+
} else if (e.key === 'ArrowUp') {
|
|
726
|
+
e.preventDefault();
|
|
727
|
+
panel._atIndex = Math.max(panel._atIndex - 1, 0);
|
|
728
|
+
_highlightAtItem(panel);
|
|
729
|
+
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
730
|
+
e.preventDefault();
|
|
731
|
+
_selectAtItem(panel._atIndex);
|
|
732
|
+
} else if (e.key === 'Escape') {
|
|
733
|
+
_hideAtPanel();
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function _highlightAtItem(panel) {
|
|
738
|
+
var items = panel.querySelectorAll('.at-mention-item');
|
|
739
|
+
items.forEach(function(el, i) {
|
|
740
|
+
el.classList.toggle('at-item-active', i === panel._atIndex);
|
|
741
|
+
});
|
|
742
|
+
// Scroll into view
|
|
743
|
+
var active = panel.querySelector('.at-item-active');
|
|
744
|
+
if (active) active.scrollIntoView({block: 'nearest'});
|
|
745
|
+
}
|
|
746
|
+
|
|
596
747
|
// ══════════════════════════════════════════════════════
|
|
597
748
|
// ── Group Chat Send (群聊消息发送) ──
|
|
598
749
|
// ══════════════════════════════════════════════════════
|