acp-ts 1.2.1 → 1.2.3

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/dist/server.js CHANGED
@@ -53,33 +53,40 @@ const messagestore_1 = require("./messagestore");
53
53
  let globalApiUrl = '';
54
54
  let globalDataDir = '';
55
55
  const messageStores = new Map();
56
- // ============================================================
57
- // Browser ↔ Server WebSocket (real-time group message push)
58
- // ============================================================
59
- const browserWsClients = new Set();
56
+ const browserWsClients = new Map();
57
+ /**
58
+ * 向绑定了指定 aid 的浏览器 WS 客户端推送消息
59
+ */
60
+ function pushToAid(aid, data) {
61
+ const payload = JSON.stringify(data);
62
+ for (const [ws, client] of browserWsClients) {
63
+ if (client.aid === aid && ws.readyState === ws_1.default.OPEN) {
64
+ ws.send(payload);
65
+ }
66
+ }
67
+ }
60
68
  /**
61
69
  * 向所有已连接的浏览器 WS 客户端广播消息
62
70
  */
63
71
  function broadcastToBrowser(data) {
64
72
  const payload = JSON.stringify(data);
65
- for (const client of browserWsClients) {
66
- if (client.readyState === ws_1.default.OPEN) {
67
- client.send(payload);
73
+ for (const [ws] of browserWsClients) {
74
+ if (ws.readyState === ws_1.default.OPEN) {
75
+ ws.send(payload);
68
76
  }
69
77
  }
70
78
  }
71
79
  let agentCP = null;
72
- let currentAid = '';
73
80
  const MAX_AIDS = 10;
74
81
  const aidInstances = new Map();
75
82
  function getActiveInstance() {
76
- return aidInstances.get(currentAid) || null;
83
+ return null; // legacy stub — use aidInstances.get(aid) directly
77
84
  }
78
85
  // 正在上线中的 Promise 缓存,防止并发调用重复上线
79
86
  const onlinePendingMap = new Map();
80
87
  // 确保指定 AID 在线,如果不在线则自动上线,返回实例
81
88
  async function ensureOnline(targetAid) {
82
- const aid = targetAid || currentAid;
89
+ const aid = targetAid;
83
90
  if (!aid)
84
91
  throw new Error('请先选择 AID');
85
92
  const existing = aidInstances.get(aid);
@@ -99,6 +106,8 @@ async function ensureOnline(targetAid) {
99
106
  }
100
107
  }
101
108
  async function doEnsureOnline(aid) {
109
+ // 确保该 AID 的会话数据已从磁盘加载
110
+ await ensureMessageStoreLoaded(aid);
102
111
  // 自动上线
103
112
  const cp = new agentcp_1.AgentCP(globalApiUrl, '', globalDataDir || undefined, { persistMessages: true, persistGroupMessages: true });
104
113
  await cp.loadAid(aid);
@@ -128,9 +137,7 @@ async function doEnsureOnline(aid) {
128
137
  hb.onInvite((invite) => {
129
138
  console.log(`[Server] 收到邀请: ${JSON.stringify(invite)}`);
130
139
  const session = getMessageStoreForAid(aid).getOrCreateSession(invite.sessionId, invite.inviteCode, invite.inviterAgentId, 'incoming', aid);
131
- if (!activeSessionId) {
132
- activeSessionId = session.sessionId;
133
- }
140
+ pushToAid(aid, { type: 'sessions_updated' });
134
141
  if (instance.agentWS) {
135
142
  instance.agentWS.acceptInviteFromHeartbeat(invite.sessionId, invite.inviterAgentId, invite.inviteCode);
136
143
  }
@@ -209,13 +216,14 @@ async function doEnsureOnline(aid) {
209
216
  }
210
217
  if (msgSessionId && getMessageStoreForAid(aid).hasSession(msgSessionId)) {
211
218
  getMessageStoreForAid(aid).addMessageToSession(msgSessionId, { type: 'received', content, from, timestamp: Date.now() });
219
+ pushToAid(aid, { type: 'p2p_message', sessionId: msgSessionId, message: { type: 'received', content, from, timestamp: Date.now() } });
220
+ pushToAid(aid, { type: 'sessions_updated' });
212
221
  }
213
222
  else if (msgSessionId && from) {
214
223
  getMessageStoreForAid(aid).getOrCreateSession(msgSessionId, '', from, 'incoming', aid);
215
- if (!activeSessionId) {
216
- activeSessionId = msgSessionId;
217
- }
218
224
  getMessageStoreForAid(aid).addMessageToSession(msgSessionId, { type: 'received', content, from, timestamp: Date.now() });
225
+ pushToAid(aid, { type: 'p2p_message', sessionId: msgSessionId, message: { type: 'received', content, from, timestamp: Date.now() } });
226
+ pushToAid(aid, { type: 'sessions_updated' });
219
227
  }
220
228
  });
221
229
  ws.onStatusChange((status) => {
@@ -366,6 +374,14 @@ async function ensureGroupClient(instance) {
366
374
  event: evt,
367
375
  });
368
376
  },
377
+ onDutyDispatch(groupId, context) {
378
+ console.log(`[Group] onDutyDispatch: group=${groupId} original_msg_id=${context.original_msg_id} sender=${context.sender_id}`);
379
+ broadcastToBrowser({
380
+ type: 'duty_dispatch',
381
+ group_id: groupId,
382
+ context,
383
+ });
384
+ },
369
385
  });
370
386
  // 同步群组列表(如未同步过)
371
387
  if (!instance.groupListSynced) {
@@ -554,7 +570,7 @@ function getAidMdOptionsForAid(aid) {
554
570
  return loadAidMdOptions()[aid] || {};
555
571
  }
556
572
  // 消息与会话管理 — 每个 AID 独立 MessageStore
557
- let activeSessionId = null;
573
+ const messageStoreLoaded = new Set();
558
574
  function getMessageStoreForAid(aid) {
559
575
  let store = messageStores.get(aid);
560
576
  if (!store) {
@@ -566,10 +582,13 @@ function getMessageStoreForAid(aid) {
566
582
  }
567
583
  return store;
568
584
  }
569
- function getMessageStore() {
570
- if (!currentAid)
571
- throw new Error('请先选择 AID');
572
- return getMessageStoreForAid(currentAid);
585
+ async function ensureMessageStoreLoaded(aid) {
586
+ const store = getMessageStoreForAid(aid);
587
+ if (!messageStoreLoaded.has(aid)) {
588
+ await store.loadSessionsForAid(aid);
589
+ messageStoreLoaded.add(aid);
590
+ }
591
+ return store;
573
592
  }
574
593
  // HTML 页面
575
594
  const indexHtml = `<!DOCTYPE html>
@@ -581,21 +600,26 @@ const indexHtml = `<!DOCTYPE html>
581
600
  <title>ACP 身份管理</title>
582
601
  <style>
583
602
  * { box-sizing: border-box; margin: 0; padding: 0; }
584
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
585
- .container { background: white; padding: 32px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); max-width: 560px; width: 100%; }
586
- h1 { color: #333; margin-bottom: 24px; text-align: center; font-size: 22px; }
587
- .hint { text-align: center; color: #999; font-size: 13px; margin-bottom: 20px; }
588
- .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 12px; }
603
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #f0f4ff 0%, #e8edf5 100%); min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; padding: 40px 20px; }
604
+ .container { background: white; padding: 0; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.08); max-width: 560px; width: 100%; overflow: hidden; }
605
+ .page-header { background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); padding: 28px 32px 22px; color: white; text-align: center; }
606
+ .page-header h1 { font-size: 20px; font-weight: 600; margin-bottom: 12px; letter-spacing: 0.5px; }
607
+ .nav-links { display: flex; justify-content: center; gap: 8px; }
608
+ .nav-links a { color: rgba(255,255,255,0.85); text-decoration: none; font-size: 12px; padding: 4px 12px; border-radius: 20px; border: 1px solid rgba(255,255,255,0.25); transition: all 0.2s; }
609
+ .nav-links a:hover { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.5); }
610
+ .page-body { padding: 24px 32px 32px; }
611
+ .hint { text-align: center; color: #9ca3af; font-size: 13px; margin-bottom: 20px; }
612
+ .create-section { margin-bottom: 24px; display: flex; flex-direction: column; gap: 10px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 18px; }
589
613
  .create-section .aid-input-row { display: flex; gap: 8px; align-items: center; }
590
- .create-section .aid-input-row input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; min-width: 0; }
591
- .create-section .aid-input-row input:focus { outline: none; border-color: #007bff; }
592
- .create-section .aid-input-row .dot-separator { color: #999; font-size: 16px; flex-shrink: 0; }
614
+ .create-section .aid-input-row input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; min-width: 0; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
615
+ .create-section .aid-input-row input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
616
+ .create-section .aid-input-row .dot-separator { color: #9ca3af; font-size: 16px; flex-shrink: 0; }
593
617
  .create-section .aid-input-row select {
594
618
  padding: 10px 30px 10px 14px;
595
- border: 1px solid #ddd;
619
+ border: 1px solid #e2e8f0;
596
620
  border-radius: 8px;
597
621
  font-size: 14px;
598
- background: white;
622
+ background: #fff;
599
623
  flex-shrink: 0;
600
624
  cursor: pointer;
601
625
  appearance: none;
@@ -605,48 +629,63 @@ const indexHtml = `<!DOCTYPE html>
605
629
  background-repeat: no-repeat;
606
630
  background-position: right 10px top 50%;
607
631
  background-size: 10px auto;
632
+ transition: border-color 0.2s, box-shadow 0.2s;
608
633
  }
609
- .create-section .aid-input-row select:focus { outline: none; border-color: #007bff; }
634
+ .create-section .aid-input-row select:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
610
635
  .create-section .extra-fields { display: flex; gap: 8px; }
611
- .create-section .extra-fields input { flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; min-width: 0; }
612
- .create-section .extra-fields input:focus { outline: none; border-color: #007bff; }
613
- .btn { display: block; width: 100%; padding: 12px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; transition: background 0.2s; }
614
- .btn-primary { background: #007bff; color: white; }
615
- .btn-primary:hover { background: #0056b3; }
636
+ .create-section .extra-fields input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; min-width: 0; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; }
637
+ .create-section .extra-fields input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37,99,235,0.1); }
638
+ .btn { display: block; width: 100%; padding: 11px; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
639
+ .btn-primary { background: linear-gradient(135deg, #2563eb, #1d4ed8); color: white; }
640
+ .btn-primary:hover { background: linear-gradient(135deg, #1d4ed8, #1e40af); box-shadow: 0 2px 8px rgba(37,99,235,0.3); }
616
641
  .btn-sm { display: inline-block; width: auto; padding: 6px 14px; font-size: 13px; border-radius: 6px; }
617
- .btn-success { background: #28a745; color: white; }
618
- .btn-success:hover { background: #218838; }
619
- .btn-danger { background: #dc3545; color: white; }
620
- .btn-danger:hover { background: #c82333; }
621
- .btn-outline { background: white; color: #007bff; border: 1px solid #007bff; }
622
- .btn-outline:hover { background: #e7f1ff; }
623
- .btn-outline.active { background: #007bff; color: white; }
624
- .btn:disabled { background: #ccc; cursor: not-allowed; border-color: #ccc; color: #fff; }
642
+ .btn-success { background: #10b981; color: white; }
643
+ .btn-success:hover { background: #059669; }
644
+ .btn-danger { background: #ef4444; color: white; }
645
+ .btn-danger:hover { background: #dc2626; }
646
+ .btn-outline { background: white; color: #2563eb; border: 1px solid #2563eb; }
647
+ .btn-outline:hover { background: #eff6ff; }
648
+ .btn-outline.active { background: #2563eb; color: white; }
649
+ .btn:disabled { background: #d1d5db; cursor: not-allowed; border-color: #d1d5db; color: #fff; }
625
650
  .aid-list { margin-bottom: 24px; }
626
- .aid-card { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px; padding: 16px; margin-bottom: 12px; transition: border-color 0.2s; display: flex; align-items: stretch; gap: 12px; }
627
- .aid-card.current { border-color: #007bff; background: #f0f7ff; }
651
+ .aid-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 16px; margin-bottom: 10px; transition: all 0.2s; display: flex; align-items: stretch; gap: 12px; }
652
+ .aid-card:hover { border-color: #93c5fd; box-shadow: 0 2px 12px rgba(37,99,235,0.06); }
653
+ .aid-card.current { border-color: #2563eb; background: #eff6ff; }
628
654
  .aid-card-left { flex: 1; min-width: 0; }
629
655
  .aid-card-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex-shrink: 0; justify-content: center; }
630
656
  .aid-card-header { margin-bottom: 10px; }
631
- .aid-name { font-family: monospace; font-size: 13px; color: #333; word-break: break-all; }
632
- .copy-btn { background: none; border: none; color: #6c757d; cursor: pointer; font-size: 12px; padding: 2px 6px; }
633
- .copy-btn:hover { color: #333; }
657
+ .aid-name { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: #1f2937; word-break: break-all; }
658
+ .copy-btn { background: none; border: none; color: #9ca3af; cursor: pointer; font-size: 12px; padding: 2px 6px; transition: color 0.2s; }
659
+ .copy-btn:hover { color: #2563eb; }
634
660
  .aid-card-status { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
635
661
  .badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
636
- .badge-success { background: #d4edda; color: #155724; }
637
- .badge-warning { background: #fff3cd; color: #856404; }
638
- .badge-danger { background: #f8d7da; color: #721c24; }
639
- .badge-info { background: #d1ecf1; color: #0c5460; }
640
- .badge-current { background: #007bff; color: white; }
662
+ .badge-success { background: #d1fae5; color: #065f46; }
663
+ .badge-warning { background: #fef3c7; color: #92400e; }
664
+ .badge-danger { background: #fee2e2; color: #991b1b; }
665
+ .badge-info { background: #dbeafe; color: #1e40af; }
666
+ .badge-current { background: #2563eb; color: white; }
641
667
  .aid-card-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
642
- .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; font-size: 14px; display: none; z-index: 1000; }
643
- .status.success { display: block; background: #d4edda; color: #155724; }
644
- .status.error { display: block; background: #f8d7da; color: #721c24; }
668
+ .status { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 10px; font-size: 14px; display: none; z-index: 1000; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
669
+ .status.success { display: block; background: #d1fae5; color: #065f46; }
670
+ .status.error { display: block; background: #fee2e2; color: #991b1b; }
671
+ @media (max-width: 480px) {
672
+ body { padding: 16px 8px; }
673
+ .page-header { padding: 22px 18px 18px; }
674
+ .page-body { padding: 18px 16px 24px; }
675
+ .create-section { padding: 14px; }
676
+ }
645
677
  </style>
646
678
  </head>
647
679
  <body>
648
680
  <div class="container">
649
- <h1>ACP 身份管理</h1>
681
+ <div class="page-header">
682
+ <h1>ACP 身份管理</h1>
683
+ <div class="nav-links">
684
+ <a href="https://agentunion.net" target="_blank">AgentUnion排行榜</a>
685
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank">ACP 开源GitHub</a>
686
+ </div>
687
+ </div>
688
+ <div class="page-body">
650
689
  <div class="hint" id="hint">最多注册 10 个 AID</div>
651
690
 
652
691
  <div class="create-section" id="createSection">
@@ -665,10 +704,11 @@ const indexHtml = `<!DOCTYPE html>
665
704
  <div class="aid-list" id="aidList"></div>
666
705
 
667
706
  <div class="status" id="status"></div>
707
+ </div>
668
708
  </div>
669
709
 
670
710
  <script>
671
- let aidData = { currentAid: '', aidList: [], aidStatus: [], apiUrl: '' };
711
+ let aidData = { aidList: [], aidStatus: [], apiUrl: '' };
672
712
 
673
713
  async function loadAidInfo() {
674
714
  try {
@@ -715,11 +755,9 @@ const indexHtml = `<!DOCTYPE html>
715
755
  }
716
756
 
717
757
  list.innerHTML = aidData.aidStatus.map(function(item) {
718
- var isCurrent = item.aid === aidData.currentAid;
719
- var cardClass = 'aid-card' + (isCurrent ? ' current' : '');
758
+ var cardClass = 'aid-card';
720
759
 
721
760
  var badges = '';
722
- if (isCurrent) badges += '<span class="badge badge-current">当前</span>';
723
761
  if (item.online) badges += '<span class="badge badge-success">已上线</span>';
724
762
  if (item.keysExist && item.certValid) {
725
763
  badges += '<span class="badge badge-info">密钥有效</span>';
@@ -730,11 +768,7 @@ const indexHtml = `<!DOCTYPE html>
730
768
  }
731
769
 
732
770
  var actions = '';
733
- if (!isCurrent) {
734
- actions += '<button class="btn btn-sm btn-outline" onclick="selectAid(\\'' + escapeAttr(item.aid) + '\\')">选为当前</button>';
735
- }
736
- // 上线并进入聊天(合并按钮)
737
- if (isCurrent && item.keysExist && item.certValid) {
771
+ if (item.keysExist && item.certValid) {
738
772
  if (item.online) {
739
773
  actions += '<button class="btn btn-sm btn-success" onclick="enterChat(\\'' + escapeAttr(item.aid) + '\\')">进入聊天</button>';
740
774
  actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
@@ -742,9 +776,6 @@ const indexHtml = `<!DOCTYPE html>
742
776
  actions += '<button class="btn btn-sm btn-success" id="goBtn_' + escapeAttr(item.aid) + '" onclick="goOnlineAndChat(\\'' + escapeAttr(item.aid) + '\\')">上线并进入</button>';
743
777
  }
744
778
  }
745
- if (!isCurrent && item.online) {
746
- actions += '<button class="btn btn-sm btn-danger" onclick="goOffline(\\'' + escapeAttr(item.aid) + '\\')">下线</button>';
747
- }
748
779
 
749
780
  return '<div class="' + cardClass + '">' +
750
781
  '<div class="aid-card-left">' +
@@ -938,7 +969,7 @@ const chatHtml = `<!DOCTYPE html>
938
969
  .bubble { padding:10px 14px; border-radius:12px; font-size:14px; line-height:1.5; word-wrap:break-word; box-shadow:0 1px 2px rgba(0,0,0,0.05); }
939
970
  .message.sent .bubble { background:var(--sent); color:#fff; border-bottom-right-radius:2px; }
940
971
  .message.received .bubble { background:var(--recv-bg); color:var(--t1); border-bottom-left-radius:2px; border:1px solid var(--border); }
941
- .msg-meta { font-size:10px; color:var(--t2); margin-top:3px; padding:0 4px; }
972
+ .msg-meta { font-size:10px; color:var(--t2); margin-bottom:3px; padding:0 4px; }
942
973
 
943
974
  .input-area { padding:12px 16px; background:#fff; border-top:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
944
975
  .input-area input { flex:1; padding:10px 14px; border-radius:20px; border:1px solid var(--border); font-size:14px; background:#f9fafb; }
@@ -974,15 +1005,18 @@ const chatHtml = `<!DOCTYPE html>
974
1005
  .bubble code { background:rgba(0,0,0,0.1); padding:2px 4px; border-radius:3px; font-family:monospace; font-size:0.9em; }
975
1006
  .bubble pre { background:#2d2d2d; color:#fff; padding:12px; border-radius:6px; overflow-x:auto; margin:8px 0; }
976
1007
  .bubble pre code { background:transparent; padding:0; color:inherit; border-radius:0; }
977
- .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; }
1008
+ .messages { flex:1; padding:16px; overflow-y:auto; display:flex; flex-direction:column; gap:12px; position:relative; }
978
1009
  .message { display:flex; flex-direction:row; max-width:85%; gap:8px; }
979
1010
  .message.sent { align-self:flex-end; flex-direction:row-reverse; }
980
1011
  .message.received { align-self:flex-start; }
981
- .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:4px; }
1012
+ .msg-avatar { width:40px; height:40px; border-radius:50%; object-fit:cover; flex-shrink:0; box-shadow:0 1px 2px rgba(0,0,0,0.1); margin-top:2px; }
982
1013
  .msg-content { display:flex; flex-direction:column; max-width:100%; min-width:0; }
983
1014
  .message.sent .msg-content { align-items:flex-end; }
984
1015
  .message.received .msg-content { align-items:flex-start; }
985
1016
  @media (min-width: 1024px) { .message { max-width: 70%; } }
1017
+ .new-msg-tip { position:sticky; bottom:8px; align-self:center; background:var(--primary); color:#fff; padding:6px 18px; border-radius:20px; font-size:12px; cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,0.15); z-index:10; display:none; transition:opacity 0.2s; animation:newMsgBounce 0.3s ease; }
1018
+ .new-msg-tip:hover { background:var(--primary-h); }
1019
+ @keyframes newMsgBounce { 0%{transform:translateY(10px);opacity:0} 100%{transform:translateY(0);opacity:1} }
986
1020
 
987
1021
  @media (max-width:768px) {
988
1022
  .sidebar { position:absolute; height:100%; z-index:20; width:280px; }
@@ -1052,6 +1086,8 @@ const chatHtml = `<!DOCTYPE html>
1052
1086
  <div class="chat-title" id="chatTitle">未选择会话</div>
1053
1087
  </div>
1054
1088
  <div class="aid-select-wrap">
1089
+ <a href="https://agentunion.net" target="_blank" class="manage-btn" title="AgentUnion排行榜">AgentUnion排行榜</a>
1090
+ <a href="https://github.com/auliwenjiang/agentcp" target="_blank" class="manage-btn" title="ACP 开源GitHub">ACP 开源GitHub</a>
1055
1091
  <a href="/" class="manage-btn" title="ACP 身份管理">
1056
1092
  <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> 身份管理
1057
1093
  </a>
@@ -1082,6 +1118,7 @@ const chatHtml = `<!DOCTYPE html>
1082
1118
  <div style="font-size:14px;font-weight:500;color:#64748b;margin-bottom:4px;">ACP Agent 安全通信</div>
1083
1119
  <div style="font-size:12px;color:#94a3b8;">选择或创建一个会话,开始点对点加密聊天</div>
1084
1120
  </div>
1121
+ <div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()">↓ 有新消息</div>
1085
1122
  </div>
1086
1123
  <div class="input-area">
1087
1124
  <input type="text" id="messageInput" placeholder="输入消息..." onkeypress="if(event.key==='Enter')sendMessage()">
@@ -1105,7 +1142,7 @@ const chatHtml = `<!DOCTYPE html>
1105
1142
  <div class="modal">
1106
1143
  <h3>创建群组</h3>
1107
1144
  <input type="text" id="groupNameInput" placeholder="输入群组名称">
1108
- <textarea id="groupDescInput" placeholder="输入群组描述(可选)" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;margin-bottom:16px;font-size:14px;resize:vertical;min-height:60px;font-family:inherit;"></textarea>
1145
+ <textarea id="groupDescInput" placeholder="输入群组描述(必填)" style="width:100%;padding:10px;border:1px solid var(--border);border-radius:8px;margin-bottom:16px;font-size:14px;resize:vertical;min-height:60px;font-family:inherit;"></textarea>
1109
1146
  <div style="margin-bottom:16px;">
1110
1147
  <label style="font-size:13px;color:var(--t2);margin-bottom:8px;display:block;">群组类型</label>
1111
1148
  <div style="display:flex;gap:12px;">
@@ -1158,7 +1195,7 @@ const chatHtml = `<!DOCTYPE html>
1158
1195
  </div>
1159
1196
  </div>
1160
1197
  <script>
1161
- var S = { aid:'', sid:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false, tab:'p2p', activeGroupId:null, groups:[], groupMsgs:[], groupTargetAid:'', isGroupCreator:false };
1198
+ var S = { aid:'', sid:null, sessionId:null, sessions:[], status:'disconnected', expanded:{}, sidebarOpen:true, aidList:[], closed:false, tab:'p2p', activeGroupId:null, groups:[], groupMsgs:[], groupTargetAid:'', isGroupCreator:false };
1162
1199
  var D = {};
1163
1200
  var agentInfoCache = {};
1164
1201
  function $(id){ return document.getElementById(id); }
@@ -1180,12 +1217,12 @@ const chatHtml = `<!DOCTYPE html>
1180
1217
  e.stopPropagation();
1181
1218
  if(!confirm('确认删除该会话?')) return;
1182
1219
  try {
1183
- var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId }) });
1220
+ var r = await fetch('/api/sessions/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: sessionId, aid: S.aid }) });
1184
1221
  var d = await r.json();
1185
1222
  if(d.success){
1186
- if(S.sid === sessionId){ S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1223
+ if(S.sid === sessionId){ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML=''; D.input.disabled=false; }
1187
1224
  D.sList.dataset.s=''; // force update
1188
- poll();
1225
+ loadSessions();
1189
1226
  } else { alert(d.error || '删除失败'); }
1190
1227
  } catch(err){ alert('删除失败: ' + err.message); }
1191
1228
  }
@@ -1194,30 +1231,43 @@ const chatHtml = `<!DOCTYPE html>
1194
1231
  e.stopPropagation();
1195
1232
  if(!confirm('确认删除与 ' + peerAid + ' 的所有会话?')) return;
1196
1233
  try {
1197
- var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid }) });
1234
+ var r = await fetch('/api/peers/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ peerAid: peerAid, aid: S.aid }) });
1198
1235
  var d = await r.json();
1199
1236
  if(d.success){
1200
- S.sid = null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1237
+ S.sid = null; S.sessionId=null; D.title.textContent='未选择会话'; D.msgs.innerHTML='';
1201
1238
  D.sList.dataset.s=''; // force update
1202
- poll();
1239
+ loadSessions();
1203
1240
  } else { alert(d.error || '删除失败'); }
1204
1241
  } catch(err){ alert('删除失败: ' + err.message); }
1205
1242
  }
1206
1243
 
1207
- function initDom(){ D.myAid=$('myAid'); D.sList=$('sessionList'); D.title=$('chatTitle'); D.msgs=$('messages'); D.input=$('messageInput'); D.sendBtn=$('sendBtn'); D.dot=$('statusDot'); D.modal=$('modal'); D.tInput=$('targetAidInput'); D.cBtn=$('connectBtn'); D.sidebar=$('sidebar'); D.aidSel=$('aidSelect'); D.aidDot=$('aidOnlineDot'); D.aidStatusToggle=$('aidStatusToggle'); D.aidStatusText=$('aidStatusText'); D.p2pPanel=$('p2pPanel'); D.groupPanel=$('groupPanel'); D.groupList=$('groupList'); D.groupInfoBar=$('groupInfoBar'); D.groupInfoText=$('groupInfoText'); D.tabP2P=$('tabP2P'); D.tabGroup=$('tabGroup'); D.encryptBanner=$('encryptBanner'); }
1244
+ function initDom(){ D.myAid=$('myAid'); D.sList=$('sessionList'); D.title=$('chatTitle'); D.msgs=$('messages'); D.input=$('messageInput'); D.sendBtn=$('sendBtn'); D.dot=$('statusDot'); D.modal=$('modal'); D.tInput=$('targetAidInput'); D.cBtn=$('connectBtn'); D.sidebar=$('sidebar'); D.aidSel=$('aidSelect'); D.aidDot=$('aidOnlineDot'); D.aidStatusToggle=$('aidStatusToggle'); D.aidStatusText=$('aidStatusText'); D.p2pPanel=$('p2pPanel'); D.groupPanel=$('groupPanel'); D.groupList=$('groupList'); D.groupInfoBar=$('groupInfoBar'); D.groupInfoText=$('groupInfoText'); D.tabP2P=$('tabP2P'); D.tabGroup=$('tabGroup'); D.encryptBanner=$('encryptBanner'); D.newMsgTip=$('newMsgTip'); }
1245
+
1246
+ function isAtBottom(){ return D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150; }
1247
+ function scrollToBottom(){ D.msgs.scrollTop=D.msgs.scrollHeight; hideNewMsgTip(); }
1248
+ function showNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='block'; }
1249
+ function hideNewMsgTip(){ if(D.newMsgTip) D.newMsgTip.style.display='none'; }
1208
1250
 
1209
1251
  async function init(){
1210
1252
  initDom();
1253
+ // 监听滚动,用户滚到底部时自动隐藏新消息提示
1254
+ D.msgs.addEventListener('scroll',function(){ if(isAtBottom()) hideNewMsgTip(); });
1211
1255
  try {
1212
1256
  var r = await fetch('/api/aid'); var d = await r.json();
1213
- if(d.currentAid){
1214
- S.aid=d.currentAid; D.myAid.textContent='我的身份: '+d.currentAid; D.myAid.title=d.currentAid;
1215
- // 填充 AID 切换下拉
1216
- S.aidList=d.aidStatus||[];
1257
+ S.aidList=d.aidStatus||[];
1258
+ if(S.aidList.length){
1259
+ // 优先从当前标签页恢复,再 fallback 到全局默认
1260
+ var saved=sessionStorage.getItem('selectedAid')||localStorage.getItem('selectedAid');
1261
+ var found=saved&&S.aidList.find(function(a){ return a.aid===saved; });
1262
+ S.aid=(found?saved:S.aidList[0].aid)||'';
1263
+ if(S.aid) sessionStorage.setItem('selectedAid',S.aid);
1264
+ }
1265
+ if(S.aid){
1266
+ D.myAid.textContent='我的身份: '+S.aid; D.myAid.title=S.aid;
1217
1267
  renderAidSelect();
1218
- // 初始加载一次ws状态,后续通过WebSocket推送更新
1219
- fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1220
- poll(); setInterval(poll,1000);
1268
+ connectGroupWs();
1269
+ fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1270
+ loadSessions();
1221
1271
  } else { window.location.href='/'; }
1222
1272
  } catch(e){ console.error(e); }
1223
1273
  }
@@ -1239,20 +1289,28 @@ const chatHtml = `<!DOCTYPE html>
1239
1289
 
1240
1290
  async function switchAid(aid){
1241
1291
  if(aid===S.aid) return;
1242
- try {
1243
- var r=await fetch('/api/aid/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1244
- var d=await r.json();
1245
- if(!d.success) return;
1246
- // 检查是否在线,不在线则自动上线
1247
- var r2=await fetch('/api/aid'); var d2=await r2.json();
1248
- var aidList=d2.aidStatus||[];
1249
- var info=aidList.find(function(a){ return a.aid===aid; });
1250
- if(!info || !info.online){
1292
+ S.aid=aid;
1293
+ S.sid=null; S.sessionId=null;
1294
+ _groupInited=false; // 切换 aid 后需重新初始化群组客户端
1295
+ localStorage.setItem('selectedAid',aid);
1296
+ sessionStorage.setItem('selectedAid',aid);
1297
+ D.myAid.textContent='我的身份: '+aid; D.myAid.title=aid;
1298
+ renderAidSelect();
1299
+ // 通知服务端本标签页绑定的 aid(WS 已连接则立即发,否则 onopen 会发)
1300
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1301
+ _groupWs.send(JSON.stringify({type:'bind_aid',aid:aid}));
1302
+ }
1303
+ // 检查是否在线,不在线则自动上线
1304
+ var info=S.aidList.find(function(a){ return a.aid===aid; });
1305
+ if(!info||!info.online){
1306
+ try {
1251
1307
  await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:aid})});
1252
- }
1253
- // 刷新页面以加载新身份的数据
1254
- location.reload();
1255
- } catch(e){}
1308
+ } catch(e){}
1309
+ }
1310
+ D.msgs.innerHTML=''; D.title.textContent='未选择会话';
1311
+ D.sList.dataset.s='';
1312
+ fetch('/api/ws/status?aid='+encodeURIComponent(aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1313
+ loadSessions();
1256
1314
  }
1257
1315
 
1258
1316
  async function toggleOnline(){
@@ -1269,19 +1327,29 @@ const chatHtml = `<!DOCTYPE html>
1269
1327
  } catch(e){}
1270
1328
  }
1271
1329
 
1272
- async function poll(){
1330
+ async function loadSessions(){
1331
+ if(!S.aid) return;
1273
1332
  try {
1274
- // P2P会话和消息仅在P2P标签页时刷新
1275
- if(S.tab==='p2p'){
1276
- var [sr,mr] = await Promise.all([fetch('/api/sessions'),fetch('/api/messages')]);
1277
- var sd=await sr.json(), md=await mr.json();
1278
- if(sd.sessions) updateSessions(sd.sessions, sd.activeSessionId);
1279
- S.closed=md.closed||false;
1280
- if(md.messages) renderMsgs(md.messages, S.closed);
1281
- }
1333
+ var r=await fetch('/api/sessions?aid='+encodeURIComponent(S.aid));
1334
+ var d=await r.json();
1335
+ if(d.sessions) updateSessions(d.sessions, S.sid);
1336
+ } catch(e){}
1337
+ }
1338
+
1339
+ async function loadMessages(){
1340
+ if(!S.aid||!S.sid||S.tab!=='p2p') return;
1341
+ try {
1342
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid));
1343
+ var d=await r.json();
1344
+ S.closed=d.closed||false;
1345
+ D.msgs.dataset.s='';
1346
+ if(d.messages) renderMsgs(d.messages, S.closed);
1282
1347
  } catch(e){}
1283
1348
  }
1284
1349
 
1350
+ // legacy poll kept for compatibility (no-op, replaced by WS push)
1351
+ function poll(){}
1352
+
1285
1353
  function updateSessions(sessions, activeId){
1286
1354
  var sig=JSON.stringify(sessions)+activeId+S.sid;
1287
1355
  if(D.sList.dataset.s===sig) return;
@@ -1395,8 +1463,8 @@ const chatHtml = `<!DOCTYPE html>
1395
1463
  return '<div class="message '+m.type+'">' +
1396
1464
  '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1397
1465
  '<div class="msg-content">' +
1398
- '<div class="bubble">'+c+'</div>' +
1399
1466
  '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1467
+ '<div class="bubble">'+c+'</div>' +
1400
1468
  '</div></div>';
1401
1469
  }).join('');
1402
1470
  if(closed){
@@ -1405,8 +1473,17 @@ const chatHtml = `<!DOCTYPE html>
1405
1473
  } else {
1406
1474
  D.input.disabled=false; D.input.placeholder='输入消息...';
1407
1475
  }
1408
- D.msgs.innerHTML=html;
1409
- D.msgs.scrollTop=D.msgs.scrollHeight;
1476
+ var wasAtBottom=isAtBottom();
1477
+ var prevScrollTop=D.msgs.scrollTop;
1478
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1479
+ D.newMsgTip=$('newMsgTip');
1480
+ // 不自动滚动,保持用户当前位置;有新消息时显示提示
1481
+ if(!wasAtBottom&&msgs.length>0){
1482
+ D.msgs.scrollTop=prevScrollTop;
1483
+ if(D.msgs.dataset.force!=='avatar') showNewMsgTip();
1484
+ } else {
1485
+ D.msgs.scrollTop=prevScrollTop;
1486
+ }
1410
1487
  }
1411
1488
 
1412
1489
  function updateDot(st){
@@ -1415,26 +1492,38 @@ const chatHtml = `<!DOCTYPE html>
1415
1492
  }
1416
1493
 
1417
1494
  async function pickSession(sid,peer){
1418
- S.sid=sid;
1495
+ if(S.tab!=='p2p') switchTab('p2p');
1496
+ S.sid=sid; S.sessionId=sid;
1497
+ hideNewMsgTip();
1419
1498
  D.title.textContent=peer;
1420
1499
  try {
1421
- await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid})});
1422
- var r=await fetch('/api/messages'); var d=await r.json();
1500
+ await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid,aid:S.aid})});
1501
+ // 通知服务端本标签页的 activeSessionId
1502
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1503
+ _groupWs.send(JSON.stringify({type:'set_active_session',sessionId:sid}));
1504
+ }
1505
+ var r=await fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(sid));
1506
+ var d=await r.json();
1423
1507
  S.closed=d.closed||false;
1424
1508
  D.msgs.dataset.s=''; // force
1425
1509
  renderMsgs(d.messages||[], S.closed);
1510
+ scrollToBottom();
1511
+ // 刷新会话列表,确保新会话出现在侧边栏
1512
+ loadSessions();
1426
1513
  } catch(e){}
1427
1514
  }
1428
1515
 
1429
1516
  async function sendMessage(){
1430
1517
  var txt=D.input.value.trim();
1431
1518
  if(!txt){ return; }
1519
+ // 用户主动发送消息,确保滚动到底部
1520
+ hideNewMsgTip();
1432
1521
  // 群组模式
1433
1522
  if(S.tab==='group'){
1434
1523
  if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1435
1524
  try {
1436
1525
  D.input.value='';
1437
- var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt})});
1526
+ var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt,aid:S.aid})});
1438
1527
  var d=await r.json();
1439
1528
  if(!d.success) alert(d.error||'发送失败');
1440
1529
  else {
@@ -1446,6 +1535,7 @@ const chatHtml = `<!DOCTYPE html>
1446
1535
  _lastGroupMsgs.push(sentMsg);
1447
1536
  _lastGroupMsgSig='';
1448
1537
  renderGroupMsgs(_lastGroupMsgs);
1538
+ scrollToBottom();
1449
1539
  }
1450
1540
  }
1451
1541
  }
@@ -1457,9 +1547,10 @@ const chatHtml = `<!DOCTYPE html>
1457
1547
  if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1458
1548
  try {
1459
1549
  D.input.value='';
1460
- var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid})});
1550
+ var r=await fetch('/api/ws/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:txt,sessionId:S.sid,aid:S.aid})});
1461
1551
  var d=await r.json();
1462
1552
  if(!d.success) alert(d.error||'发送失败');
1553
+ else { await loadMessages(); scrollToBottom(); }
1463
1554
  } catch(e){ alert('发送失败'); }
1464
1555
  }
1465
1556
 
@@ -1473,7 +1564,7 @@ const chatHtml = `<!DOCTYPE html>
1473
1564
 
1474
1565
  async function newSessionWith(peerAid){
1475
1566
  try {
1476
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid})});
1567
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:peerAid,aid:S.aid})});
1477
1568
  var d=await r.json();
1478
1569
  if(d.success){ pickSession(d.sessionId,peerAid); }
1479
1570
  else { alert(d.error||'连接失败'); }
@@ -1485,7 +1576,7 @@ const chatHtml = `<!DOCTYPE html>
1485
1576
  if(!aid) return;
1486
1577
  D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1487
1578
  try {
1488
- var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid})});
1579
+ var r=await fetch('/api/ws/connect',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({targetAid:aid,aid:S.aid})});
1489
1580
  var d=await r.json();
1490
1581
  if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1491
1582
  else { alert(d.error||'连接失败'); }
@@ -1504,11 +1595,15 @@ const chatHtml = `<!DOCTYPE html>
1504
1595
  if(isNaN(d.getTime())) return '';
1505
1596
  var now=new Date();
1506
1597
  var pad=function(v){ return v<10?'0'+v:''+v; };
1507
- var H=pad(d.getHours()), M=pad(d.getMinutes()), ss=pad(d.getSeconds());
1598
+ var H=pad(d.getHours()), M=pad(d.getMinutes());
1508
1599
  if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1509
- return H+':'+M+':'+ss;
1600
+ return H+':'+M;
1510
1601
  }
1511
- return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M+':'+ss;
1602
+ // 今年内省略年份
1603
+ if(d.getFullYear()===now.getFullYear()){
1604
+ return pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1605
+ }
1606
+ return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M;
1512
1607
  }
1513
1608
 
1514
1609
  // ============================================================
@@ -1530,7 +1625,7 @@ const chatHtml = `<!DOCTYPE html>
1530
1625
  _lastGroupMsgSig='';
1531
1626
  initGroupClient();
1532
1627
  pollGroupList();
1533
- if(S.activeGroupId) pollGroupMessages();
1628
+ if(S.activeGroupId) pollGroupMessages().then(function(){ scrollToBottom(); });
1534
1629
  else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1535
1630
  } else {
1536
1631
  D.encryptBanner.style.display='flex';
@@ -1538,15 +1633,19 @@ const chatHtml = `<!DOCTYPE html>
1538
1633
  D.input.placeholder='输入消息...';
1539
1634
  D.input.disabled=false;
1540
1635
  _lastGroupMsgSig='';
1541
- // 切回P2P时立即刷新消息
1636
+ // 立即清空消息区域,防止群消息残留
1637
+ D.msgs.innerHTML='';
1638
+ // 切回P2P时立即刷新会话列表和消息
1639
+ D.sList.dataset.s='';
1542
1640
  D.msgs.dataset.s='';
1641
+ loadSessions();
1543
1642
  if(S.sid){
1544
- fetch('/api/messages').then(function(r){ return r.json(); }).then(function(d){
1643
+ fetch('/api/messages?aid='+encodeURIComponent(S.aid)+'&sessionId='+encodeURIComponent(S.sid)).then(function(r){ return r.json(); }).then(function(d){
1644
+ if(S.tab!=='p2p') return;
1545
1645
  S.closed=d.closed||false;
1546
1646
  if(d.messages) renderMsgs(d.messages, S.closed);
1647
+ scrollToBottom();
1547
1648
  }).catch(function(){});
1548
- } else {
1549
- D.msgs.innerHTML='';
1550
1649
  }
1551
1650
  }
1552
1651
  }
@@ -1555,7 +1654,7 @@ const chatHtml = `<!DOCTYPE html>
1555
1654
  async function initGroupClient(){
1556
1655
  if(_groupInited) return;
1557
1656
  try {
1558
- var r=await fetch('/api/group/init',{method:'POST'});
1657
+ var r=await fetch('/api/group/init',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
1559
1658
  var d=await r.json();
1560
1659
  if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1561
1660
  } catch(e){ console.error('群组初始化失败',e); }
@@ -1563,7 +1662,7 @@ const chatHtml = `<!DOCTYPE html>
1563
1662
 
1564
1663
  async function pollGroupList(){
1565
1664
  try {
1566
- var r=await fetch('/api/group/list');
1665
+ var r=await fetch('/api/group/list?aid='+encodeURIComponent(S.aid));
1567
1666
  var d=await r.json();
1568
1667
  if(d.groups){ S.groups=d.groups; renderGroupList(); }
1569
1668
  } catch(e){}
@@ -1585,6 +1684,8 @@ const chatHtml = `<!DOCTYPE html>
1585
1684
  async function pickGroup(groupId,name){
1586
1685
  S.activeGroupId=groupId;
1587
1686
  S.isGroupCreator=false;
1687
+ _lastGroupMsgSig='';
1688
+ hideNewMsgTip();
1588
1689
  D.title.textContent=name;
1589
1690
  D.groupInfoBar.style.display='flex';
1590
1691
  D.groupInfoText.textContent=name;
@@ -1597,11 +1698,11 @@ const chatHtml = `<!DOCTYPE html>
1597
1698
  $('groupReviewBtn').style.display='none';
1598
1699
  renderGroupList();
1599
1700
  try {
1600
- await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId})});
1701
+ await fetch('/api/group/select',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
1601
1702
  } catch(e){}
1602
1703
  // 获取群信息判断是否为创建者
1603
1704
  try {
1604
- var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId));
1705
+ var r=await fetch('/api/group/info?groupId='+encodeURIComponent(groupId)+'&aid='+encodeURIComponent(S.aid));
1605
1706
  var d=await r.json();
1606
1707
  if(d.creator&&d.creator===S.aid){
1607
1708
  S.isGroupCreator=true;
@@ -1614,16 +1715,16 @@ const chatHtml = `<!DOCTYPE html>
1614
1715
  // 获取失败时默认显示复制群链接
1615
1716
  $('groupCopyLinkBtn').style.display='';
1616
1717
  }
1617
- pollGroupMessages();
1718
+ pollGroupMessages().then(function(){ scrollToBottom(); });
1618
1719
  }
1619
1720
 
1620
1721
  var _lastGroupMsgSig='';
1621
1722
  async function pollGroupMessages(){
1622
1723
  if(!S.activeGroupId||S.tab!=='group') return;
1623
1724
  try {
1624
- var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId));
1725
+ var r=await fetch('/api/group/messages?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1625
1726
  var d=await r.json();
1626
- if(d.messages) renderGroupMsgs(d.messages);
1727
+ if(S.tab==='group'&&d.messages) renderGroupMsgs(d.messages);
1627
1728
  } catch(e){}
1628
1729
  }
1629
1730
 
@@ -1632,17 +1733,31 @@ const chatHtml = `<!DOCTYPE html>
1632
1733
  // ============================================================
1633
1734
  var _groupWs=null;
1634
1735
  var _groupWsReconnectTimer=null;
1736
+ var _groupWsReconnectDelay=1000; // exponential backoff start
1737
+ var _groupWsPingTimer=null;
1635
1738
 
1636
1739
  function connectGroupWs(){
1637
1740
  if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
1638
1741
  var proto=location.protocol==='https:'?'wss:':'ws:';
1639
- _groupWs=new WebSocket(proto+'//'+location.host+'/ws/group');
1742
+ _groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
1640
1743
  _groupWs.onopen=function(){
1641
- console.log('[WS] group connected');
1744
+ console.log('[WS] ui connected');
1745
+ _groupWsReconnectDelay=1000; // reset backoff on success
1642
1746
  if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
1643
- // 重连后主动拉取最新状态,防止断连期间丢失推送
1644
- fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1747
+ // 绑定当前 aid
1748
+ if(S.aid) _groupWs.send(JSON.stringify({type:'bind_aid',aid:S.aid}));
1749
+ // 重连后主动拉取最新状态
1750
+ if(S.aid) fetch('/api/ws/status?aid='+encodeURIComponent(S.aid)).then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
1645
1751
  fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
1752
+ // 重连后补拉一次会话列表,防止断连期间丢失推送
1753
+ loadSessions();
1754
+ // 启动 keepalive ping(每 25s 发一次,防止代理/防火墙断连)
1755
+ if(_groupWsPingTimer) clearInterval(_groupWsPingTimer);
1756
+ _groupWsPingTimer=setInterval(function(){
1757
+ if(_groupWs&&_groupWs.readyState===WebSocket.OPEN){
1758
+ try{ _groupWs.send(JSON.stringify({type:'ping'})); }catch(e){}
1759
+ }
1760
+ },25000);
1646
1761
  };
1647
1762
  _groupWs.onmessage=function(ev){
1648
1763
  try {
@@ -1651,12 +1766,17 @@ const chatHtml = `<!DOCTYPE html>
1651
1766
  } catch(e){ console.error('[WS] parse error',e); }
1652
1767
  };
1653
1768
  _groupWs.onclose=function(){
1654
- console.log('[WS] group disconnected, reconnecting in 3s...');
1769
+ console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
1655
1770
  _groupWs=null;
1656
- _groupWsReconnectTimer=setTimeout(connectGroupWs,3000);
1771
+ if(_groupWsPingTimer){ clearInterval(_groupWsPingTimer); _groupWsPingTimer=null; }
1772
+ _groupWsReconnectTimer=setTimeout(function(){
1773
+ _groupWsReconnectDelay=Math.min(_groupWsReconnectDelay*2,30000); // cap at 30s
1774
+ connectGroupWs();
1775
+ },_groupWsReconnectDelay);
1657
1776
  };
1658
1777
  _groupWs.onerror=function(e){
1659
- console.error('[WS] group error',e);
1778
+ console.error('[WS] ui error',e);
1779
+ // onerror is always followed by onclose, so reconnect is handled there
1660
1780
  };
1661
1781
  }
1662
1782
 
@@ -1670,6 +1790,17 @@ const chatHtml = `<!DOCTYPE html>
1670
1790
  renderAidSelect();
1671
1791
  return;
1672
1792
  }
1793
+ if(data.type==='p2p_message'){
1794
+ // 实时推送的 P2P 消息
1795
+ if(S.tab==='p2p' && data.sessionId===S.sid){
1796
+ loadMessages();
1797
+ }
1798
+ return;
1799
+ }
1800
+ if(data.type==='sessions_updated'){
1801
+ loadSessions();
1802
+ return;
1803
+ }
1673
1804
  if(data.type==='group_message'){
1674
1805
  // 实时推送的完整消息
1675
1806
  var msg=data.message;
@@ -1712,17 +1843,18 @@ const chatHtml = `<!DOCTYPE html>
1712
1843
  }
1713
1844
  }
1714
1845
 
1715
- // 启动 WebSocket 连接
1716
- connectGroupWs();
1717
-
1718
1846
  var _lastGroupMsgs=[];
1719
1847
  function renderGroupMsgs(msgs){
1848
+ // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
1849
+ if(S.tab!=='group') return;
1720
1850
  var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
1721
1851
  if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
1852
+ var prevCount=_lastGroupMsgs.length;
1722
1853
  _lastGroupMsgSig=sig;
1723
1854
  _lastGroupMsgs=msgs;
1724
1855
  if(!msgs.length){
1725
- D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div>';
1856
+ D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">暂无消息</div><div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1857
+ D.newMsgTip=$('newMsgTip');
1726
1858
  return;
1727
1859
  }
1728
1860
  var needFetch=[];
@@ -1739,16 +1871,26 @@ const chatHtml = `<!DOCTYPE html>
1739
1871
  return '<div class="message '+(sent?'sent':'received')+'">' +
1740
1872
  '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1741
1873
  '<div class="msg-content">' +
1742
- '<div class="bubble">'+c+'</div>' +
1743
1874
  '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1875
+ '<div class="bubble">'+c+'</div>' +
1744
1876
  '</div></div>';
1745
1877
  }).join('');
1746
- D.msgs.innerHTML=html;
1747
- D.msgs.scrollTop=D.msgs.scrollHeight;
1878
+ var wasAtBottom=isAtBottom();
1879
+ var prevScrollTop=D.msgs.scrollTop;
1880
+ D.msgs.innerHTML=html+'<div class="new-msg-tip" id="newMsgTip" onclick="scrollToBottom()" style="display:none;">↓ 有新消息</div>';
1881
+ D.newMsgTip=$('newMsgTip');
1882
+ // 有新消息且用户不在底部:保持位置,显示提示
1883
+ if(msgs.length>prevCount&&prevCount>0&&!wasAtBottom){
1884
+ D.msgs.scrollTop=prevScrollTop;
1885
+ showNewMsgTip();
1886
+ } else {
1887
+ D.msgs.scrollTop=prevScrollTop;
1888
+ }
1748
1889
  // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
1749
1890
  var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
1750
1891
  unique.forEach(function(aid){
1751
1892
  fetchAgentInfo(aid).then(function(){
1893
+ if(S.tab!=='group') return;
1752
1894
  _lastGroupMsgSig='';
1753
1895
  _lastGroupMsgs._forceRender=true;
1754
1896
  renderGroupMsgs(_lastGroupMsgs);
@@ -1767,6 +1909,7 @@ const chatHtml = `<!DOCTYPE html>
1767
1909
  var name=$('groupNameInput').value.trim();
1768
1910
  if(!name) return;
1769
1911
  var description=$('groupDescInput').value.trim();
1912
+ if(!description){ $('groupDescInput').focus(); return; }
1770
1913
  var visibility=document.querySelector('input[name="groupVisibility"]:checked').value;
1771
1914
  var btn=$('createGroupBtn');
1772
1915
  btn.disabled=true; btn.textContent='创建中...';
@@ -1797,7 +1940,7 @@ const chatHtml = `<!DOCTYPE html>
1797
1940
  var btn=$('joinGroupBtn');
1798
1941
  btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
1799
1942
  try {
1800
- var r=await fetch('/api/group/join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupUrl:groupUrl,code:code||undefined})});
1943
+ var r=await fetch('/api/group/join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupUrl:groupUrl,code:code||undefined,aid:S.aid})});
1801
1944
  var d=await r.json();
1802
1945
  if(d.success){
1803
1946
  hideJoinGroupModal();
@@ -1819,7 +1962,7 @@ const chatHtml = `<!DOCTYPE html>
1819
1962
  async function generateInviteLink(){
1820
1963
  if(!S.activeGroupId) return;
1821
1964
  try {
1822
- var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId})});
1965
+ var r=await fetch('/api/group/invite-code',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,aid:S.aid})});
1823
1966
  var d=await r.json();
1824
1967
  if(d.success&&d.code){
1825
1968
  var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
@@ -1893,7 +2036,7 @@ const chatHtml = `<!DOCTYPE html>
1893
2036
  async function showGroupMembers(){
1894
2037
  if(!S.activeGroupId) return;
1895
2038
  try {
1896
- var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId));
2039
+ var r=await fetch('/api/group/members?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1897
2040
  var d=await r.json();
1898
2041
  if(d.members){
1899
2042
  var html=d.members.map(function(m){
@@ -1964,7 +2107,7 @@ const chatHtml = `<!DOCTYPE html>
1964
2107
  async function showPendingRequests(){
1965
2108
  if(!S.activeGroupId) return;
1966
2109
  try {
1967
- var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId));
2110
+ var r=await fetch('/api/group/pending-requests?groupId='+encodeURIComponent(S.activeGroupId)+'&aid='+encodeURIComponent(S.aid));
1968
2111
  var d=await r.json();
1969
2112
  if(d.requests&&d.requests.length>0){
1970
2113
  // 先渲染基础结构,然后异步加载 agent info
@@ -2018,7 +2161,7 @@ const chatHtml = `<!DOCTYPE html>
2018
2161
  async function reviewJoin(agentId,action){
2019
2162
  if(!S.activeGroupId) return;
2020
2163
  try {
2021
- var r=await fetch('/api/group/review-join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,agentId:agentId,action:action})});
2164
+ var r=await fetch('/api/group/review-join',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,agentId:agentId,action:action,aid:S.aid})});
2022
2165
  var d=await r.json();
2023
2166
  if(d.success){ showPendingRequests(); }
2024
2167
  else { alert(d.error||'操作失败'); }
@@ -2028,7 +2171,7 @@ const chatHtml = `<!DOCTYPE html>
2028
2171
  async function leaveGroup(groupId){
2029
2172
  if(!confirm('确认退出该群组?')) return;
2030
2173
  try {
2031
- var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId})});
2174
+ var r=await fetch('/api/group/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:groupId,aid:S.aid})});
2032
2175
  var d=await r.json();
2033
2176
  if(d.success){
2034
2177
  if(S.activeGroupId===groupId){
@@ -2052,7 +2195,7 @@ const chatHtml = `<!DOCTYPE html>
2052
2195
  showMyGroupsModal();
2053
2196
  $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2054
2197
  try {
2055
- var r=await fetch('/api/group/my-groups');
2198
+ var r=await fetch('/api/group/my-groups?aid='+encodeURIComponent(S.aid));
2056
2199
  var d=await r.json();
2057
2200
  if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2058
2201
  var groups=d.groups||[];
@@ -2190,7 +2333,7 @@ async function handleRequest(req, res) {
2190
2333
  try {
2191
2334
  const aidList = await datamanager_1.CertAndKeyStore.getAids();
2192
2335
  const aidStatus = await getAidStatusList();
2193
- sendJson(res, { currentAid, aidList, aidStatus, apiUrl: globalApiUrl });
2336
+ sendJson(res, { aidList, aidStatus, apiUrl: globalApiUrl });
2194
2337
  }
2195
2338
  catch (e) {
2196
2339
  sendJson(res, { error: e.message }, 500);
@@ -2216,11 +2359,8 @@ async function handleRequest(req, res) {
2216
2359
  }
2217
2360
  const loaded = await agentCP.loadAid(aid);
2218
2361
  if (loaded) {
2219
- const oldAid = currentAid;
2220
- currentAid = aid;
2221
- // 切换会话:保存旧 AID 的会话,加载新 AID 的会话
2222
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
2223
- activeSessionId = null;
2362
+ // 加载该 AID 的持久化会话
2363
+ await ensureMessageStoreLoaded(aid);
2224
2364
  // 切换身份时自动上线(含群组初始化),A/B 同时保持在线
2225
2365
  try {
2226
2366
  await ensureOnline(aid);
@@ -2268,7 +2408,6 @@ async function handleRequest(req, res) {
2268
2408
  }
2269
2409
  try {
2270
2410
  const created = await agentCP.createAid(aid);
2271
- currentAid = created;
2272
2411
  // 保存自定义昵称和描述
2273
2412
  const nickname = (body.nickname || '').trim();
2274
2413
  const description = (body.description || '').trim();
@@ -2357,7 +2496,7 @@ async function handleRequest(req, res) {
2357
2496
  if (pathname === '/api/ws/start' && method === 'POST') {
2358
2497
  try {
2359
2498
  const body = await parseBody(req);
2360
- const targetAid = body.aid || currentAid;
2499
+ const targetAid = body.aid;
2361
2500
  if (!targetAid) {
2362
2501
  sendJson(res, { success: false, error: '请先选择 AID' });
2363
2502
  return;
@@ -2373,9 +2512,13 @@ async function handleRequest(req, res) {
2373
2512
  if (pathname === '/api/ws/connect' && method === 'POST') {
2374
2513
  try {
2375
2514
  const body = await parseBody(req);
2376
- const targetAid = body.targetAid;
2515
+ const { targetAid, aid } = body;
2516
+ if (!aid) {
2517
+ sendJson(res, { success: false, error: '缺少 aid' });
2518
+ return;
2519
+ }
2377
2520
  // 自动上线
2378
- const instance = await ensureOnline();
2521
+ const instance = await ensureOnline(aid);
2379
2522
  if (!instance.agentWS) {
2380
2523
  sendJson(res, { success: false, error: '自动上线失败' });
2381
2524
  return;
@@ -2398,9 +2541,8 @@ async function handleRequest(req, res) {
2398
2541
  console.log('邀请状态:', status);
2399
2542
  });
2400
2543
  });
2401
- // 创建 outgoing session 并设为活跃
2402
- getMessageStore().getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', currentAid);
2403
- activeSessionId = result.sessionId;
2544
+ // 创建 outgoing session
2545
+ getMessageStoreForAid(aid).getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', aid);
2404
2546
  sendJson(res, Object.assign({ success: true }, result));
2405
2547
  }
2406
2548
  catch (e) {
@@ -2411,24 +2553,26 @@ async function handleRequest(req, res) {
2411
2553
  if (pathname === '/api/ws/send' && method === 'POST') {
2412
2554
  try {
2413
2555
  const body = await parseBody(req);
2414
- const { message, sessionId } = body;
2556
+ const { message, sessionId, aid } = body;
2415
2557
  if (!message) {
2416
2558
  sendJson(res, { success: false, error: '消息不能为空' });
2417
2559
  return;
2418
2560
  }
2561
+ if (!aid) {
2562
+ sendJson(res, { success: false, error: '缺少 aid' });
2563
+ return;
2564
+ }
2419
2565
  // 自动上线
2420
- const instance = await ensureOnline();
2566
+ const instance = await ensureOnline(aid);
2421
2567
  if (!instance.agentWS) {
2422
2568
  sendJson(res, { success: false, error: '自动上线失败' });
2423
2569
  return;
2424
2570
  }
2425
- // 使用指定的 sessionId 或当前活跃会话
2426
- const sid = sessionId || activeSessionId;
2427
- if (!sid) {
2428
- sendJson(res, { success: false, error: '没有活跃会话' });
2571
+ if (!sessionId) {
2572
+ sendJson(res, { success: false, error: '缺少 sessionId' });
2429
2573
  return;
2430
2574
  }
2431
- const session = getMessageStore().getSession(sid);
2575
+ const session = getMessageStoreForAid(aid).getSession(sessionId);
2432
2576
  if (!session) {
2433
2577
  sendJson(res, { success: false, error: '会话不存在' });
2434
2578
  return;
@@ -2438,7 +2582,7 @@ async function handleRequest(req, res) {
2438
2582
  return;
2439
2583
  }
2440
2584
  instance.agentWS.send(message, session.peerAid, session.sessionId, session.identifyingCode);
2441
- getMessageStore().addMessageToSession(sid, {
2585
+ getMessageStoreForAid(aid).addMessageToSession(sessionId, {
2442
2586
  type: 'sent',
2443
2587
  content: message,
2444
2588
  to: session.peerAid,
@@ -2452,12 +2596,20 @@ async function handleRequest(req, res) {
2452
2596
  return;
2453
2597
  }
2454
2598
  if (pathname === '/api/messages' && method === 'GET') {
2455
- const session = activeSessionId ? getMessageStore().getSession(activeSessionId) : null;
2456
- sendJson(res, { messages: session ? session.messages : [], activeSessionId, closed: session ? (session.closed || false) : false });
2599
+ const aid = parsedUrl.query.aid;
2600
+ const sessionId = parsedUrl.query.sessionId;
2601
+ if (!aid || !sessionId) {
2602
+ sendJson(res, { messages: [], activeSessionId: null, closed: false });
2603
+ return;
2604
+ }
2605
+ const store = await ensureMessageStoreLoaded(aid);
2606
+ const session = store.getSession(sessionId);
2607
+ sendJson(res, { messages: session ? session.messages : [], activeSessionId: sessionId, closed: session ? (session.closed || false) : false });
2457
2608
  return;
2458
2609
  }
2459
2610
  if (pathname === '/api/ws/status' && method === 'GET') {
2460
- const instance = getActiveInstance();
2611
+ const aid = parsedUrl.query.aid;
2612
+ const instance = aid ? aidInstances.get(aid) : null;
2461
2613
  if (!instance || !instance.agentWS) {
2462
2614
  sendJson(res, { connected: false, status: 'disconnected' });
2463
2615
  }
@@ -2467,19 +2619,26 @@ async function handleRequest(req, res) {
2467
2619
  return;
2468
2620
  }
2469
2621
  if (pathname === '/api/sessions' && method === 'GET') {
2470
- sendJson(res, { sessions: getMessageStore().getSessionList(currentAid), activeSessionId });
2622
+ const aid = parsedUrl.query.aid;
2623
+ if (!aid) {
2624
+ sendJson(res, { sessions: [], activeSessionId: null });
2625
+ return;
2626
+ }
2627
+ const store = await ensureMessageStoreLoaded(aid);
2628
+ sendJson(res, { sessions: store.getSessionList(aid), activeSessionId: null });
2471
2629
  return;
2472
2630
  }
2473
2631
  if (pathname === '/api/sessions/active' && method === 'POST') {
2474
2632
  try {
2475
2633
  const body = await parseBody(req);
2476
- const { sessionId } = body;
2477
- if (!sessionId || !getMessageStore().hasSession(sessionId)) {
2634
+ const { sessionId, aid } = body;
2635
+ if (!aid || !sessionId || !getMessageStoreForAid(aid).hasSession(sessionId)) {
2478
2636
  sendJson(res, { success: false, error: '会话不存在' });
2479
2637
  return;
2480
2638
  }
2481
- activeSessionId = sessionId;
2482
- sendJson(res, { success: true, activeSessionId });
2639
+ // 通知绑定了该 aid 的客户端更新 activeSessionId
2640
+ pushToAid(aid, { type: 'set_active_session', sessionId });
2641
+ sendJson(res, { success: true, activeSessionId: sessionId });
2483
2642
  }
2484
2643
  catch (e) {
2485
2644
  sendJson(res, { success: false, error: e.message });
@@ -2489,15 +2648,17 @@ async function handleRequest(req, res) {
2489
2648
  if (pathname === '/api/sessions/delete' && method === 'POST') {
2490
2649
  try {
2491
2650
  const body = await parseBody(req);
2492
- const { sessionId } = body;
2651
+ const { sessionId, aid } = body;
2493
2652
  if (!sessionId) {
2494
2653
  sendJson(res, { success: false, error: '会话ID不能为空' });
2495
2654
  return;
2496
2655
  }
2497
- const deleted = await getMessageStore().deleteSession(sessionId);
2656
+ if (!aid) {
2657
+ sendJson(res, { success: false, error: '缺少 aid' });
2658
+ return;
2659
+ }
2660
+ const deleted = await getMessageStoreForAid(aid).deleteSession(sessionId);
2498
2661
  if (deleted) {
2499
- if (activeSessionId === sessionId)
2500
- activeSessionId = null;
2501
2662
  sendJson(res, { success: true });
2502
2663
  }
2503
2664
  else {
@@ -2512,18 +2673,16 @@ async function handleRequest(req, res) {
2512
2673
  if (pathname === '/api/peers/delete' && method === 'POST') {
2513
2674
  try {
2514
2675
  const body = await parseBody(req);
2515
- const { peerAid } = body;
2676
+ const { peerAid, aid } = body;
2516
2677
  if (!peerAid) {
2517
2678
  sendJson(res, { success: false, error: 'AID不能为空' });
2518
2679
  return;
2519
2680
  }
2520
- const count = await getMessageStore().deletePeer(peerAid, currentAid);
2521
- if (activeSessionId) {
2522
- const session = getMessageStore().getSession(activeSessionId);
2523
- if (!session || session.peerAid === peerAid) {
2524
- activeSessionId = null;
2525
- }
2681
+ if (!aid) {
2682
+ sendJson(res, { success: false, error: '缺少 aid' });
2683
+ return;
2526
2684
  }
2685
+ const count = await getMessageStoreForAid(aid).deletePeer(peerAid, aid);
2527
2686
  sendJson(res, { success: true, count });
2528
2687
  }
2529
2688
  catch (e) {
@@ -2536,7 +2695,13 @@ async function handleRequest(req, res) {
2536
2695
  // ============================================================
2537
2696
  if (pathname === '/api/group/init' && method === 'POST') {
2538
2697
  try {
2539
- const instance = await ensureOnline();
2698
+ const body = await parseBody(req);
2699
+ const aid = body.aid;
2700
+ if (!aid) {
2701
+ sendJson(res, { success: false, error: '缺少 aid' });
2702
+ return;
2703
+ }
2704
+ const instance = await ensureOnline(aid);
2540
2705
  await ensureGroupClient(instance);
2541
2706
  sendJson(res, { success: true, targetAid: instance.groupTargetAid });
2542
2707
  }
@@ -2548,12 +2713,16 @@ async function handleRequest(req, res) {
2548
2713
  if (pathname === '/api/group/create' && method === 'POST') {
2549
2714
  try {
2550
2715
  const body = await parseBody(req);
2551
- const { name, visibility, description } = body;
2716
+ const { name, visibility, description, aid } = body;
2717
+ if (!aid) {
2718
+ sendJson(res, { success: false, error: '缺少 aid' });
2719
+ return;
2720
+ }
2552
2721
  if (!name) {
2553
2722
  sendJson(res, { success: false, error: '群组名称不能为空' });
2554
2723
  return;
2555
2724
  }
2556
- const instance = await ensureOnline();
2725
+ const instance = await ensureOnline(aid);
2557
2726
  await ensureGroupClient(instance);
2558
2727
  const ops = instance.agentCP.groupOps;
2559
2728
  const target = instance.groupTargetAid;
@@ -2566,6 +2735,8 @@ async function handleRequest(req, res) {
2566
2735
  console.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
2567
2736
  // 加入本地存储
2568
2737
  instance.agentCP.addGroupToStore(result.group_id, name);
2738
+ // 注册在线,才能收到实时消息推送
2739
+ await instance.agentCP.joinGroupSession(result.group_id);
2569
2740
  sendJson(res, Object.assign({ success: true }, result));
2570
2741
  }
2571
2742
  catch (e) {
@@ -2575,7 +2746,12 @@ async function handleRequest(req, res) {
2575
2746
  }
2576
2747
  if (pathname === '/api/group/list' && method === 'GET') {
2577
2748
  try {
2578
- const instance = await ensureOnline();
2749
+ const aid = parsedUrl.query.aid;
2750
+ if (!aid) {
2751
+ sendJson(res, { success: false, error: '缺少 aid', groups: [] });
2752
+ return;
2753
+ }
2754
+ const instance = await ensureOnline(aid);
2579
2755
  await ensureGroupClient(instance);
2580
2756
  // 首次访问时从服务端同步群组列表
2581
2757
  if (!instance.groupListSynced) {
@@ -2598,8 +2774,12 @@ async function handleRequest(req, res) {
2598
2774
  if (pathname === '/api/group/select' && method === 'POST') {
2599
2775
  try {
2600
2776
  const body = await parseBody(req);
2601
- const { groupId } = body;
2602
- const instance = await ensureOnline();
2777
+ const { groupId, aid } = body;
2778
+ if (!aid) {
2779
+ sendJson(res, { success: false, error: '缺少 aid' });
2780
+ return;
2781
+ }
2782
+ const instance = await ensureOnline(aid);
2603
2783
  instance.activeGroupId = groupId || null;
2604
2784
  sendJson(res, { success: true });
2605
2785
  }
@@ -2611,11 +2791,16 @@ async function handleRequest(req, res) {
2611
2791
  if (pathname === '/api/group/info' && method === 'GET') {
2612
2792
  try {
2613
2793
  const groupId = parsedUrl.query.groupId;
2794
+ const aid = parsedUrl.query.aid;
2614
2795
  if (!groupId) {
2615
2796
  sendJson(res, { success: false, error: '缺少 groupId' });
2616
2797
  return;
2617
2798
  }
2618
- const instance = await ensureOnline();
2799
+ if (!aid) {
2800
+ sendJson(res, { success: false, error: '缺少 aid' });
2801
+ return;
2802
+ }
2803
+ const instance = await ensureOnline(aid);
2619
2804
  await ensureGroupClient(instance);
2620
2805
  const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
2621
2806
  sendJson(res, Object.assign({ success: true }, info));
@@ -2628,12 +2813,16 @@ async function handleRequest(req, res) {
2628
2813
  if (pathname === '/api/group/send' && method === 'POST') {
2629
2814
  try {
2630
2815
  const body = await parseBody(req);
2631
- const { groupId, message } = body;
2816
+ const { groupId, message, aid } = body;
2632
2817
  if (!groupId || !message) {
2633
2818
  sendJson(res, { success: false, error: '缺少 groupId 或 message' });
2634
2819
  return;
2635
2820
  }
2636
- const instance = await ensureOnline();
2821
+ if (!aid) {
2822
+ sendJson(res, { success: false, error: '缺少 aid' });
2823
+ return;
2824
+ }
2825
+ const instance = await ensureOnline(aid);
2637
2826
  await ensureGroupClient(instance);
2638
2827
  const result = await instance.agentCP.groupOps.sendGroupMessage(instance.groupTargetAid, groupId, message, 'text');
2639
2828
  // 添加到本地存储
@@ -2654,7 +2843,12 @@ async function handleRequest(req, res) {
2654
2843
  if (pathname === '/api/group/messages' && method === 'GET') {
2655
2844
  try {
2656
2845
  const groupId = parsedUrl.query.groupId || '';
2657
- const instance = await ensureOnline();
2846
+ const aid = parsedUrl.query.aid;
2847
+ if (!aid) {
2848
+ sendJson(res, { success: false, error: '缺少 aid', messages: [] });
2849
+ return;
2850
+ }
2851
+ const instance = await ensureOnline(aid);
2658
2852
  if (!groupId) {
2659
2853
  sendJson(res, { success: true, messages: [] });
2660
2854
  return;
@@ -2673,12 +2867,16 @@ async function handleRequest(req, res) {
2673
2867
  if (pathname === '/api/group/invite-code' && method === 'POST') {
2674
2868
  try {
2675
2869
  const body = await parseBody(req);
2676
- const { groupId } = body;
2870
+ const { groupId, aid } = body;
2677
2871
  if (!groupId) {
2678
2872
  sendJson(res, { success: false, error: '缺少 groupId' });
2679
2873
  return;
2680
2874
  }
2681
- const instance = await ensureOnline();
2875
+ if (!aid) {
2876
+ sendJson(res, { success: false, error: '缺少 aid' });
2877
+ return;
2878
+ }
2879
+ const instance = await ensureOnline(aid);
2682
2880
  await ensureGroupClient(instance);
2683
2881
  const result = await instance.agentCP.groupOps.createInviteCode(instance.groupTargetAid, groupId, body.options);
2684
2882
  const groupUrl = `https://${instance.groupTargetAid}/${groupId}`;
@@ -2692,17 +2890,20 @@ async function handleRequest(req, res) {
2692
2890
  if (pathname === '/api/group/join' && method === 'POST') {
2693
2891
  try {
2694
2892
  const body = await parseBody(req);
2695
- const { groupUrl, code } = body;
2893
+ const { groupUrl, code, aid } = body;
2696
2894
  if (!groupUrl) {
2697
2895
  sendJson(res, { success: false, error: '缺少群聊链接' });
2698
2896
  return;
2699
2897
  }
2898
+ if (!aid) {
2899
+ sendJson(res, { success: false, error: '缺少 aid' });
2900
+ return;
2901
+ }
2700
2902
  const { targetAid, groupId } = group_1.GroupOperations.parseGroupUrl(groupUrl);
2701
- const instance = await ensureOnline();
2903
+ const instance = await ensureOnline(aid);
2702
2904
  await ensureGroupClient(instance);
2703
- if (code) {
2704
- // 免审核:邀请码加入
2705
- await instance.agentCP.groupOps.useInviteCode(targetAid, groupId, code);
2905
+ // 加入成功后的统一处理:获取群名、写入本地存储、注册在线会话
2906
+ const finalizeJoin = async () => {
2706
2907
  let groupName = groupId;
2707
2908
  try {
2708
2909
  const info = await instance.agentCP.groupOps.getGroupInfo(targetAid, groupId);
@@ -2710,12 +2911,26 @@ async function handleRequest(req, res) {
2710
2911
  }
2711
2912
  catch (_) { }
2712
2913
  instance.agentCP.addGroupToStore(groupId, groupName);
2914
+ await instance.agentCP.joinGroupSession(groupId);
2915
+ };
2916
+ if (code) {
2917
+ // 免审核:邀请码加入
2918
+ await instance.agentCP.groupOps.useInviteCode(targetAid, groupId, code);
2919
+ await finalizeJoin();
2713
2920
  sendJson(res, { success: true, group_id: groupId });
2714
2921
  }
2715
2922
  else {
2716
- // 审核模式:发送入群申请
2717
- const requestId = await instance.agentCP.groupOps.requestJoin(targetAid, groupId, body.message || '');
2718
- sendJson(res, { success: true, pending: true, request_id: requestId });
2923
+ // 申请加入:公开群直接加入,私密群等待审核
2924
+ const result = await instance.agentCP.groupOps.requestJoin(targetAid, groupId, body.message || '');
2925
+ if (result.status === 'joined') {
2926
+ // 公开群:直接加入成功
2927
+ await finalizeJoin();
2928
+ sendJson(res, { success: true, group_id: groupId });
2929
+ }
2930
+ else {
2931
+ // 私密群:等待管理员审核
2932
+ sendJson(res, { success: true, pending: true, request_id: result.request_id });
2933
+ }
2719
2934
  }
2720
2935
  }
2721
2936
  catch (e) {
@@ -2726,11 +2941,16 @@ async function handleRequest(req, res) {
2726
2941
  if (pathname === '/api/group/pending-requests' && method === 'GET') {
2727
2942
  try {
2728
2943
  const groupId = parsedUrl.query.groupId;
2944
+ const aid = parsedUrl.query.aid;
2729
2945
  if (!groupId) {
2730
2946
  sendJson(res, { success: false, error: '缺少 groupId' });
2731
2947
  return;
2732
2948
  }
2733
- const instance = await ensureOnline();
2949
+ if (!aid) {
2950
+ sendJson(res, { success: false, error: '缺少 aid' });
2951
+ return;
2952
+ }
2953
+ const instance = await ensureOnline(aid);
2734
2954
  await ensureGroupClient(instance);
2735
2955
  const result = await instance.agentCP.groupOps.getPendingRequests(instance.groupTargetAid, groupId);
2736
2956
  sendJson(res, Object.assign({ success: true }, result));
@@ -2743,12 +2963,16 @@ async function handleRequest(req, res) {
2743
2963
  if (pathname === '/api/group/review-join' && method === 'POST') {
2744
2964
  try {
2745
2965
  const body = await parseBody(req);
2746
- const { groupId, agentId, action } = body;
2966
+ const { groupId, agentId, action, aid } = body;
2747
2967
  if (!groupId || !agentId || !action) {
2748
2968
  sendJson(res, { success: false, error: '缺少参数' });
2749
2969
  return;
2750
2970
  }
2751
- const instance = await ensureOnline();
2971
+ if (!aid) {
2972
+ sendJson(res, { success: false, error: '缺少 aid' });
2973
+ return;
2974
+ }
2975
+ const instance = await ensureOnline(aid);
2752
2976
  await ensureGroupClient(instance);
2753
2977
  await instance.agentCP.groupOps.reviewJoinRequest(instance.groupTargetAid, groupId, agentId, action, body.reason || '');
2754
2978
  sendJson(res, { success: true });
@@ -2761,11 +2985,16 @@ async function handleRequest(req, res) {
2761
2985
  if (pathname === '/api/group/members' && method === 'GET') {
2762
2986
  try {
2763
2987
  const groupId = parsedUrl.query.groupId;
2988
+ const aid = parsedUrl.query.aid;
2764
2989
  if (!groupId) {
2765
2990
  sendJson(res, { success: false, error: '缺少 groupId' });
2766
2991
  return;
2767
2992
  }
2768
- const instance = await ensureOnline();
2993
+ if (!aid) {
2994
+ sendJson(res, { success: false, error: '缺少 aid' });
2995
+ return;
2996
+ }
2997
+ const instance = await ensureOnline(aid);
2769
2998
  await ensureGroupClient(instance);
2770
2999
  const result = await instance.agentCP.groupOps.getMembers(instance.groupTargetAid, groupId);
2771
3000
  sendJson(res, Object.assign({ success: true }, result));
@@ -2777,7 +3006,12 @@ async function handleRequest(req, res) {
2777
3006
  }
2778
3007
  if (pathname === '/api/group/my-groups' && method === 'GET') {
2779
3008
  try {
2780
- const instance = await ensureOnline();
3009
+ const aid = parsedUrl.query.aid;
3010
+ if (!aid) {
3011
+ sendJson(res, { success: false, error: '缺少 aid', groups: [] });
3012
+ return;
3013
+ }
3014
+ const instance = await ensureOnline(aid);
2781
3015
  await ensureGroupClient(instance);
2782
3016
  const ops = instance.agentCP.groupOps;
2783
3017
  const target = instance.groupTargetAid;
@@ -2803,12 +3037,16 @@ async function handleRequest(req, res) {
2803
3037
  if (pathname === '/api/group/leave' && method === 'POST') {
2804
3038
  try {
2805
3039
  const body = await parseBody(req);
2806
- const { groupId } = body;
3040
+ const { groupId, aid } = body;
2807
3041
  if (!groupId) {
2808
3042
  sendJson(res, { success: false, error: '缺少 groupId' });
2809
3043
  return;
2810
3044
  }
2811
- const instance = await ensureOnline();
3045
+ if (!aid) {
3046
+ sendJson(res, { success: false, error: '缺少 aid' });
3047
+ return;
3048
+ }
3049
+ const instance = await ensureOnline(aid);
2812
3050
  await ensureGroupClient(instance);
2813
3051
  await instance.agentCP.groupOps.leaveGroup(instance.groupTargetAid, groupId);
2814
3052
  await instance.agentCP.removeGroupFromStore(groupId);
@@ -2860,22 +3098,58 @@ function startServer(port, apiUrl, dataDir = '') {
2860
3098
  await agentCP.loadCurrentAid();
2861
3099
  }
2862
3100
  }
2863
- currentAid = aid;
2864
3101
  console.log(`已加载 AID: ${aid}`);
2865
3102
  // 加载该 AID 的持久化会话
2866
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
3103
+ await ensureMessageStoreLoaded(aid);
2867
3104
  console.log(`已加载会话`);
2868
3105
  }
2869
3106
  }).catch(() => { });
2870
3107
  const server = http.createServer(handleRequest);
2871
3108
  // WebSocket server for browser ↔ server real-time communication
2872
3109
  const wss = new ws_1.default.Server({ noServer: true });
3110
+ // Periodically terminate dead connections (no pong within 30s)
3111
+ const wsAliveMap = new WeakMap();
3112
+ const wssHeartbeat = setInterval(() => {
3113
+ for (const [ws] of browserWsClients) {
3114
+ if (wsAliveMap.get(ws) === false) {
3115
+ ws.terminate();
3116
+ browserWsClients.delete(ws);
3117
+ continue;
3118
+ }
3119
+ wsAliveMap.set(ws, false);
3120
+ ws.ping();
3121
+ }
3122
+ }, 30000);
2873
3123
  server.on('upgrade', (req, socket, head) => {
2874
3124
  const pathname = url.parse(req.url || '', true).pathname;
2875
- if (pathname === '/ws/group') {
3125
+ if (pathname === '/ws/ui' || pathname === '/ws/group') {
2876
3126
  wss.handleUpgrade(req, socket, head, (ws) => {
2877
- browserWsClients.add(ws);
3127
+ const client = { ws, aid: '', activeSessionId: null };
3128
+ browserWsClients.set(ws, client);
3129
+ wsAliveMap.set(ws, true);
3130
+ ws.on('pong', () => wsAliveMap.set(ws, true));
2878
3131
  console.log(`[WS] browser client connected, total=${browserWsClients.size}`);
3132
+ ws.on('message', (raw) => {
3133
+ try {
3134
+ const msg = JSON.parse(raw.toString());
3135
+ if (msg.type === 'ping') {
3136
+ if (ws.readyState === ws_1.default.OPEN)
3137
+ ws.send(JSON.stringify({ type: 'pong' }));
3138
+ }
3139
+ else if (msg.type === 'bind_aid') {
3140
+ client.aid = msg.aid || '';
3141
+ // 推送当前该 aid 的 ws 状态
3142
+ const instance = aidInstances.get(client.aid);
3143
+ if (instance) {
3144
+ ws.send(JSON.stringify({ type: 'ws_status', aid: client.aid, status: instance.wsStatus }));
3145
+ }
3146
+ }
3147
+ else if (msg.type === 'set_active_session') {
3148
+ client.activeSessionId = msg.sessionId || null;
3149
+ }
3150
+ }
3151
+ catch (_a) { }
3152
+ });
2879
3153
  ws.on('close', () => {
2880
3154
  browserWsClients.delete(ws);
2881
3155
  console.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
@@ -2893,6 +3167,7 @@ function startServer(port, apiUrl, dataDir = '') {
2893
3167
  // 资源清理函数
2894
3168
  const cleanup = async () => {
2895
3169
  console.log('\n正在关闭服务...');
3170
+ clearInterval(wssHeartbeat);
2896
3171
  // 持久化 agent info 缓存
2897
3172
  saveAgentInfoCacheToDisk();
2898
3173
  // 持久化当前会话
@@ -2930,8 +3205,8 @@ function startServer(port, apiUrl, dataDir = '') {
2930
3205
  }
2931
3206
  aidInstances.clear();
2932
3207
  // 关闭所有浏览器 WS 连接
2933
- for (const client of browserWsClients) {
2934
- client.close();
3208
+ for (const [ws] of browserWsClients) {
3209
+ ws.close();
2935
3210
  }
2936
3211
  browserWsClients.clear();
2937
3212
  wss.close();