acp-ts 1.2.2 → 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,9 +1473,17 @@ const chatHtml = `<!DOCTYPE html>
1405
1473
  } else {
1406
1474
  D.input.disabled=false; D.input.placeholder='输入消息...';
1407
1475
  }
1408
- var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1409
- D.msgs.innerHTML=html;
1410
- if(wasAtBottom) 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
+ }
1411
1487
  }
1412
1488
 
1413
1489
  function updateDot(st){
@@ -1416,26 +1492,38 @@ const chatHtml = `<!DOCTYPE html>
1416
1492
  }
1417
1493
 
1418
1494
  async function pickSession(sid,peer){
1419
- S.sid=sid;
1495
+ if(S.tab!=='p2p') switchTab('p2p');
1496
+ S.sid=sid; S.sessionId=sid;
1497
+ hideNewMsgTip();
1420
1498
  D.title.textContent=peer;
1421
1499
  try {
1422
- await fetch('/api/sessions/active',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:sid})});
1423
- 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();
1424
1507
  S.closed=d.closed||false;
1425
1508
  D.msgs.dataset.s=''; // force
1426
1509
  renderMsgs(d.messages||[], S.closed);
1510
+ scrollToBottom();
1511
+ // 刷新会话列表,确保新会话出现在侧边栏
1512
+ loadSessions();
1427
1513
  } catch(e){}
1428
1514
  }
1429
1515
 
1430
1516
  async function sendMessage(){
1431
1517
  var txt=D.input.value.trim();
1432
1518
  if(!txt){ return; }
1519
+ // 用户主动发送消息,确保滚动到底部
1520
+ hideNewMsgTip();
1433
1521
  // 群组模式
1434
1522
  if(S.tab==='group'){
1435
1523
  if(!S.activeGroupId){ alert('请先选择一个群组'); return; }
1436
1524
  try {
1437
1525
  D.input.value='';
1438
- 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})});
1439
1527
  var d=await r.json();
1440
1528
  if(!d.success) alert(d.error||'发送失败');
1441
1529
  else {
@@ -1447,6 +1535,7 @@ const chatHtml = `<!DOCTYPE html>
1447
1535
  _lastGroupMsgs.push(sentMsg);
1448
1536
  _lastGroupMsgSig='';
1449
1537
  renderGroupMsgs(_lastGroupMsgs);
1538
+ scrollToBottom();
1450
1539
  }
1451
1540
  }
1452
1541
  }
@@ -1458,9 +1547,10 @@ const chatHtml = `<!DOCTYPE html>
1458
1547
  if(S.closed){ alert('该会话已关闭,请新建会话继续通信'); return; }
1459
1548
  try {
1460
1549
  D.input.value='';
1461
- 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})});
1462
1551
  var d=await r.json();
1463
1552
  if(!d.success) alert(d.error||'发送失败');
1553
+ else { await loadMessages(); scrollToBottom(); }
1464
1554
  } catch(e){ alert('发送失败'); }
1465
1555
  }
1466
1556
 
@@ -1474,7 +1564,7 @@ const chatHtml = `<!DOCTYPE html>
1474
1564
 
1475
1565
  async function newSessionWith(peerAid){
1476
1566
  try {
1477
- 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})});
1478
1568
  var d=await r.json();
1479
1569
  if(d.success){ pickSession(d.sessionId,peerAid); }
1480
1570
  else { alert(d.error||'连接失败'); }
@@ -1486,7 +1576,7 @@ const chatHtml = `<!DOCTYPE html>
1486
1576
  if(!aid) return;
1487
1577
  D.cBtn.disabled=true; D.cBtn.textContent='连接中...';
1488
1578
  try {
1489
- 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})});
1490
1580
  var d=await r.json();
1491
1581
  if(d.success){ hideModal(); pickSession(d.sessionId,aid); }
1492
1582
  else { alert(d.error||'连接失败'); }
@@ -1505,11 +1595,15 @@ const chatHtml = `<!DOCTYPE html>
1505
1595
  if(isNaN(d.getTime())) return '';
1506
1596
  var now=new Date();
1507
1597
  var pad=function(v){ return v<10?'0'+v:''+v; };
1508
- var H=pad(d.getHours()), M=pad(d.getMinutes()), ss=pad(d.getSeconds());
1598
+ var H=pad(d.getHours()), M=pad(d.getMinutes());
1509
1599
  if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
1510
- return H+':'+M+':'+ss;
1600
+ return H+':'+M;
1511
1601
  }
1512
- 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;
1513
1607
  }
1514
1608
 
1515
1609
  // ============================================================
@@ -1531,7 +1625,7 @@ const chatHtml = `<!DOCTYPE html>
1531
1625
  _lastGroupMsgSig='';
1532
1626
  initGroupClient();
1533
1627
  pollGroupList();
1534
- if(S.activeGroupId) pollGroupMessages();
1628
+ if(S.activeGroupId) pollGroupMessages().then(function(){ scrollToBottom(); });
1535
1629
  else { D.msgs.innerHTML='<div style="text-align:center;color:#ccc;margin-top:20px;font-size:12px;">选择或创建一个群组</div>'; }
1536
1630
  } else {
1537
1631
  D.encryptBanner.style.display='flex';
@@ -1539,15 +1633,19 @@ const chatHtml = `<!DOCTYPE html>
1539
1633
  D.input.placeholder='输入消息...';
1540
1634
  D.input.disabled=false;
1541
1635
  _lastGroupMsgSig='';
1542
- // 切回P2P时立即刷新消息
1636
+ // 立即清空消息区域,防止群消息残留
1637
+ D.msgs.innerHTML='';
1638
+ // 切回P2P时立即刷新会话列表和消息
1639
+ D.sList.dataset.s='';
1543
1640
  D.msgs.dataset.s='';
1641
+ loadSessions();
1544
1642
  if(S.sid){
1545
- 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;
1546
1645
  S.closed=d.closed||false;
1547
1646
  if(d.messages) renderMsgs(d.messages, S.closed);
1647
+ scrollToBottom();
1548
1648
  }).catch(function(){});
1549
- } else {
1550
- D.msgs.innerHTML='';
1551
1649
  }
1552
1650
  }
1553
1651
  }
@@ -1556,7 +1654,7 @@ const chatHtml = `<!DOCTYPE html>
1556
1654
  async function initGroupClient(){
1557
1655
  if(_groupInited) return;
1558
1656
  try {
1559
- 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})});
1560
1658
  var d=await r.json();
1561
1659
  if(d.success){ _groupInited=true; if(d.targetAid) S.groupTargetAid=d.targetAid; }
1562
1660
  } catch(e){ console.error('群组初始化失败',e); }
@@ -1564,7 +1662,7 @@ const chatHtml = `<!DOCTYPE html>
1564
1662
 
1565
1663
  async function pollGroupList(){
1566
1664
  try {
1567
- var r=await fetch('/api/group/list');
1665
+ var r=await fetch('/api/group/list?aid='+encodeURIComponent(S.aid));
1568
1666
  var d=await r.json();
1569
1667
  if(d.groups){ S.groups=d.groups; renderGroupList(); }
1570
1668
  } catch(e){}
@@ -1586,6 +1684,8 @@ const chatHtml = `<!DOCTYPE html>
1586
1684
  async function pickGroup(groupId,name){
1587
1685
  S.activeGroupId=groupId;
1588
1686
  S.isGroupCreator=false;
1687
+ _lastGroupMsgSig='';
1688
+ hideNewMsgTip();
1589
1689
  D.title.textContent=name;
1590
1690
  D.groupInfoBar.style.display='flex';
1591
1691
  D.groupInfoText.textContent=name;
@@ -1598,11 +1698,11 @@ const chatHtml = `<!DOCTYPE html>
1598
1698
  $('groupReviewBtn').style.display='none';
1599
1699
  renderGroupList();
1600
1700
  try {
1601
- 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})});
1602
1702
  } catch(e){}
1603
1703
  // 获取群信息判断是否为创建者
1604
1704
  try {
1605
- 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));
1606
1706
  var d=await r.json();
1607
1707
  if(d.creator&&d.creator===S.aid){
1608
1708
  S.isGroupCreator=true;
@@ -1615,16 +1715,16 @@ const chatHtml = `<!DOCTYPE html>
1615
1715
  // 获取失败时默认显示复制群链接
1616
1716
  $('groupCopyLinkBtn').style.display='';
1617
1717
  }
1618
- pollGroupMessages();
1718
+ pollGroupMessages().then(function(){ scrollToBottom(); });
1619
1719
  }
1620
1720
 
1621
1721
  var _lastGroupMsgSig='';
1622
1722
  async function pollGroupMessages(){
1623
1723
  if(!S.activeGroupId||S.tab!=='group') return;
1624
1724
  try {
1625
- 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));
1626
1726
  var d=await r.json();
1627
- if(d.messages) renderGroupMsgs(d.messages);
1727
+ if(S.tab==='group'&&d.messages) renderGroupMsgs(d.messages);
1628
1728
  } catch(e){}
1629
1729
  }
1630
1730
 
@@ -1633,17 +1733,31 @@ const chatHtml = `<!DOCTYPE html>
1633
1733
  // ============================================================
1634
1734
  var _groupWs=null;
1635
1735
  var _groupWsReconnectTimer=null;
1736
+ var _groupWsReconnectDelay=1000; // exponential backoff start
1737
+ var _groupWsPingTimer=null;
1636
1738
 
1637
1739
  function connectGroupWs(){
1638
1740
  if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
1639
1741
  var proto=location.protocol==='https:'?'wss:':'ws:';
1640
- _groupWs=new WebSocket(proto+'//'+location.host+'/ws/group');
1742
+ _groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
1641
1743
  _groupWs.onopen=function(){
1642
- console.log('[WS] group connected');
1744
+ console.log('[WS] ui connected');
1745
+ _groupWsReconnectDelay=1000; // reset backoff on success
1643
1746
  if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
1644
- // 重连后主动拉取最新状态,防止断连期间丢失推送
1645
- 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(){});
1646
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);
1647
1761
  };
1648
1762
  _groupWs.onmessage=function(ev){
1649
1763
  try {
@@ -1652,12 +1766,17 @@ const chatHtml = `<!DOCTYPE html>
1652
1766
  } catch(e){ console.error('[WS] parse error',e); }
1653
1767
  };
1654
1768
  _groupWs.onclose=function(){
1655
- console.log('[WS] group disconnected, reconnecting in 3s...');
1769
+ console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
1656
1770
  _groupWs=null;
1657
- _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);
1658
1776
  };
1659
1777
  _groupWs.onerror=function(e){
1660
- 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
1661
1780
  };
1662
1781
  }
1663
1782
 
@@ -1671,6 +1790,17 @@ const chatHtml = `<!DOCTYPE html>
1671
1790
  renderAidSelect();
1672
1791
  return;
1673
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
+ }
1674
1804
  if(data.type==='group_message'){
1675
1805
  // 实时推送的完整消息
1676
1806
  var msg=data.message;
@@ -1713,17 +1843,18 @@ const chatHtml = `<!DOCTYPE html>
1713
1843
  }
1714
1844
  }
1715
1845
 
1716
- // 启动 WebSocket 连接
1717
- connectGroupWs();
1718
-
1719
1846
  var _lastGroupMsgs=[];
1720
1847
  function renderGroupMsgs(msgs){
1848
+ // 不在群组 tab 时不渲染,防止覆盖 P2P 消息
1849
+ if(S.tab!=='group') return;
1721
1850
  var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
1722
1851
  if(_lastGroupMsgSig===sig&&!msgs._forceRender) return;
1852
+ var prevCount=_lastGroupMsgs.length;
1723
1853
  _lastGroupMsgSig=sig;
1724
1854
  _lastGroupMsgs=msgs;
1725
1855
  if(!msgs.length){
1726
- 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');
1727
1858
  return;
1728
1859
  }
1729
1860
  var needFetch=[];
@@ -1740,17 +1871,26 @@ const chatHtml = `<!DOCTYPE html>
1740
1871
  return '<div class="message '+(sent?'sent':'received')+'">' +
1741
1872
  '<img class="msg-avatar" src="'+avatarSrc+'" title="'+escH(name)+'">' +
1742
1873
  '<div class="msg-content">' +
1743
- '<div class="bubble">'+c+'</div>' +
1744
1874
  '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1875
+ '<div class="bubble">'+c+'</div>' +
1745
1876
  '</div></div>';
1746
1877
  }).join('');
1747
- var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1748
- D.msgs.innerHTML=html;
1749
- if(wasAtBottom) 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
+ }
1750
1889
  // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
1751
1890
  var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
1752
1891
  unique.forEach(function(aid){
1753
1892
  fetchAgentInfo(aid).then(function(){
1893
+ if(S.tab!=='group') return;
1754
1894
  _lastGroupMsgSig='';
1755
1895
  _lastGroupMsgs._forceRender=true;
1756
1896
  renderGroupMsgs(_lastGroupMsgs);
@@ -1769,6 +1909,7 @@ const chatHtml = `<!DOCTYPE html>
1769
1909
  var name=$('groupNameInput').value.trim();
1770
1910
  if(!name) return;
1771
1911
  var description=$('groupDescInput').value.trim();
1912
+ if(!description){ $('groupDescInput').focus(); return; }
1772
1913
  var visibility=document.querySelector('input[name="groupVisibility"]:checked').value;
1773
1914
  var btn=$('createGroupBtn');
1774
1915
  btn.disabled=true; btn.textContent='创建中...';
@@ -1799,7 +1940,7 @@ const chatHtml = `<!DOCTYPE html>
1799
1940
  var btn=$('joinGroupBtn');
1800
1941
  btn.disabled=true; btn.textContent=code?'加入中...':'申请中...';
1801
1942
  try {
1802
- 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})});
1803
1944
  var d=await r.json();
1804
1945
  if(d.success){
1805
1946
  hideJoinGroupModal();
@@ -1821,7 +1962,7 @@ const chatHtml = `<!DOCTYPE html>
1821
1962
  async function generateInviteLink(){
1822
1963
  if(!S.activeGroupId) return;
1823
1964
  try {
1824
- 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})});
1825
1966
  var d=await r.json();
1826
1967
  if(d.success&&d.code){
1827
1968
  var baseUrl=d.group_url||('https://'+S.groupTargetAid+'/'+S.activeGroupId);
@@ -1895,7 +2036,7 @@ const chatHtml = `<!DOCTYPE html>
1895
2036
  async function showGroupMembers(){
1896
2037
  if(!S.activeGroupId) return;
1897
2038
  try {
1898
- 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));
1899
2040
  var d=await r.json();
1900
2041
  if(d.members){
1901
2042
  var html=d.members.map(function(m){
@@ -1966,7 +2107,7 @@ const chatHtml = `<!DOCTYPE html>
1966
2107
  async function showPendingRequests(){
1967
2108
  if(!S.activeGroupId) return;
1968
2109
  try {
1969
- 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));
1970
2111
  var d=await r.json();
1971
2112
  if(d.requests&&d.requests.length>0){
1972
2113
  // 先渲染基础结构,然后异步加载 agent info
@@ -2020,7 +2161,7 @@ const chatHtml = `<!DOCTYPE html>
2020
2161
  async function reviewJoin(agentId,action){
2021
2162
  if(!S.activeGroupId) return;
2022
2163
  try {
2023
- 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})});
2024
2165
  var d=await r.json();
2025
2166
  if(d.success){ showPendingRequests(); }
2026
2167
  else { alert(d.error||'操作失败'); }
@@ -2030,7 +2171,7 @@ const chatHtml = `<!DOCTYPE html>
2030
2171
  async function leaveGroup(groupId){
2031
2172
  if(!confirm('确认退出该群组?')) return;
2032
2173
  try {
2033
- 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})});
2034
2175
  var d=await r.json();
2035
2176
  if(d.success){
2036
2177
  if(S.activeGroupId===groupId){
@@ -2054,7 +2195,7 @@ const chatHtml = `<!DOCTYPE html>
2054
2195
  showMyGroupsModal();
2055
2196
  $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#999;">加载中...</div>';
2056
2197
  try {
2057
- var r=await fetch('/api/group/my-groups');
2198
+ var r=await fetch('/api/group/my-groups?aid='+encodeURIComponent(S.aid));
2058
2199
  var d=await r.json();
2059
2200
  if(!d.success){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">'+escH(d.error||'获取失败')+'</div>'; return; }
2060
2201
  var groups=d.groups||[];
@@ -2192,7 +2333,7 @@ async function handleRequest(req, res) {
2192
2333
  try {
2193
2334
  const aidList = await datamanager_1.CertAndKeyStore.getAids();
2194
2335
  const aidStatus = await getAidStatusList();
2195
- sendJson(res, { currentAid, aidList, aidStatus, apiUrl: globalApiUrl });
2336
+ sendJson(res, { aidList, aidStatus, apiUrl: globalApiUrl });
2196
2337
  }
2197
2338
  catch (e) {
2198
2339
  sendJson(res, { error: e.message }, 500);
@@ -2218,11 +2359,8 @@ async function handleRequest(req, res) {
2218
2359
  }
2219
2360
  const loaded = await agentCP.loadAid(aid);
2220
2361
  if (loaded) {
2221
- const oldAid = currentAid;
2222
- currentAid = aid;
2223
- // 切换会话:保存旧 AID 的会话,加载新 AID 的会话
2224
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
2225
- activeSessionId = null;
2362
+ // 加载该 AID 的持久化会话
2363
+ await ensureMessageStoreLoaded(aid);
2226
2364
  // 切换身份时自动上线(含群组初始化),A/B 同时保持在线
2227
2365
  try {
2228
2366
  await ensureOnline(aid);
@@ -2270,7 +2408,6 @@ async function handleRequest(req, res) {
2270
2408
  }
2271
2409
  try {
2272
2410
  const created = await agentCP.createAid(aid);
2273
- currentAid = created;
2274
2411
  // 保存自定义昵称和描述
2275
2412
  const nickname = (body.nickname || '').trim();
2276
2413
  const description = (body.description || '').trim();
@@ -2359,7 +2496,7 @@ async function handleRequest(req, res) {
2359
2496
  if (pathname === '/api/ws/start' && method === 'POST') {
2360
2497
  try {
2361
2498
  const body = await parseBody(req);
2362
- const targetAid = body.aid || currentAid;
2499
+ const targetAid = body.aid;
2363
2500
  if (!targetAid) {
2364
2501
  sendJson(res, { success: false, error: '请先选择 AID' });
2365
2502
  return;
@@ -2375,9 +2512,13 @@ async function handleRequest(req, res) {
2375
2512
  if (pathname === '/api/ws/connect' && method === 'POST') {
2376
2513
  try {
2377
2514
  const body = await parseBody(req);
2378
- const targetAid = body.targetAid;
2515
+ const { targetAid, aid } = body;
2516
+ if (!aid) {
2517
+ sendJson(res, { success: false, error: '缺少 aid' });
2518
+ return;
2519
+ }
2379
2520
  // 自动上线
2380
- const instance = await ensureOnline();
2521
+ const instance = await ensureOnline(aid);
2381
2522
  if (!instance.agentWS) {
2382
2523
  sendJson(res, { success: false, error: '自动上线失败' });
2383
2524
  return;
@@ -2400,9 +2541,8 @@ async function handleRequest(req, res) {
2400
2541
  console.log('邀请状态:', status);
2401
2542
  });
2402
2543
  });
2403
- // 创建 outgoing session 并设为活跃
2404
- getMessageStore().getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', currentAid);
2405
- activeSessionId = result.sessionId;
2544
+ // 创建 outgoing session
2545
+ getMessageStoreForAid(aid).getOrCreateSession(result.sessionId, result.identifyingCode, targetAid, 'outgoing', aid);
2406
2546
  sendJson(res, Object.assign({ success: true }, result));
2407
2547
  }
2408
2548
  catch (e) {
@@ -2413,24 +2553,26 @@ async function handleRequest(req, res) {
2413
2553
  if (pathname === '/api/ws/send' && method === 'POST') {
2414
2554
  try {
2415
2555
  const body = await parseBody(req);
2416
- const { message, sessionId } = body;
2556
+ const { message, sessionId, aid } = body;
2417
2557
  if (!message) {
2418
2558
  sendJson(res, { success: false, error: '消息不能为空' });
2419
2559
  return;
2420
2560
  }
2561
+ if (!aid) {
2562
+ sendJson(res, { success: false, error: '缺少 aid' });
2563
+ return;
2564
+ }
2421
2565
  // 自动上线
2422
- const instance = await ensureOnline();
2566
+ const instance = await ensureOnline(aid);
2423
2567
  if (!instance.agentWS) {
2424
2568
  sendJson(res, { success: false, error: '自动上线失败' });
2425
2569
  return;
2426
2570
  }
2427
- // 使用指定的 sessionId 或当前活跃会话
2428
- const sid = sessionId || activeSessionId;
2429
- if (!sid) {
2430
- sendJson(res, { success: false, error: '没有活跃会话' });
2571
+ if (!sessionId) {
2572
+ sendJson(res, { success: false, error: '缺少 sessionId' });
2431
2573
  return;
2432
2574
  }
2433
- const session = getMessageStore().getSession(sid);
2575
+ const session = getMessageStoreForAid(aid).getSession(sessionId);
2434
2576
  if (!session) {
2435
2577
  sendJson(res, { success: false, error: '会话不存在' });
2436
2578
  return;
@@ -2440,7 +2582,7 @@ async function handleRequest(req, res) {
2440
2582
  return;
2441
2583
  }
2442
2584
  instance.agentWS.send(message, session.peerAid, session.sessionId, session.identifyingCode);
2443
- getMessageStore().addMessageToSession(sid, {
2585
+ getMessageStoreForAid(aid).addMessageToSession(sessionId, {
2444
2586
  type: 'sent',
2445
2587
  content: message,
2446
2588
  to: session.peerAid,
@@ -2454,12 +2596,20 @@ async function handleRequest(req, res) {
2454
2596
  return;
2455
2597
  }
2456
2598
  if (pathname === '/api/messages' && method === 'GET') {
2457
- const session = activeSessionId ? getMessageStore().getSession(activeSessionId) : null;
2458
- 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 });
2459
2608
  return;
2460
2609
  }
2461
2610
  if (pathname === '/api/ws/status' && method === 'GET') {
2462
- const instance = getActiveInstance();
2611
+ const aid = parsedUrl.query.aid;
2612
+ const instance = aid ? aidInstances.get(aid) : null;
2463
2613
  if (!instance || !instance.agentWS) {
2464
2614
  sendJson(res, { connected: false, status: 'disconnected' });
2465
2615
  }
@@ -2469,19 +2619,26 @@ async function handleRequest(req, res) {
2469
2619
  return;
2470
2620
  }
2471
2621
  if (pathname === '/api/sessions' && method === 'GET') {
2472
- 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 });
2473
2629
  return;
2474
2630
  }
2475
2631
  if (pathname === '/api/sessions/active' && method === 'POST') {
2476
2632
  try {
2477
2633
  const body = await parseBody(req);
2478
- const { sessionId } = body;
2479
- if (!sessionId || !getMessageStore().hasSession(sessionId)) {
2634
+ const { sessionId, aid } = body;
2635
+ if (!aid || !sessionId || !getMessageStoreForAid(aid).hasSession(sessionId)) {
2480
2636
  sendJson(res, { success: false, error: '会话不存在' });
2481
2637
  return;
2482
2638
  }
2483
- activeSessionId = sessionId;
2484
- sendJson(res, { success: true, activeSessionId });
2639
+ // 通知绑定了该 aid 的客户端更新 activeSessionId
2640
+ pushToAid(aid, { type: 'set_active_session', sessionId });
2641
+ sendJson(res, { success: true, activeSessionId: sessionId });
2485
2642
  }
2486
2643
  catch (e) {
2487
2644
  sendJson(res, { success: false, error: e.message });
@@ -2491,15 +2648,17 @@ async function handleRequest(req, res) {
2491
2648
  if (pathname === '/api/sessions/delete' && method === 'POST') {
2492
2649
  try {
2493
2650
  const body = await parseBody(req);
2494
- const { sessionId } = body;
2651
+ const { sessionId, aid } = body;
2495
2652
  if (!sessionId) {
2496
2653
  sendJson(res, { success: false, error: '会话ID不能为空' });
2497
2654
  return;
2498
2655
  }
2499
- 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);
2500
2661
  if (deleted) {
2501
- if (activeSessionId === sessionId)
2502
- activeSessionId = null;
2503
2662
  sendJson(res, { success: true });
2504
2663
  }
2505
2664
  else {
@@ -2514,18 +2673,16 @@ async function handleRequest(req, res) {
2514
2673
  if (pathname === '/api/peers/delete' && method === 'POST') {
2515
2674
  try {
2516
2675
  const body = await parseBody(req);
2517
- const { peerAid } = body;
2676
+ const { peerAid, aid } = body;
2518
2677
  if (!peerAid) {
2519
2678
  sendJson(res, { success: false, error: 'AID不能为空' });
2520
2679
  return;
2521
2680
  }
2522
- const count = await getMessageStore().deletePeer(peerAid, currentAid);
2523
- if (activeSessionId) {
2524
- const session = getMessageStore().getSession(activeSessionId);
2525
- if (!session || session.peerAid === peerAid) {
2526
- activeSessionId = null;
2527
- }
2681
+ if (!aid) {
2682
+ sendJson(res, { success: false, error: '缺少 aid' });
2683
+ return;
2528
2684
  }
2685
+ const count = await getMessageStoreForAid(aid).deletePeer(peerAid, aid);
2529
2686
  sendJson(res, { success: true, count });
2530
2687
  }
2531
2688
  catch (e) {
@@ -2538,7 +2695,13 @@ async function handleRequest(req, res) {
2538
2695
  // ============================================================
2539
2696
  if (pathname === '/api/group/init' && method === 'POST') {
2540
2697
  try {
2541
- 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);
2542
2705
  await ensureGroupClient(instance);
2543
2706
  sendJson(res, { success: true, targetAid: instance.groupTargetAid });
2544
2707
  }
@@ -2550,12 +2713,16 @@ async function handleRequest(req, res) {
2550
2713
  if (pathname === '/api/group/create' && method === 'POST') {
2551
2714
  try {
2552
2715
  const body = await parseBody(req);
2553
- 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
+ }
2554
2721
  if (!name) {
2555
2722
  sendJson(res, { success: false, error: '群组名称不能为空' });
2556
2723
  return;
2557
2724
  }
2558
- const instance = await ensureOnline();
2725
+ const instance = await ensureOnline(aid);
2559
2726
  await ensureGroupClient(instance);
2560
2727
  const ops = instance.agentCP.groupOps;
2561
2728
  const target = instance.groupTargetAid;
@@ -2568,6 +2735,8 @@ async function handleRequest(req, res) {
2568
2735
  console.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
2569
2736
  // 加入本地存储
2570
2737
  instance.agentCP.addGroupToStore(result.group_id, name);
2738
+ // 注册在线,才能收到实时消息推送
2739
+ await instance.agentCP.joinGroupSession(result.group_id);
2571
2740
  sendJson(res, Object.assign({ success: true }, result));
2572
2741
  }
2573
2742
  catch (e) {
@@ -2577,7 +2746,12 @@ async function handleRequest(req, res) {
2577
2746
  }
2578
2747
  if (pathname === '/api/group/list' && method === 'GET') {
2579
2748
  try {
2580
- 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);
2581
2755
  await ensureGroupClient(instance);
2582
2756
  // 首次访问时从服务端同步群组列表
2583
2757
  if (!instance.groupListSynced) {
@@ -2600,8 +2774,12 @@ async function handleRequest(req, res) {
2600
2774
  if (pathname === '/api/group/select' && method === 'POST') {
2601
2775
  try {
2602
2776
  const body = await parseBody(req);
2603
- const { groupId } = body;
2604
- 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);
2605
2783
  instance.activeGroupId = groupId || null;
2606
2784
  sendJson(res, { success: true });
2607
2785
  }
@@ -2613,11 +2791,16 @@ async function handleRequest(req, res) {
2613
2791
  if (pathname === '/api/group/info' && method === 'GET') {
2614
2792
  try {
2615
2793
  const groupId = parsedUrl.query.groupId;
2794
+ const aid = parsedUrl.query.aid;
2616
2795
  if (!groupId) {
2617
2796
  sendJson(res, { success: false, error: '缺少 groupId' });
2618
2797
  return;
2619
2798
  }
2620
- const instance = await ensureOnline();
2799
+ if (!aid) {
2800
+ sendJson(res, { success: false, error: '缺少 aid' });
2801
+ return;
2802
+ }
2803
+ const instance = await ensureOnline(aid);
2621
2804
  await ensureGroupClient(instance);
2622
2805
  const info = await instance.agentCP.groupOps.getGroupInfo(instance.groupTargetAid, groupId);
2623
2806
  sendJson(res, Object.assign({ success: true }, info));
@@ -2630,12 +2813,16 @@ async function handleRequest(req, res) {
2630
2813
  if (pathname === '/api/group/send' && method === 'POST') {
2631
2814
  try {
2632
2815
  const body = await parseBody(req);
2633
- const { groupId, message } = body;
2816
+ const { groupId, message, aid } = body;
2634
2817
  if (!groupId || !message) {
2635
2818
  sendJson(res, { success: false, error: '缺少 groupId 或 message' });
2636
2819
  return;
2637
2820
  }
2638
- const instance = await ensureOnline();
2821
+ if (!aid) {
2822
+ sendJson(res, { success: false, error: '缺少 aid' });
2823
+ return;
2824
+ }
2825
+ const instance = await ensureOnline(aid);
2639
2826
  await ensureGroupClient(instance);
2640
2827
  const result = await instance.agentCP.groupOps.sendGroupMessage(instance.groupTargetAid, groupId, message, 'text');
2641
2828
  // 添加到本地存储
@@ -2656,7 +2843,12 @@ async function handleRequest(req, res) {
2656
2843
  if (pathname === '/api/group/messages' && method === 'GET') {
2657
2844
  try {
2658
2845
  const groupId = parsedUrl.query.groupId || '';
2659
- 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);
2660
2852
  if (!groupId) {
2661
2853
  sendJson(res, { success: true, messages: [] });
2662
2854
  return;
@@ -2675,12 +2867,16 @@ async function handleRequest(req, res) {
2675
2867
  if (pathname === '/api/group/invite-code' && method === 'POST') {
2676
2868
  try {
2677
2869
  const body = await parseBody(req);
2678
- const { groupId } = body;
2870
+ const { groupId, aid } = body;
2679
2871
  if (!groupId) {
2680
2872
  sendJson(res, { success: false, error: '缺少 groupId' });
2681
2873
  return;
2682
2874
  }
2683
- const instance = await ensureOnline();
2875
+ if (!aid) {
2876
+ sendJson(res, { success: false, error: '缺少 aid' });
2877
+ return;
2878
+ }
2879
+ const instance = await ensureOnline(aid);
2684
2880
  await ensureGroupClient(instance);
2685
2881
  const result = await instance.agentCP.groupOps.createInviteCode(instance.groupTargetAid, groupId, body.options);
2686
2882
  const groupUrl = `https://${instance.groupTargetAid}/${groupId}`;
@@ -2694,17 +2890,20 @@ async function handleRequest(req, res) {
2694
2890
  if (pathname === '/api/group/join' && method === 'POST') {
2695
2891
  try {
2696
2892
  const body = await parseBody(req);
2697
- const { groupUrl, code } = body;
2893
+ const { groupUrl, code, aid } = body;
2698
2894
  if (!groupUrl) {
2699
2895
  sendJson(res, { success: false, error: '缺少群聊链接' });
2700
2896
  return;
2701
2897
  }
2898
+ if (!aid) {
2899
+ sendJson(res, { success: false, error: '缺少 aid' });
2900
+ return;
2901
+ }
2702
2902
  const { targetAid, groupId } = group_1.GroupOperations.parseGroupUrl(groupUrl);
2703
- const instance = await ensureOnline();
2903
+ const instance = await ensureOnline(aid);
2704
2904
  await ensureGroupClient(instance);
2705
- if (code) {
2706
- // 免审核:邀请码加入
2707
- await instance.agentCP.groupOps.useInviteCode(targetAid, groupId, code);
2905
+ // 加入成功后的统一处理:获取群名、写入本地存储、注册在线会话
2906
+ const finalizeJoin = async () => {
2708
2907
  let groupName = groupId;
2709
2908
  try {
2710
2909
  const info = await instance.agentCP.groupOps.getGroupInfo(targetAid, groupId);
@@ -2712,12 +2911,26 @@ async function handleRequest(req, res) {
2712
2911
  }
2713
2912
  catch (_) { }
2714
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();
2715
2920
  sendJson(res, { success: true, group_id: groupId });
2716
2921
  }
2717
2922
  else {
2718
- // 审核模式:发送入群申请
2719
- const requestId = await instance.agentCP.groupOps.requestJoin(targetAid, groupId, body.message || '');
2720
- 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
+ }
2721
2934
  }
2722
2935
  }
2723
2936
  catch (e) {
@@ -2728,11 +2941,16 @@ async function handleRequest(req, res) {
2728
2941
  if (pathname === '/api/group/pending-requests' && method === 'GET') {
2729
2942
  try {
2730
2943
  const groupId = parsedUrl.query.groupId;
2944
+ const aid = parsedUrl.query.aid;
2731
2945
  if (!groupId) {
2732
2946
  sendJson(res, { success: false, error: '缺少 groupId' });
2733
2947
  return;
2734
2948
  }
2735
- const instance = await ensureOnline();
2949
+ if (!aid) {
2950
+ sendJson(res, { success: false, error: '缺少 aid' });
2951
+ return;
2952
+ }
2953
+ const instance = await ensureOnline(aid);
2736
2954
  await ensureGroupClient(instance);
2737
2955
  const result = await instance.agentCP.groupOps.getPendingRequests(instance.groupTargetAid, groupId);
2738
2956
  sendJson(res, Object.assign({ success: true }, result));
@@ -2745,12 +2963,16 @@ async function handleRequest(req, res) {
2745
2963
  if (pathname === '/api/group/review-join' && method === 'POST') {
2746
2964
  try {
2747
2965
  const body = await parseBody(req);
2748
- const { groupId, agentId, action } = body;
2966
+ const { groupId, agentId, action, aid } = body;
2749
2967
  if (!groupId || !agentId || !action) {
2750
2968
  sendJson(res, { success: false, error: '缺少参数' });
2751
2969
  return;
2752
2970
  }
2753
- const instance = await ensureOnline();
2971
+ if (!aid) {
2972
+ sendJson(res, { success: false, error: '缺少 aid' });
2973
+ return;
2974
+ }
2975
+ const instance = await ensureOnline(aid);
2754
2976
  await ensureGroupClient(instance);
2755
2977
  await instance.agentCP.groupOps.reviewJoinRequest(instance.groupTargetAid, groupId, agentId, action, body.reason || '');
2756
2978
  sendJson(res, { success: true });
@@ -2763,11 +2985,16 @@ async function handleRequest(req, res) {
2763
2985
  if (pathname === '/api/group/members' && method === 'GET') {
2764
2986
  try {
2765
2987
  const groupId = parsedUrl.query.groupId;
2988
+ const aid = parsedUrl.query.aid;
2766
2989
  if (!groupId) {
2767
2990
  sendJson(res, { success: false, error: '缺少 groupId' });
2768
2991
  return;
2769
2992
  }
2770
- const instance = await ensureOnline();
2993
+ if (!aid) {
2994
+ sendJson(res, { success: false, error: '缺少 aid' });
2995
+ return;
2996
+ }
2997
+ const instance = await ensureOnline(aid);
2771
2998
  await ensureGroupClient(instance);
2772
2999
  const result = await instance.agentCP.groupOps.getMembers(instance.groupTargetAid, groupId);
2773
3000
  sendJson(res, Object.assign({ success: true }, result));
@@ -2779,7 +3006,12 @@ async function handleRequest(req, res) {
2779
3006
  }
2780
3007
  if (pathname === '/api/group/my-groups' && method === 'GET') {
2781
3008
  try {
2782
- 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);
2783
3015
  await ensureGroupClient(instance);
2784
3016
  const ops = instance.agentCP.groupOps;
2785
3017
  const target = instance.groupTargetAid;
@@ -2805,12 +3037,16 @@ async function handleRequest(req, res) {
2805
3037
  if (pathname === '/api/group/leave' && method === 'POST') {
2806
3038
  try {
2807
3039
  const body = await parseBody(req);
2808
- const { groupId } = body;
3040
+ const { groupId, aid } = body;
2809
3041
  if (!groupId) {
2810
3042
  sendJson(res, { success: false, error: '缺少 groupId' });
2811
3043
  return;
2812
3044
  }
2813
- const instance = await ensureOnline();
3045
+ if (!aid) {
3046
+ sendJson(res, { success: false, error: '缺少 aid' });
3047
+ return;
3048
+ }
3049
+ const instance = await ensureOnline(aid);
2814
3050
  await ensureGroupClient(instance);
2815
3051
  await instance.agentCP.groupOps.leaveGroup(instance.groupTargetAid, groupId);
2816
3052
  await instance.agentCP.removeGroupFromStore(groupId);
@@ -2862,22 +3098,58 @@ function startServer(port, apiUrl, dataDir = '') {
2862
3098
  await agentCP.loadCurrentAid();
2863
3099
  }
2864
3100
  }
2865
- currentAid = aid;
2866
3101
  console.log(`已加载 AID: ${aid}`);
2867
3102
  // 加载该 AID 的持久化会话
2868
- await getMessageStoreForAid(aid).loadSessionsForAid(aid);
3103
+ await ensureMessageStoreLoaded(aid);
2869
3104
  console.log(`已加载会话`);
2870
3105
  }
2871
3106
  }).catch(() => { });
2872
3107
  const server = http.createServer(handleRequest);
2873
3108
  // WebSocket server for browser ↔ server real-time communication
2874
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);
2875
3123
  server.on('upgrade', (req, socket, head) => {
2876
3124
  const pathname = url.parse(req.url || '', true).pathname;
2877
- if (pathname === '/ws/group') {
3125
+ if (pathname === '/ws/ui' || pathname === '/ws/group') {
2878
3126
  wss.handleUpgrade(req, socket, head, (ws) => {
2879
- 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));
2880
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
+ });
2881
3153
  ws.on('close', () => {
2882
3154
  browserWsClients.delete(ws);
2883
3155
  console.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
@@ -2895,6 +3167,7 @@ function startServer(port, apiUrl, dataDir = '') {
2895
3167
  // 资源清理函数
2896
3168
  const cleanup = async () => {
2897
3169
  console.log('\n正在关闭服务...');
3170
+ clearInterval(wssHeartbeat);
2898
3171
  // 持久化 agent info 缓存
2899
3172
  saveAgentInfoCacheToDisk();
2900
3173
  // 持久化当前会话
@@ -2932,8 +3205,8 @@ function startServer(port, apiUrl, dataDir = '') {
2932
3205
  }
2933
3206
  aidInstances.clear();
2934
3207
  // 关闭所有浏览器 WS 连接
2935
- for (const client of browserWsClients) {
2936
- client.close();
3208
+ for (const [ws] of browserWsClients) {
3209
+ ws.close();
2937
3210
  }
2938
3211
  browserWsClients.clear();
2939
3212
  wss.close();