myagent-ai 1.23.28 → 1.23.29

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.
@@ -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,6 +152,7 @@ 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>
@@ -246,6 +246,31 @@ class ToolDispatcher:
246
246
  if not media_result.get("success"):
247
247
  result["output"] += f"\n[视频播放失败: {media_result.get('error', '')}]"
248
248
 
249
+ # [v1.23.29] 检测 __CHAT_AGENT__ 标记 — CLI chat 命令输出此标记
250
+ # 格式: __CHAT_AGENT__agent_path|agent_name|message__END__
251
+ chat_markers = _re.findall(r'__CHAT_AGENT__(.+?)\|(.+?)\|(.+?)__END__', clean_output)
252
+ if chat_markers:
253
+ clean_output = _re.sub(r'__CHAT_AGENT__.+?__END__\n?', '', clean_output).strip()
254
+ result["output"] = clean_output
255
+ for chat_agent_path, chat_agent_name, chat_msg in chat_markers:
256
+ # Store as a chat agent event in sent_files for persistence
257
+ if sent_files is not None:
258
+ sent_files.append({
259
+ "_type": "chat_agent",
260
+ "target_agent": chat_agent_path.strip(),
261
+ "target_name": chat_agent_name.strip(),
262
+ "message": chat_msg.strip(),
263
+ })
264
+ # Emit SSE event for frontend display
265
+ try:
266
+ await self._emit_sse("v2_chat_agent", {
267
+ "target_agent": chat_agent_path.strip(),
268
+ "target_name": chat_agent_name.strip(),
269
+ "message": chat_msg.strip(),
270
+ }, stream_callback)
271
+ except Exception:
272
+ pass
273
+
249
274
  return result
250
275
 
251
276
  async def _exec_recall_memory(self, params: Dict, task_id: str) -> Dict:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.23.28",
3
+ "version": "1.23.29",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
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. 广播到所有非禁言成员agent,并行处理
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(active_members)}
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
- group_context = (
7708
- f"## 群聊上下文\n"
7709
- f"- 群名称: {group.name}\n"
7710
- f"- 群ID: {group.id}\n"
7711
- f"- 群描述: {group.description}\n"
7712
- f"- 当前发言者: 用户\n"
7713
- f"- 你的身份: {my_name} ({my_role})"
7714
- + (f" — {my_desc}" if my_desc else "")
7715
- + f"\n- 群成员 ({len(group.members)}人):\n"
7716
- + "\n".join(member_lines)
7717
- + "\n\n注意:你只代表自己发言,回复时使用第一人称。"
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
- # 并行调用所有成员agent
7770
- tasks = [process_agent_member(m) for m in active_members]
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(
@@ -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
+ }
@@ -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
@@ -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">向所有成员发送消息,每个 Agent 会分别回复</p>'
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
  // ══════════════════════════════════════════════════════