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/agentcp.js +3 -0
- package/dist/group/events.d.ts +2 -1
- package/dist/group/events.js +25 -1
- package/dist/group/index.d.ts +1 -1
- package/dist/group/index.js +2 -1
- package/dist/group/operations.d.ts +28 -6
- package/dist/group/operations.js +62 -6
- package/dist/group/types.d.ts +56 -1
- package/dist/group/types.js +15 -1
- package/dist/server.js +506 -231
- package/package.json +1 -1
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
66
|
-
if (
|
|
67
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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: #
|
|
585
|
-
.container { background: white; padding:
|
|
586
|
-
|
|
587
|
-
.
|
|
588
|
-
.
|
|
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 #
|
|
591
|
-
.create-section .aid-input-row input:focus { outline: none; border-color: #
|
|
592
|
-
.create-section .aid-input-row .dot-separator { color: #
|
|
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 #
|
|
619
|
+
border: 1px solid #e2e8f0;
|
|
596
620
|
border-radius: 8px;
|
|
597
621
|
font-size: 14px;
|
|
598
|
-
background:
|
|
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: #
|
|
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 #
|
|
612
|
-
.create-section .extra-fields input:focus { outline: none; border-color: #
|
|
613
|
-
.btn { display: block; width: 100%; padding:
|
|
614
|
-
.btn-primary { background: #
|
|
615
|
-
.btn-primary:hover { background: #
|
|
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: #
|
|
618
|
-
.btn-success:hover { background: #
|
|
619
|
-
.btn-danger { background: #
|
|
620
|
-
.btn-danger:hover { background: #
|
|
621
|
-
.btn-outline { background: white; color: #
|
|
622
|
-
.btn-outline:hover { background: #
|
|
623
|
-
.btn-outline.active { background: #
|
|
624
|
-
.btn:disabled { background: #
|
|
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: #
|
|
627
|
-
.aid-card
|
|
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: #
|
|
632
|
-
.copy-btn { background: none; border: none; color: #
|
|
633
|
-
.copy-btn:hover { color: #
|
|
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: #
|
|
637
|
-
.badge-warning { background: #
|
|
638
|
-
.badge-danger { background: #
|
|
639
|
-
.badge-info { background: #
|
|
640
|
-
.badge-current { background: #
|
|
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:
|
|
643
|
-
.status.success { display: block; background: #
|
|
644
|
-
.status.error { display: block; background: #
|
|
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
|
-
<
|
|
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 = {
|
|
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
|
|
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 (
|
|
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-
|
|
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:
|
|
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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
//
|
|
1216
|
-
|
|
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
|
-
|
|
1219
|
-
fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
|
|
1220
|
-
|
|
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
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
|
|
1330
|
+
async function loadSessions(){
|
|
1331
|
+
if(!S.aid) return;
|
|
1273
1332
|
try {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1409
|
-
D.msgs.scrollTop
|
|
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.
|
|
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
|
-
|
|
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())
|
|
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
|
|
1600
|
+
return H+':'+M;
|
|
1510
1601
|
}
|
|
1511
|
-
|
|
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
|
-
//
|
|
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/
|
|
1742
|
+
_groupWs=new WebSocket(proto+'//'+location.host+'/ws/ui');
|
|
1640
1743
|
_groupWs.onopen=function(){
|
|
1641
|
-
console.log('[WS]
|
|
1744
|
+
console.log('[WS] ui connected');
|
|
1745
|
+
_groupWsReconnectDelay=1000; // reset backoff on success
|
|
1642
1746
|
if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
|
|
1643
|
-
//
|
|
1644
|
-
|
|
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]
|
|
1769
|
+
console.log('[WS] ui disconnected, reconnecting in '+_groupWsReconnectDelay+'ms...');
|
|
1655
1770
|
_groupWs=null;
|
|
1656
|
-
|
|
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]
|
|
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
|
-
|
|
1747
|
-
D.msgs.scrollTop
|
|
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, {
|
|
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
|
-
|
|
2220
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2426
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
2456
|
-
|
|
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
|
|
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
|
-
|
|
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 || !
|
|
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
|
-
|
|
2482
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
2934
|
-
|
|
3208
|
+
for (const [ws] of browserWsClients) {
|
|
3209
|
+
ws.close();
|
|
2935
3210
|
}
|
|
2936
3211
|
browserWsClients.clear();
|
|
2937
3212
|
wss.close();
|