acp-ts 1.1.7 → 1.1.9
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.d.ts +55 -3
- package/dist/agentcp.js +146 -10
- package/dist/group/client.js +30 -5
- package/dist/group/operations.d.ts +22 -1
- package/dist/group/operations.js +39 -3
- package/dist/server.js +298 -36
- package/dist/websocket.js +0 -5
- package/package.json +1 -1
package/dist/agentcp.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ declare class AgentCP implements IAgentCP {
|
|
|
19
19
|
private _groupTargetAid;
|
|
20
20
|
private _groupSessionId;
|
|
21
21
|
private _persistGroupMessages;
|
|
22
|
+
private _onlineGroups;
|
|
23
|
+
private _heartbeatTimer;
|
|
24
|
+
private _heartbeatIntervalMs;
|
|
22
25
|
get messageStore(): MessageStore;
|
|
23
26
|
constructor(apiUrl: string, seedPassword?: string, basePath?: string, options?: {
|
|
24
27
|
persistMessages?: boolean;
|
|
@@ -86,16 +89,23 @@ declare class AgentCP implements IAgentCP {
|
|
|
86
89
|
*/
|
|
87
90
|
setGroupEventHandler(handler: ACPGroupEventHandler): void;
|
|
88
91
|
/**
|
|
89
|
-
*
|
|
92
|
+
* 创建默认的群组事件处理器。
|
|
93
|
+
* - onGroupMessage: 自动存储消息到 GroupMessageStore + 自动 ACK
|
|
94
|
+
* - onJoinApproved: 自动注册到 Home AP + 存储群组
|
|
90
95
|
* 外部可通过 setGroupEventHandler 覆盖。
|
|
91
96
|
*/
|
|
92
97
|
private _createDefaultGroupEventHandler;
|
|
98
|
+
/**
|
|
99
|
+
* 自动 ACK 消息(SDK 内部行为,对上层透明)
|
|
100
|
+
*/
|
|
101
|
+
private _autoAckMessage;
|
|
93
102
|
/**
|
|
94
103
|
* 设置群组游标存储
|
|
95
104
|
*/
|
|
96
105
|
setGroupCursorStore(store: CursorStore): void;
|
|
97
106
|
/**
|
|
98
|
-
*
|
|
107
|
+
* 关闭群组客户端。
|
|
108
|
+
* 自动停止心跳、清理在线群组状态。
|
|
99
109
|
*/
|
|
100
110
|
closeGroupClient(): void;
|
|
101
111
|
/**
|
|
@@ -158,9 +168,51 @@ declare class AgentCP implements IAgentCP {
|
|
|
158
168
|
getGroupLastMsgId(groupId: string): number;
|
|
159
169
|
/**
|
|
160
170
|
* 从服务端拉取新消息并同步到本地存储。
|
|
171
|
+
* 循环拉取直到 has_more=false,每批拉取后自动 ACK。
|
|
161
172
|
* 返回所有本地缓存的消息(包括新拉取的)。
|
|
173
|
+
*
|
|
174
|
+
* @param groupId 群组 ID
|
|
175
|
+
* @param afterMsgId 从哪条消息之后开始拉取,0 表示使用服务端自动游标模式
|
|
176
|
+
* @param limit 每次拉取数量上限,0 表示使用服务端默认值
|
|
177
|
+
*/
|
|
178
|
+
pullAndStoreGroupMessages(groupId: string, afterMsgId?: number, limit?: number): Promise<GroupMessage[]>;
|
|
179
|
+
/**
|
|
180
|
+
* 加入群组会话(完整生命周期):
|
|
181
|
+
* 1. register_online → 告知 group.ap 在线
|
|
182
|
+
* 2. 将群组加入在线列表
|
|
183
|
+
* 3. 启动心跳定时器(首次时启动)
|
|
184
|
+
*/
|
|
185
|
+
joinGroupSession(groupId: string): Promise<void>;
|
|
186
|
+
/**
|
|
187
|
+
* 离开群组会话(优雅退出):
|
|
188
|
+
* 1. 从在线群组列表移除
|
|
189
|
+
* 2. 如果没有在线群组了,unregister_online + 停止心跳定时器
|
|
190
|
+
*/
|
|
191
|
+
leaveGroupSession(groupId: string): Promise<void>;
|
|
192
|
+
/**
|
|
193
|
+
* 离开所有群组会话
|
|
194
|
+
*/
|
|
195
|
+
leaveAllGroupSessions(): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* 获取当前在线群组列表
|
|
198
|
+
*/
|
|
199
|
+
getOnlineGroups(): string[];
|
|
200
|
+
/**
|
|
201
|
+
* 设置心跳间隔(毫秒),默认 180000(3 分钟)
|
|
202
|
+
*/
|
|
203
|
+
setHeartbeatInterval(intervalMs: number): void;
|
|
204
|
+
/**
|
|
205
|
+
* 确保心跳定时器已启动
|
|
206
|
+
*/
|
|
207
|
+
private _ensureHeartbeat;
|
|
208
|
+
/**
|
|
209
|
+
* 停止心跳定时器
|
|
210
|
+
*/
|
|
211
|
+
private _stopHeartbeat;
|
|
212
|
+
/**
|
|
213
|
+
* 发送心跳保活
|
|
162
214
|
*/
|
|
163
|
-
|
|
215
|
+
private _sendHeartbeats;
|
|
164
216
|
/**
|
|
165
217
|
* 关闭群消息存储,刷新所有未写入的数据
|
|
166
218
|
*/
|
package/dist/agentcp.js
CHANGED
|
@@ -62,6 +62,10 @@ class AgentCP {
|
|
|
62
62
|
this._groupTargetAid = '';
|
|
63
63
|
this._groupSessionId = '';
|
|
64
64
|
this._persistGroupMessages = false;
|
|
65
|
+
// Group lifecycle management
|
|
66
|
+
this._onlineGroups = new Set();
|
|
67
|
+
this._heartbeatTimer = null;
|
|
68
|
+
this._heartbeatIntervalMs = 180000; // 3 minutes
|
|
65
69
|
if (!apiUrl) {
|
|
66
70
|
this.handleError("参数缺失:apiUrl不应为空");
|
|
67
71
|
}
|
|
@@ -369,11 +373,9 @@ class AgentCP {
|
|
|
369
373
|
}
|
|
370
374
|
const data = message.data || message;
|
|
371
375
|
const sender = (_a = data.sender) !== null && _a !== void 0 ? _a : "";
|
|
372
|
-
// console.log(`[Group] handleGroupMessage: sender="${sender}" targetAid="${this._groupTargetAid}" match=${sender === this._groupTargetAid}`);
|
|
373
376
|
if (sender === this._groupTargetAid) {
|
|
374
377
|
try {
|
|
375
378
|
const rawMsg = (_b = data.message) !== null && _b !== void 0 ? _b : "";
|
|
376
|
-
// console.log(`[Group] rawMsg type=${typeof rawMsg}, length=${typeof rawMsg === 'string' ? rawMsg.length : 'N/A'}, first 300 chars: ${typeof rawMsg === 'string' ? rawMsg.substring(0, 300) : JSON.stringify(rawMsg).substring(0, 300)}`);
|
|
377
379
|
if (typeof rawMsg === 'string' && rawMsg) {
|
|
378
380
|
this.groupClient.handleIncoming(rawMsg);
|
|
379
381
|
}
|
|
@@ -398,7 +400,9 @@ class AgentCP {
|
|
|
398
400
|
}
|
|
399
401
|
}
|
|
400
402
|
/**
|
|
401
|
-
*
|
|
403
|
+
* 创建默认的群组事件处理器。
|
|
404
|
+
* - onGroupMessage: 自动存储消息到 GroupMessageStore + 自动 ACK
|
|
405
|
+
* - onJoinApproved: 自动注册到 Home AP + 存储群组
|
|
402
406
|
* 外部可通过 setGroupEventHandler 覆盖。
|
|
403
407
|
*/
|
|
404
408
|
_createDefaultGroupEventHandler() {
|
|
@@ -441,14 +445,28 @@ class AgentCP {
|
|
|
441
445
|
onJoinRequestReceived(groupId, agentId, message) {
|
|
442
446
|
console.log(`[Group][DefaultHandler] onJoinRequestReceived: group=${groupId} agent=${agentId}`);
|
|
443
447
|
},
|
|
444
|
-
onGroupMessage(groupId, msg) {
|
|
448
|
+
onGroupMessage: (groupId, msg) => {
|
|
445
449
|
console.log(`[Group][DefaultHandler] onGroupMessage: group=${groupId} msgId=${msg.msg_id} sender=${msg.sender}`);
|
|
450
|
+
// 自动存储消息到本地
|
|
451
|
+
this.addGroupMessageToStore(groupId, msg);
|
|
452
|
+
// 自动 ACK(异步,不阻塞回调)
|
|
453
|
+
this._autoAckMessage(groupId, msg.msg_id);
|
|
446
454
|
},
|
|
447
455
|
onGroupEvent(groupId, evt) {
|
|
448
456
|
console.log(`[Group][DefaultHandler] onGroupEvent: group=${groupId} event=${evt.event_type}`);
|
|
449
457
|
},
|
|
450
458
|
};
|
|
451
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* 自动 ACK 消息(SDK 内部行为,对上层透明)
|
|
462
|
+
*/
|
|
463
|
+
_autoAckMessage(groupId, msgId) {
|
|
464
|
+
if (!this.groupOps || !this._groupTargetAid)
|
|
465
|
+
return;
|
|
466
|
+
this.groupOps.ackMessages(this._groupTargetAid, groupId, msgId).catch(e => {
|
|
467
|
+
console.warn(`[Group] auto ack failed: group=${groupId} msgId=${msgId}`, e.message || e);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
452
470
|
/**
|
|
453
471
|
* 设置群组游标存储
|
|
454
472
|
*/
|
|
@@ -458,9 +476,12 @@ class AgentCP {
|
|
|
458
476
|
}
|
|
459
477
|
}
|
|
460
478
|
/**
|
|
461
|
-
*
|
|
479
|
+
* 关闭群组客户端。
|
|
480
|
+
* 自动停止心跳、清理在线群组状态。
|
|
462
481
|
*/
|
|
463
482
|
closeGroupClient() {
|
|
483
|
+
this._stopHeartbeat();
|
|
484
|
+
this._onlineGroups.clear();
|
|
464
485
|
if (this.groupClient) {
|
|
465
486
|
try {
|
|
466
487
|
this.groupClient.close();
|
|
@@ -614,16 +635,24 @@ class AgentCP {
|
|
|
614
635
|
}
|
|
615
636
|
/**
|
|
616
637
|
* 从服务端拉取新消息并同步到本地存储。
|
|
638
|
+
* 循环拉取直到 has_more=false,每批拉取后自动 ACK。
|
|
617
639
|
* 返回所有本地缓存的消息(包括新拉取的)。
|
|
640
|
+
*
|
|
641
|
+
* @param groupId 群组 ID
|
|
642
|
+
* @param afterMsgId 从哪条消息之后开始拉取,0 表示使用服务端自动游标模式
|
|
643
|
+
* @param limit 每次拉取数量上限,0 表示使用服务端默认值
|
|
618
644
|
*/
|
|
619
|
-
async pullAndStoreGroupMessages(groupId, limit = 50) {
|
|
645
|
+
async pullAndStoreGroupMessages(groupId, afterMsgId = 0, limit = 50) {
|
|
620
646
|
if (!this.groupOps || !this._groupTargetAid) {
|
|
621
647
|
throw new Error('群组客户端未初始化');
|
|
622
648
|
}
|
|
623
|
-
|
|
649
|
+
let after = afterMsgId;
|
|
624
650
|
try {
|
|
625
|
-
|
|
626
|
-
|
|
651
|
+
while (true) {
|
|
652
|
+
const pulled = await this.groupOps.pullMessages(this._groupTargetAid, groupId, after, limit);
|
|
653
|
+
if (!pulled.messages || pulled.messages.length === 0) {
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
627
656
|
const msgs = pulled.messages.map(m => {
|
|
628
657
|
var _a, _b, _c, _d, _e, _f;
|
|
629
658
|
return ({
|
|
@@ -636,13 +665,120 @@ class AgentCP {
|
|
|
636
665
|
});
|
|
637
666
|
});
|
|
638
667
|
this.addGroupMessagesToStore(groupId, msgs);
|
|
668
|
+
// ACK 这批消息中的最后一条
|
|
669
|
+
const lastMsgId = msgs[msgs.length - 1].msg_id;
|
|
670
|
+
await this.groupOps.ackMessages(this._groupTargetAid, groupId, lastMsgId);
|
|
671
|
+
// 更新 after 用于下一轮拉取
|
|
672
|
+
after = lastMsgId;
|
|
673
|
+
if (!pulled.has_more) {
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
639
676
|
}
|
|
640
677
|
}
|
|
641
678
|
catch (e) {
|
|
642
|
-
console.warn('[AgentCP]
|
|
679
|
+
console.warn('[AgentCP] pullAndStoreGroupMessages error:', e.message);
|
|
643
680
|
}
|
|
644
681
|
return this.getLocalGroupMessages(groupId);
|
|
645
682
|
}
|
|
683
|
+
// ============================================================
|
|
684
|
+
// Group Session Lifecycle (register_online / pull / heartbeat / unregister)
|
|
685
|
+
// ============================================================
|
|
686
|
+
/**
|
|
687
|
+
* 加入群组会话(完整生命周期):
|
|
688
|
+
* 1. register_online → 告知 group.ap 在线
|
|
689
|
+
* 2. 将群组加入在线列表
|
|
690
|
+
* 3. 启动心跳定时器(首次时启动)
|
|
691
|
+
*/
|
|
692
|
+
async joinGroupSession(groupId) {
|
|
693
|
+
if (!this.groupOps || !this._groupTargetAid) {
|
|
694
|
+
throw new Error('群组客户端未初始化,请先调用 initGroupClient');
|
|
695
|
+
}
|
|
696
|
+
// Step 1: register_online(仅通知 group.ap 在线,不再返回游标)
|
|
697
|
+
await this.groupOps.registerOnline(this._groupTargetAid);
|
|
698
|
+
this._onlineGroups.add(groupId);
|
|
699
|
+
console.log(`[Group] joinGroupSession: group=${groupId}`);
|
|
700
|
+
// Step 2: 启动心跳定时器(首次加入群组时启动)
|
|
701
|
+
this._ensureHeartbeat();
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* 离开群组会话(优雅退出):
|
|
705
|
+
* 1. 从在线群组列表移除
|
|
706
|
+
* 2. 如果没有在线群组了,unregister_online + 停止心跳定时器
|
|
707
|
+
*/
|
|
708
|
+
async leaveGroupSession(groupId) {
|
|
709
|
+
if (!this.groupOps || !this._groupTargetAid)
|
|
710
|
+
return;
|
|
711
|
+
this._onlineGroups.delete(groupId);
|
|
712
|
+
// 如果没有在线群组了,通知 group.ap 下线并停止心跳
|
|
713
|
+
if (this._onlineGroups.size === 0) {
|
|
714
|
+
try {
|
|
715
|
+
await this.groupOps.unregisterOnline(this._groupTargetAid);
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
console.warn(`[Group] unregisterOnline failed`, e.message || e);
|
|
719
|
+
}
|
|
720
|
+
this._stopHeartbeat();
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 离开所有群组会话
|
|
725
|
+
*/
|
|
726
|
+
async leaveAllGroupSessions() {
|
|
727
|
+
const groups = Array.from(this._onlineGroups);
|
|
728
|
+
for (const groupId of groups) {
|
|
729
|
+
await this.leaveGroupSession(groupId);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* 获取当前在线群组列表
|
|
734
|
+
*/
|
|
735
|
+
getOnlineGroups() {
|
|
736
|
+
return Array.from(this._onlineGroups);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* 设置心跳间隔(毫秒),默认 180000(3 分钟)
|
|
740
|
+
*/
|
|
741
|
+
setHeartbeatInterval(intervalMs) {
|
|
742
|
+
this._heartbeatIntervalMs = intervalMs;
|
|
743
|
+
// 如果心跳已在运行,重新启动以应用新间隔
|
|
744
|
+
if (this._heartbeatTimer) {
|
|
745
|
+
this._stopHeartbeat();
|
|
746
|
+
this._ensureHeartbeat();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 确保心跳定时器已启动
|
|
751
|
+
*/
|
|
752
|
+
_ensureHeartbeat() {
|
|
753
|
+
if (this._heartbeatTimer)
|
|
754
|
+
return;
|
|
755
|
+
if (this._onlineGroups.size === 0)
|
|
756
|
+
return;
|
|
757
|
+
this._heartbeatTimer = setInterval(() => {
|
|
758
|
+
this._sendHeartbeats();
|
|
759
|
+
}, this._heartbeatIntervalMs);
|
|
760
|
+
console.log(`[Group] heartbeat started: interval=${this._heartbeatIntervalMs}ms`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 停止心跳定时器
|
|
764
|
+
*/
|
|
765
|
+
_stopHeartbeat() {
|
|
766
|
+
if (this._heartbeatTimer) {
|
|
767
|
+
clearInterval(this._heartbeatTimer);
|
|
768
|
+
this._heartbeatTimer = null;
|
|
769
|
+
console.log(`[Group] heartbeat stopped`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* 发送心跳保活
|
|
774
|
+
*/
|
|
775
|
+
_sendHeartbeats() {
|
|
776
|
+
if (!this.groupOps || !this._groupTargetAid)
|
|
777
|
+
return;
|
|
778
|
+
this.groupOps.heartbeat(this._groupTargetAid).catch(e => {
|
|
779
|
+
console.warn(`[Group] heartbeat failed`, e.message || e);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
646
782
|
/**
|
|
647
783
|
* 关闭群消息存储,刷新所有未写入的数据
|
|
648
784
|
*/
|
package/dist/group/client.js
CHANGED
|
@@ -74,24 +74,21 @@ class ACPGroupClient {
|
|
|
74
74
|
* Called by the message dispatch chain in AgentCP.
|
|
75
75
|
*/
|
|
76
76
|
handleIncoming(payload) {
|
|
77
|
-
var _a, _b, _c;
|
|
78
|
-
// console.log(`[GroupClient] <<< handleIncoming raw payload (first 500 chars): ${payload.substring(0, 500)}`);
|
|
77
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
79
78
|
let data;
|
|
80
79
|
try {
|
|
81
80
|
data = JSON.parse(payload);
|
|
82
81
|
}
|
|
83
82
|
catch (e) {
|
|
84
|
-
console.error(`[GroupClient]
|
|
83
|
+
console.error(`[GroupClient] JSON.parse failed for incoming payload:`, e);
|
|
85
84
|
return;
|
|
86
85
|
}
|
|
87
|
-
// console.log(`[GroupClient] <<< parsed data: action=${data.action} request_id=${data.request_id} event=${data.event} code=${data.code}`);
|
|
88
86
|
// Try as response (has request_id)
|
|
89
87
|
const requestId = (_a = data.request_id) !== null && _a !== void 0 ? _a : "";
|
|
90
88
|
if (requestId) {
|
|
91
89
|
const resp = (0, types_1.parseGroupResponse)(data);
|
|
92
90
|
const pending = this._pendingReqs.get(requestId);
|
|
93
91
|
if (pending) {
|
|
94
|
-
// console.log(`[GroupClient] <<< matched pending request: reqId=${requestId}, resolving`);
|
|
95
92
|
clearTimeout(pending.timer);
|
|
96
93
|
this._pendingReqs.delete(requestId);
|
|
97
94
|
pending.resolve(resp);
|
|
@@ -119,6 +116,34 @@ class ACPGroupClient {
|
|
|
119
116
|
}
|
|
120
117
|
return;
|
|
121
118
|
}
|
|
119
|
+
// Handle action-based push messages from group.ap (e.g. message_push)
|
|
120
|
+
// These have action field but no event/request_id, need to be mapped to notification events
|
|
121
|
+
const action = (_d = data.action) !== null && _d !== void 0 ? _d : "";
|
|
122
|
+
if (action === "message_push" && data.data) {
|
|
123
|
+
console.log(`[GroupClient] message_push -> group_message: group=${data.group_id} msg_id=${data.data.msg_id}`);
|
|
124
|
+
const msgData = data.data;
|
|
125
|
+
const notify = {
|
|
126
|
+
action: "group_notify",
|
|
127
|
+
group_id: (_e = data.group_id) !== null && _e !== void 0 ? _e : "",
|
|
128
|
+
event: types_1.NOTIFY_GROUP_MESSAGE,
|
|
129
|
+
data: {
|
|
130
|
+
msg_id: (_f = msgData.msg_id) !== null && _f !== void 0 ? _f : 0,
|
|
131
|
+
sender: (_g = msgData.sender) !== null && _g !== void 0 ? _g : "",
|
|
132
|
+
content: (_h = msgData.content) !== null && _h !== void 0 ? _h : "",
|
|
133
|
+
content_type: (_j = msgData.content_type) !== null && _j !== void 0 ? _j : "text",
|
|
134
|
+
timestamp: (_k = msgData.timestamp) !== null && _k !== void 0 ? _k : 0,
|
|
135
|
+
metadata: (_l = msgData.metadata) !== null && _l !== void 0 ? _l : null,
|
|
136
|
+
},
|
|
137
|
+
timestamp: (_m = msgData.timestamp) !== null && _m !== void 0 ? _m : 0,
|
|
138
|
+
};
|
|
139
|
+
if (this._handler != null) {
|
|
140
|
+
(0, events_1.dispatchAcpNotify)(this._handler, notify);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.warn(`[GroupClient] !!! message_push dropped: no event handler registered.`);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
122
147
|
console.warn(`[GroupClient] !!! unhandled incoming message: no request_id and no event field`, JSON.stringify(data).substring(0, 300));
|
|
123
148
|
}
|
|
124
149
|
// -- Lifecycle --
|
|
@@ -42,15 +42,36 @@ export declare class GroupOperations {
|
|
|
42
42
|
inviteCode?: string;
|
|
43
43
|
message?: string;
|
|
44
44
|
}): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* 注册上线,告知 group.ap 当前客户端在线,可以接收消息推送。
|
|
47
|
+
* 客户端每次启动或重新连接时调用一次即可。
|
|
48
|
+
*/
|
|
49
|
+
registerOnline(targetAid: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* 主动下线(优雅退出)。
|
|
52
|
+
* 客户端退出时调用,立即从在线列表移除。
|
|
53
|
+
*/
|
|
54
|
+
unregisterOnline(targetAid: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 心跳保活。
|
|
57
|
+
* 在线注册有 5 分钟超时,SDK 需定时发送(建议 2~4 分钟)。
|
|
58
|
+
*/
|
|
59
|
+
heartbeat(targetAid: string): Promise<void>;
|
|
45
60
|
createGroup(targetAid: string, name: string, options?: {
|
|
46
61
|
alias?: string;
|
|
47
62
|
subject?: string;
|
|
48
63
|
visibility?: string;
|
|
64
|
+
description?: string;
|
|
49
65
|
tags?: string[];
|
|
50
66
|
}): Promise<CreateGroupResp>;
|
|
51
67
|
addMember(targetAid: string, groupId: string, agentId: string, role?: string): Promise<void>;
|
|
52
68
|
sendGroupMessage(targetAid: string, groupId: string, content: string, contentType?: string, metadata?: Record<string, any>): Promise<SendMessageResp>;
|
|
53
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 拉取消息。
|
|
71
|
+
* - afterMsgId > 0: 指定位置模式,从该 ID 之后开始拉取
|
|
72
|
+
* - afterMsgId = 0 或不传: 自动游标模式(推荐),服务端基于 current_msg_id 自动计算
|
|
73
|
+
*/
|
|
74
|
+
pullMessages(targetAid: string, groupId: string, afterMsgId?: number, limit?: number): Promise<PullMessagesResp>;
|
|
54
75
|
ackMessages(targetAid: string, groupId: string, msgId: number): Promise<void>;
|
|
55
76
|
pullEvents(targetAid: string, groupId: string, afterEventId: number, limit?: number): Promise<PullEventsResp>;
|
|
56
77
|
ackEvents(targetAid: string, groupId: string, eventId: number): Promise<void>;
|
package/dist/group/operations.js
CHANGED
|
@@ -59,6 +59,33 @@ class GroupOperations {
|
|
|
59
59
|
return this.requestJoin(targetAid, groupId, (_a = options === null || options === void 0 ? void 0 : options.message) !== null && _a !== void 0 ? _a : '');
|
|
60
60
|
}
|
|
61
61
|
// ============================================================
|
|
62
|
+
// Phase 0: Lifecycle (register / heartbeat / unregister)
|
|
63
|
+
// ============================================================
|
|
64
|
+
/**
|
|
65
|
+
* 注册上线,告知 group.ap 当前客户端在线,可以接收消息推送。
|
|
66
|
+
* 客户端每次启动或重新连接时调用一次即可。
|
|
67
|
+
*/
|
|
68
|
+
async registerOnline(targetAid) {
|
|
69
|
+
const resp = await this._client.sendRequest(targetAid, "", "register_online", null);
|
|
70
|
+
this._check(resp, "register_online");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 主动下线(优雅退出)。
|
|
74
|
+
* 客户端退出时调用,立即从在线列表移除。
|
|
75
|
+
*/
|
|
76
|
+
async unregisterOnline(targetAid) {
|
|
77
|
+
const resp = await this._client.sendRequest(targetAid, "", "unregister_online", null);
|
|
78
|
+
this._check(resp, "unregister_online");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 心跳保活。
|
|
82
|
+
* 在线注册有 5 分钟超时,SDK 需定时发送(建议 2~4 分钟)。
|
|
83
|
+
*/
|
|
84
|
+
async heartbeat(targetAid) {
|
|
85
|
+
const resp = await this._client.sendRequest(targetAid, "", "heartbeat", null);
|
|
86
|
+
this._check(resp, "heartbeat");
|
|
87
|
+
}
|
|
88
|
+
// ============================================================
|
|
62
89
|
// Phase 1: Basic Operations
|
|
63
90
|
// ============================================================
|
|
64
91
|
async createGroup(targetAid, name, options) {
|
|
@@ -70,6 +97,8 @@ class GroupOperations {
|
|
|
70
97
|
params.subject = options.subject;
|
|
71
98
|
if (options === null || options === void 0 ? void 0 : options.visibility)
|
|
72
99
|
params.visibility = options.visibility;
|
|
100
|
+
if (options === null || options === void 0 ? void 0 : options.description)
|
|
101
|
+
params.description = options.description;
|
|
73
102
|
if (options === null || options === void 0 ? void 0 : options.tags)
|
|
74
103
|
params.tags = options.tags;
|
|
75
104
|
const resp = await this._client.sendRequest(targetAid, "", "create_group", params);
|
|
@@ -96,12 +125,19 @@ class GroupOperations {
|
|
|
96
125
|
const d = resp.data || {};
|
|
97
126
|
return { msg_id: (_a = d.msg_id) !== null && _a !== void 0 ? _a : 0, timestamp: (_b = d.timestamp) !== null && _b !== void 0 ? _b : 0 };
|
|
98
127
|
}
|
|
99
|
-
|
|
128
|
+
/**
|
|
129
|
+
* 拉取消息。
|
|
130
|
+
* - afterMsgId > 0: 指定位置模式,从该 ID 之后开始拉取
|
|
131
|
+
* - afterMsgId = 0 或不传: 自动游标模式(推荐),服务端基于 current_msg_id 自动计算
|
|
132
|
+
*/
|
|
133
|
+
async pullMessages(targetAid, groupId, afterMsgId = 0, limit = 0) {
|
|
100
134
|
var _a, _b, _c;
|
|
101
|
-
const params = {
|
|
135
|
+
const params = {};
|
|
136
|
+
if (afterMsgId > 0)
|
|
137
|
+
params.after_msg_id = afterMsgId;
|
|
102
138
|
if (limit > 0)
|
|
103
139
|
params.limit = limit;
|
|
104
|
-
const resp = await this._client.sendRequest(targetAid, groupId, "pull_messages", params);
|
|
140
|
+
const resp = await this._client.sendRequest(targetAid, groupId, "pull_messages", Object.keys(params).length > 0 ? params : null);
|
|
105
141
|
this._check(resp, "pull_messages");
|
|
106
142
|
const d = resp.data || {};
|
|
107
143
|
return {
|
package/dist/server.js
CHANGED
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.startServer = startServer;
|
|
37
40
|
const http = __importStar(require("http"));
|
|
@@ -39,6 +42,7 @@ const url = __importStar(require("url"));
|
|
|
39
42
|
const fs = __importStar(require("fs"));
|
|
40
43
|
const path = __importStar(require("path"));
|
|
41
44
|
const https = __importStar(require("https"));
|
|
45
|
+
const ws_1 = __importDefault(require("ws"));
|
|
42
46
|
const datamanager_1 = require("./datamanager");
|
|
43
47
|
const agentcp_1 = require("./agentcp");
|
|
44
48
|
const agentws_1 = require("./agentws");
|
|
@@ -49,6 +53,21 @@ const messagestore_1 = require("./messagestore");
|
|
|
49
53
|
let globalApiUrl = '';
|
|
50
54
|
let globalDataDir = '';
|
|
51
55
|
const messageStores = new Map();
|
|
56
|
+
// ============================================================
|
|
57
|
+
// Browser ↔ Server WebSocket (real-time group message push)
|
|
58
|
+
// ============================================================
|
|
59
|
+
const browserWsClients = new Set();
|
|
60
|
+
/**
|
|
61
|
+
* 向所有已连接的浏览器 WS 客户端广播消息
|
|
62
|
+
*/
|
|
63
|
+
function broadcastToBrowser(data) {
|
|
64
|
+
const payload = JSON.stringify(data);
|
|
65
|
+
for (const client of browserWsClients) {
|
|
66
|
+
if (client.readyState === ws_1.default.OPEN) {
|
|
67
|
+
client.send(payload);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
let agentCP = null;
|
|
53
72
|
let currentAid = '';
|
|
54
73
|
const MAX_AIDS = 10;
|
|
@@ -186,9 +205,22 @@ async function doEnsureOnline(aid) {
|
|
|
186
205
|
ws.onStatusChange((status) => {
|
|
187
206
|
instance.wsStatus = status;
|
|
188
207
|
instance.wsConnected = status === 'connected';
|
|
208
|
+
broadcastToBrowser({ type: 'ws_status', aid, status });
|
|
189
209
|
});
|
|
190
210
|
await ws.startWebSocket();
|
|
191
211
|
instance.online = true;
|
|
212
|
+
// 上线成功,推送 AID 状态变更到前端
|
|
213
|
+
getAidStatusList().then(aidStatus => {
|
|
214
|
+
broadcastToBrowser({ type: 'aid_status', aidStatus });
|
|
215
|
+
}).catch(() => { });
|
|
216
|
+
// AID 上线后自动初始化群组功能,确保所有身份都能收到群消息推送
|
|
217
|
+
try {
|
|
218
|
+
await ensureGroupClient(instance);
|
|
219
|
+
console.log(`[Server] AID ${aid} 群组功能自动初始化完成`);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
console.warn(`[Server] AID ${aid} 群组功能自动初始化失败(不影响上线):`, e.message);
|
|
223
|
+
}
|
|
192
224
|
return instance;
|
|
193
225
|
}
|
|
194
226
|
// 确保群组客户端已初始化
|
|
@@ -228,16 +260,33 @@ async function ensureGroupClient(instance) {
|
|
|
228
260
|
instance.agentCP.setGroupEventHandler({
|
|
229
261
|
onNewMessage(groupId, latestMsgId, sender, preview) {
|
|
230
262
|
console.log(`[Group] onNewMessage: group=${groupId} msgId=${latestMsgId} sender=${sender} preview=${preview}`);
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
// 通知浏览器有新消息(轻量通知,前端可据此决定是否刷新)
|
|
264
|
+
broadcastToBrowser({
|
|
265
|
+
type: 'new_message_notify',
|
|
266
|
+
group_id: groupId,
|
|
267
|
+
latest_msg_id: latestMsgId,
|
|
268
|
+
sender,
|
|
269
|
+
preview,
|
|
234
270
|
});
|
|
235
271
|
},
|
|
236
272
|
onNewEvent(groupId, latestEventId, eventType, summary) {
|
|
237
273
|
console.log(`[Group] onNewEvent: group=${groupId} eventId=${latestEventId} type=${eventType} summary=${summary}`);
|
|
274
|
+
broadcastToBrowser({
|
|
275
|
+
type: 'new_event',
|
|
276
|
+
group_id: groupId,
|
|
277
|
+
latest_event_id: latestEventId,
|
|
278
|
+
event_type: eventType,
|
|
279
|
+
summary,
|
|
280
|
+
});
|
|
238
281
|
},
|
|
239
282
|
onGroupInvite(groupId, groupAddress, invitedBy) {
|
|
240
283
|
console.log(`[Group] onGroupInvite: group=${groupId} address=${groupAddress} invitedBy=${invitedBy}`);
|
|
284
|
+
broadcastToBrowser({
|
|
285
|
+
type: 'group_invite',
|
|
286
|
+
group_id: groupId,
|
|
287
|
+
group_address: groupAddress,
|
|
288
|
+
invited_by: invitedBy,
|
|
289
|
+
});
|
|
241
290
|
},
|
|
242
291
|
onJoinApproved(groupId, groupAddress) {
|
|
243
292
|
console.log(`[Group] onJoinApproved: group=${groupId} address=${groupAddress}`);
|
|
@@ -257,25 +306,71 @@ async function ensureGroupClient(instance) {
|
|
|
257
306
|
instance.agentCP.addGroupToStore(groupId, groupName);
|
|
258
307
|
const groupUrl = groupAddress || `https://${instance.groupTargetAid}/${groupId}`;
|
|
259
308
|
await instance.agentCP.registerGroupToHomeAP(groupId, groupUrl);
|
|
309
|
+
// 新加入的群也立即注册在线
|
|
310
|
+
await instance.agentCP.joinGroupSession(groupId);
|
|
260
311
|
}
|
|
261
312
|
catch (e) {
|
|
262
313
|
console.error(`[Group] onJoinApproved processing failed: group=${groupId}`, e.message);
|
|
263
314
|
}
|
|
264
315
|
})();
|
|
316
|
+
broadcastToBrowser({
|
|
317
|
+
type: 'join_approved',
|
|
318
|
+
group_id: groupId,
|
|
319
|
+
group_address: groupAddress,
|
|
320
|
+
});
|
|
265
321
|
},
|
|
266
322
|
onJoinRejected(groupId, reason) {
|
|
267
323
|
console.log(`[Group] onJoinRejected: group=${groupId} reason=${reason}`);
|
|
324
|
+
broadcastToBrowser({ type: 'join_rejected', group_id: groupId, reason });
|
|
268
325
|
},
|
|
269
326
|
onJoinRequestReceived(groupId, agentId, message) {
|
|
270
327
|
console.log(`[Group] onJoinRequestReceived: group=${groupId} agent=${agentId} msg=${message}`);
|
|
328
|
+
broadcastToBrowser({ type: 'join_request', group_id: groupId, agent_id: agentId, message });
|
|
271
329
|
},
|
|
272
330
|
onGroupMessage(groupId, msg) {
|
|
331
|
+
var _a;
|
|
273
332
|
console.log(`[Group] onGroupMessage: group=${groupId} msgId=${msg.msg_id} sender=${msg.sender}`);
|
|
333
|
+
// 存储消息到本地 + 自动 ACK
|
|
334
|
+
instance.agentCP.addGroupMessageToStore(groupId, msg);
|
|
335
|
+
(_a = instance.agentCP.groupOps) === null || _a === void 0 ? void 0 : _a.ackMessages(instance.groupTargetAid, groupId, msg.msg_id).catch(e => {
|
|
336
|
+
console.warn(`[Group] auto ack failed: group=${groupId} msgId=${msg.msg_id}`, e.message || e);
|
|
337
|
+
});
|
|
338
|
+
// 推送给浏览器
|
|
339
|
+
broadcastToBrowser({
|
|
340
|
+
type: 'group_message',
|
|
341
|
+
group_id: groupId,
|
|
342
|
+
message: msg,
|
|
343
|
+
});
|
|
274
344
|
},
|
|
275
345
|
onGroupEvent(groupId, evt) {
|
|
276
346
|
console.log(`[Group] onGroupEvent: group=${groupId} event=${evt.event_type}`);
|
|
347
|
+
broadcastToBrowser({
|
|
348
|
+
type: 'group_event',
|
|
349
|
+
group_id: groupId,
|
|
350
|
+
event: evt,
|
|
351
|
+
});
|
|
277
352
|
},
|
|
278
353
|
});
|
|
354
|
+
// 同步群组列表(如未同步过)
|
|
355
|
+
if (!instance.groupListSynced) {
|
|
356
|
+
try {
|
|
357
|
+
await instance.agentCP.syncGroupList();
|
|
358
|
+
instance.groupListSynced = true;
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
console.warn('[Group] syncGroupList error:', e.message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// 为所有已加入群组注册上线(register_online + 拉取未读 + 启动心跳)
|
|
365
|
+
const groups = instance.agentCP.getLocalGroupList();
|
|
366
|
+
for (const group of groups) {
|
|
367
|
+
try {
|
|
368
|
+
await instance.agentCP.joinGroupSession(group.group_id);
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
console.warn(`[Group] joinGroupSession failed: ${group.group_id}`, e.message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
279
374
|
instance.groupInitialized = true;
|
|
280
375
|
instance.groupSessionId = groupSessionId;
|
|
281
376
|
instance.groupTargetAid = targetAid;
|
|
@@ -840,8 +935,9 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
840
935
|
.modal-overlay.show { display:flex; }
|
|
841
936
|
.modal { background:#fff; width:90%; max-width:400px; border-radius:12px; padding:24px; box-shadow:0 10px 25px rgba(0,0,0,0.1); }
|
|
842
937
|
.modal h3 { margin-bottom:16px; font-size:16px; }
|
|
843
|
-
.modal input { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; }
|
|
844
|
-
.modal input:focus { outline:none; border-color:var(--primary); }
|
|
938
|
+
.modal input[type="text"], .modal input[type="password"], .modal input[type="url"] { width:100%; padding:10px; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; font-size:14px; box-sizing:border-box; }
|
|
939
|
+
.modal input[type="text"]:focus, .modal input[type="password"]:focus, .modal input[type="url"]:focus { outline:none; border-color:var(--primary); }
|
|
940
|
+
.modal input[type="radio"] { width:auto; margin:0; }
|
|
845
941
|
.modal-btns { display:flex; justify-content:flex-end; gap:10px; }
|
|
846
942
|
.mbtn { padding:8px 16px; border-radius:6px; font-size:13px; cursor:pointer; border:none; }
|
|
847
943
|
.mbtn-cancel { background:#f3f4f6; color:var(--t1); }
|
|
@@ -992,7 +1088,15 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
992
1088
|
<div class="modal-overlay" id="createGroupModal">
|
|
993
1089
|
<div class="modal">
|
|
994
1090
|
<h3>创建群组</h3>
|
|
995
|
-
<input type="text" id="groupNameInput" placeholder="输入群组名称"
|
|
1091
|
+
<input type="text" id="groupNameInput" placeholder="输入群组名称">
|
|
1092
|
+
<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>
|
|
1093
|
+
<div style="margin-bottom:16px;">
|
|
1094
|
+
<label style="font-size:13px;color:var(--t2);margin-bottom:8px;display:block;">群组类型</label>
|
|
1095
|
+
<div style="display:flex;gap:12px;">
|
|
1096
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="public" checked> 公开群</label>
|
|
1097
|
+
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;"><input type="radio" name="groupVisibility" value="private"> 私密群</label>
|
|
1098
|
+
</div>
|
|
1099
|
+
</div>
|
|
996
1100
|
<div class="modal-btns">
|
|
997
1101
|
<button class="mbtn mbtn-cancel" onclick="hideCreateGroupModal()">取消</button>
|
|
998
1102
|
<button class="mbtn mbtn-ok" id="createGroupBtn" onclick="doCreateGroup()">创建</button>
|
|
@@ -1095,6 +1199,8 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1095
1199
|
// 填充 AID 切换下拉
|
|
1096
1200
|
S.aidList=d.aidStatus||[];
|
|
1097
1201
|
renderAidSelect();
|
|
1202
|
+
// 初始加载一次ws状态,后续通过WebSocket推送更新
|
|
1203
|
+
fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
|
|
1098
1204
|
poll(); setInterval(poll,1000);
|
|
1099
1205
|
} else { window.location.href='/'; }
|
|
1100
1206
|
} catch(e){ console.error(e); }
|
|
@@ -1143,18 +1249,12 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1143
1249
|
} else {
|
|
1144
1250
|
await fetch('/api/ws/start',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({aid:S.aid})});
|
|
1145
1251
|
}
|
|
1146
|
-
|
|
1147
|
-
S.aidList=d.aidStatus||[];
|
|
1148
|
-
renderAidSelect();
|
|
1252
|
+
// AID 状态变更通过 WS 推送 aid_status 自动更新,无需再拉取
|
|
1149
1253
|
} catch(e){}
|
|
1150
1254
|
}
|
|
1151
1255
|
|
|
1152
|
-
var _pollCount=0;
|
|
1153
1256
|
async function poll(){
|
|
1154
1257
|
try {
|
|
1155
|
-
var wr=await fetch('/api/ws/status');
|
|
1156
|
-
var wd=await wr.json();
|
|
1157
|
-
updateDot(wd.status);
|
|
1158
1258
|
// P2P会话和消息仅在P2P标签页时刷新
|
|
1159
1259
|
if(S.tab==='p2p'){
|
|
1160
1260
|
var [sr,mr] = await Promise.all([fetch('/api/sessions'),fetch('/api/messages')]);
|
|
@@ -1163,12 +1263,6 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1163
1263
|
S.closed=md.closed||false;
|
|
1164
1264
|
if(md.messages) renderMsgs(md.messages, S.closed);
|
|
1165
1265
|
}
|
|
1166
|
-
// 每5次轮询刷新一次AID在线状态
|
|
1167
|
-
if(++_pollCount%5===0){
|
|
1168
|
-
var ar=await fetch('/api/aid'); var ad=await ar.json();
|
|
1169
|
-
S.aidList=ad.aidStatus||[];
|
|
1170
|
-
renderAidSelect();
|
|
1171
|
-
}
|
|
1172
1266
|
} catch(e){}
|
|
1173
1267
|
}
|
|
1174
1268
|
|
|
@@ -1212,7 +1306,7 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1212
1306
|
html+='<div class="aid-group-sessions'+(isOpen?' open':'')+'">';
|
|
1213
1307
|
list.forEach(function(s){
|
|
1214
1308
|
var active=s.sessionId===S.sid;
|
|
1215
|
-
var time=
|
|
1309
|
+
var time=fmtTime(s.lastMessageAt);
|
|
1216
1310
|
var tc=s.type==='outgoing'?'outgoing':'incoming';
|
|
1217
1311
|
var tt=s.type==='outgoing'?'OUT':'IN';
|
|
1218
1312
|
var name=s.lastMessage||'';
|
|
@@ -1278,7 +1372,7 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1278
1372
|
});
|
|
1279
1373
|
}
|
|
1280
1374
|
var avatarSrc = getAvatarSrc(info ? info.type : '');
|
|
1281
|
-
var t=
|
|
1375
|
+
var t=fmtTime(m.timestamp);
|
|
1282
1376
|
var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content):escH(m.content);
|
|
1283
1377
|
var name = (info && info.name) ? info.name : sender;
|
|
1284
1378
|
|
|
@@ -1327,7 +1421,18 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1327
1421
|
var r=await fetch('/api/group/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({groupId:S.activeGroupId,message:txt})});
|
|
1328
1422
|
var d=await r.json();
|
|
1329
1423
|
if(!d.success) alert(d.error||'发送失败');
|
|
1330
|
-
else
|
|
1424
|
+
else {
|
|
1425
|
+
// 发送成功:立即追加到本地显示(服务端已存储,不用等 WS 推送)
|
|
1426
|
+
if(d.msg_id){
|
|
1427
|
+
var sentMsg={msg_id:d.msg_id,sender:S.aid,content:txt,content_type:'text',timestamp:d.timestamp||Date.now()};
|
|
1428
|
+
var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===sentMsg.msg_id; });
|
|
1429
|
+
if(!exists){
|
|
1430
|
+
_lastGroupMsgs.push(sentMsg);
|
|
1431
|
+
_lastGroupMsgSig='';
|
|
1432
|
+
renderGroupMsgs(_lastGroupMsgs);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1331
1436
|
} catch(e){ alert('发送失败'); }
|
|
1332
1437
|
return;
|
|
1333
1438
|
}
|
|
@@ -1374,6 +1479,21 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1374
1479
|
|
|
1375
1480
|
function escH(t){ var d=document.createElement('div'); d.textContent=t; return d.innerHTML; }
|
|
1376
1481
|
function escA(t){ return t.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"\\\\'"); }
|
|
1482
|
+
function fmtTime(ts){
|
|
1483
|
+
if(!ts) return '';
|
|
1484
|
+
var n=Number(ts);
|
|
1485
|
+
if(isNaN(n)) return '';
|
|
1486
|
+
if(n<1e12) n=n*1000;
|
|
1487
|
+
var d=new Date(n);
|
|
1488
|
+
if(isNaN(d.getTime())) return '';
|
|
1489
|
+
var now=new Date();
|
|
1490
|
+
var pad=function(v){ return v<10?'0'+v:''+v; };
|
|
1491
|
+
var H=pad(d.getHours()), M=pad(d.getMinutes()), ss=pad(d.getSeconds());
|
|
1492
|
+
if(d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth()&&d.getDate()===now.getDate()){
|
|
1493
|
+
return H+':'+M+':'+ss;
|
|
1494
|
+
}
|
|
1495
|
+
return d.getFullYear()+'/'+pad(d.getMonth()+1)+'/'+pad(d.getDate())+' '+H+':'+M+':'+ss;
|
|
1496
|
+
}
|
|
1377
1497
|
|
|
1378
1498
|
// ============================================================
|
|
1379
1499
|
// Group Functions
|
|
@@ -1491,6 +1611,76 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1491
1611
|
} catch(e){}
|
|
1492
1612
|
}
|
|
1493
1613
|
|
|
1614
|
+
// ============================================================
|
|
1615
|
+
// WebSocket: real-time group message push
|
|
1616
|
+
// ============================================================
|
|
1617
|
+
var _groupWs=null;
|
|
1618
|
+
var _groupWsReconnectTimer=null;
|
|
1619
|
+
|
|
1620
|
+
function connectGroupWs(){
|
|
1621
|
+
if(_groupWs&&(_groupWs.readyState===WebSocket.OPEN||_groupWs.readyState===WebSocket.CONNECTING)) return;
|
|
1622
|
+
var proto=location.protocol==='https:'?'wss:':'ws:';
|
|
1623
|
+
_groupWs=new WebSocket(proto+'//'+location.host+'/ws/group');
|
|
1624
|
+
_groupWs.onopen=function(){
|
|
1625
|
+
console.log('[WS] group connected');
|
|
1626
|
+
if(_groupWsReconnectTimer){ clearTimeout(_groupWsReconnectTimer); _groupWsReconnectTimer=null; }
|
|
1627
|
+
// 重连后主动拉取最新状态,防止断连期间丢失推送
|
|
1628
|
+
fetch('/api/ws/status').then(function(r){return r.json();}).then(function(d){updateDot(d.status);}).catch(function(){});
|
|
1629
|
+
fetch('/api/aid').then(function(r){return r.json();}).then(function(d){if(d.aidStatus){S.aidList=d.aidStatus;renderAidSelect();}}).catch(function(){});
|
|
1630
|
+
};
|
|
1631
|
+
_groupWs.onmessage=function(ev){
|
|
1632
|
+
try {
|
|
1633
|
+
var data=JSON.parse(ev.data);
|
|
1634
|
+
handleGroupWsMessage(data);
|
|
1635
|
+
} catch(e){ console.error('[WS] parse error',e); }
|
|
1636
|
+
};
|
|
1637
|
+
_groupWs.onclose=function(){
|
|
1638
|
+
console.log('[WS] group disconnected, reconnecting in 3s...');
|
|
1639
|
+
_groupWs=null;
|
|
1640
|
+
_groupWsReconnectTimer=setTimeout(connectGroupWs,3000);
|
|
1641
|
+
};
|
|
1642
|
+
_groupWs.onerror=function(e){
|
|
1643
|
+
console.error('[WS] group error',e);
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function handleGroupWsMessage(data){
|
|
1648
|
+
if(data.type==='ws_status'){
|
|
1649
|
+
if(!data.aid||data.aid===S.aid) updateDot(data.status);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if(data.type==='aid_status'){
|
|
1653
|
+
S.aidList=data.aidStatus||[];
|
|
1654
|
+
renderAidSelect();
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
if(data.type==='group_message'){
|
|
1658
|
+
// 实时推送的完整消息
|
|
1659
|
+
var msg=data.message;
|
|
1660
|
+
var gid=data.group_id;
|
|
1661
|
+
if(gid===S.activeGroupId&&S.tab==='group'){
|
|
1662
|
+
// 追加到当前消息列表并重新渲染
|
|
1663
|
+
var exists=_lastGroupMsgs.some(function(m){ return m.msg_id===msg.msg_id; });
|
|
1664
|
+
if(!exists){
|
|
1665
|
+
_lastGroupMsgs.push(msg);
|
|
1666
|
+
_lastGroupMsgSig=''; // 强制重新渲染
|
|
1667
|
+
renderGroupMsgs(_lastGroupMsgs);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
} else if(data.type==='new_message_notify'){
|
|
1671
|
+
// 轻量通知:如果是当前活跃群组,拉取最新消息(本地读取,很快)
|
|
1672
|
+
if(data.group_id===S.activeGroupId&&S.tab==='group'){
|
|
1673
|
+
pollGroupMessages();
|
|
1674
|
+
}
|
|
1675
|
+
} else if(data.type==='join_approved'||data.type==='group_invite'){
|
|
1676
|
+
// 群组变动,刷新群组列表
|
|
1677
|
+
pollGroupList();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// 启动 WebSocket 连接
|
|
1682
|
+
connectGroupWs();
|
|
1683
|
+
|
|
1494
1684
|
var _lastGroupMsgs=[];
|
|
1495
1685
|
function renderGroupMsgs(msgs){
|
|
1496
1686
|
var sig=msgs.length+(msgs.length>0?(msgs[msgs.length-1].msg_id||0):'');
|
|
@@ -1508,7 +1698,8 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1508
1698
|
var info=agentInfoCache[sender];
|
|
1509
1699
|
if(!info){ needFetch.push(sender); }
|
|
1510
1700
|
var avatarSrc=getAvatarSrc(info?info.type:'');
|
|
1511
|
-
var t=m.timestamp?
|
|
1701
|
+
var t=m.timestamp?fmtTime(m.timestamp):'';
|
|
1702
|
+
|
|
1512
1703
|
var c=(typeof marked!=='undefined'&&marked.parse)?marked.parse(m.content||''):escH(m.content||'');
|
|
1513
1704
|
var name=(info&&info.name)?info.name:sender;
|
|
1514
1705
|
return '<div class="message '+(sent?'sent':'received')+'">' +
|
|
@@ -1532,7 +1723,7 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1532
1723
|
}
|
|
1533
1724
|
|
|
1534
1725
|
// Group modals
|
|
1535
|
-
function showCreateGroupModal(){ $('createGroupModal').classList.add('show'); $('groupNameInput').value=''; $('groupNameInput').focus(); }
|
|
1726
|
+
function showCreateGroupModal(){ $('createGroupModal').classList.add('show'); $('groupNameInput').value=''; $('groupDescInput').value=''; document.querySelector('input[name="groupVisibility"][value="public"]').checked=true; $('groupNameInput').focus(); }
|
|
1536
1727
|
function hideCreateGroupModal(){ $('createGroupModal').classList.remove('show'); }
|
|
1537
1728
|
function showJoinGroupModal(){ $('joinGroupModal').classList.add('show'); $('joinGroupUrlInput').value=''; $('joinGroupUrlInput').focus(); }
|
|
1538
1729
|
function hideJoinGroupModal(){ $('joinGroupModal').classList.remove('show'); }
|
|
@@ -1541,10 +1732,12 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1541
1732
|
async function doCreateGroup(){
|
|
1542
1733
|
var name=$('groupNameInput').value.trim();
|
|
1543
1734
|
if(!name) return;
|
|
1735
|
+
var description=$('groupDescInput').value.trim();
|
|
1736
|
+
var visibility=document.querySelector('input[name="groupVisibility"]:checked').value;
|
|
1544
1737
|
var btn=$('createGroupBtn');
|
|
1545
1738
|
btn.disabled=true; btn.textContent='创建中...';
|
|
1546
1739
|
try {
|
|
1547
|
-
var r=await fetch('/api/group/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name})});
|
|
1740
|
+
var r=await fetch('/api/group/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,visibility:visibility,description:description||undefined})});
|
|
1548
1741
|
var d=await r.json();
|
|
1549
1742
|
if(d.success){
|
|
1550
1743
|
hideCreateGroupModal();
|
|
@@ -1744,7 +1937,7 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1744
1937
|
var html=d.requests.map(function(req){
|
|
1745
1938
|
var aid=req.agent_id||'';
|
|
1746
1939
|
var msg=req.message?escH(req.message):'';
|
|
1747
|
-
var time=req.created_at?
|
|
1940
|
+
var time=req.created_at?fmtTime(req.created_at):'';
|
|
1748
1941
|
var cachedInfo=agentInfoCache[aid];
|
|
1749
1942
|
var avatarSrc=getAvatarSrc(cachedInfo?cachedInfo.type:'');
|
|
1750
1943
|
var displayName=(cachedInfo&&cachedInfo.name)?cachedInfo.name:aid;
|
|
@@ -1849,12 +2042,8 @@ const chatHtml = `<!DOCTYPE html>
|
|
|
1849
2042
|
} catch(e){ $('myGroupsContent').innerHTML='<div style="text-align:center;padding:20px;color:#e74c3c;">请求失败: '+escH(e.message)+'</div>'; }
|
|
1850
2043
|
}
|
|
1851
2044
|
|
|
1852
|
-
//
|
|
1853
|
-
|
|
1854
|
-
poll=async function(){
|
|
1855
|
-
await _origPoll();
|
|
1856
|
-
if(S.tab==='group'&&S.activeGroupId) pollGroupMessages();
|
|
1857
|
-
};
|
|
2045
|
+
// 扩展轮询:保留 P2P 等基础轮询,群组消息已通过 WebSocket 实时推送
|
|
2046
|
+
// 不再每秒轮询群消息
|
|
1858
2047
|
|
|
1859
2048
|
init();
|
|
1860
2049
|
<\/script>
|
|
@@ -1998,6 +2187,13 @@ async function handleRequest(req, res) {
|
|
|
1998
2187
|
// 切换会话:保存旧 AID 的会话,加载新 AID 的会话
|
|
1999
2188
|
await getMessageStoreForAid(aid).loadSessionsForAid(aid);
|
|
2000
2189
|
activeSessionId = null;
|
|
2190
|
+
// 切换身份时自动上线(含群组初始化),A/B 同时保持在线
|
|
2191
|
+
try {
|
|
2192
|
+
await ensureOnline(aid);
|
|
2193
|
+
}
|
|
2194
|
+
catch (e) {
|
|
2195
|
+
console.warn(`[Select] AID ${aid} 自动上线失败:`, e.message);
|
|
2196
|
+
}
|
|
2001
2197
|
sendJson(res, { success: true, aid });
|
|
2002
2198
|
}
|
|
2003
2199
|
else {
|
|
@@ -2091,6 +2287,20 @@ async function handleRequest(req, res) {
|
|
|
2091
2287
|
sendJson(res, { success: false, error: '该 AID 未上线' });
|
|
2092
2288
|
return;
|
|
2093
2289
|
}
|
|
2290
|
+
if (instance.groupInitialized) {
|
|
2291
|
+
try {
|
|
2292
|
+
await instance.agentCP.leaveAllGroupSessions();
|
|
2293
|
+
}
|
|
2294
|
+
catch (e) {
|
|
2295
|
+
console.warn(`[Group] leaveAllGroupSessions error:`, e.message);
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
await instance.agentCP.closeGroupMessageStore();
|
|
2299
|
+
}
|
|
2300
|
+
catch (e) {
|
|
2301
|
+
console.warn(`[Group] closeGroupMessageStore error:`, e.message);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2094
2304
|
if (instance.heartbeatClient) {
|
|
2095
2305
|
instance.heartbeatClient.offline();
|
|
2096
2306
|
}
|
|
@@ -2099,6 +2309,10 @@ async function handleRequest(req, res) {
|
|
|
2099
2309
|
}
|
|
2100
2310
|
aidInstances.delete(aid);
|
|
2101
2311
|
console.log(`[Server] AID ${aid} 已下线`);
|
|
2312
|
+
// 下线后推送 AID 状态变更到前端
|
|
2313
|
+
getAidStatusList().then(aidStatus => {
|
|
2314
|
+
broadcastToBrowser({ type: 'aid_status', aidStatus });
|
|
2315
|
+
}).catch(() => { });
|
|
2102
2316
|
sendJson(res, { success: true, aid });
|
|
2103
2317
|
}
|
|
2104
2318
|
catch (e) {
|
|
@@ -2300,7 +2514,7 @@ async function handleRequest(req, res) {
|
|
|
2300
2514
|
if (pathname === '/api/group/create' && method === 'POST') {
|
|
2301
2515
|
try {
|
|
2302
2516
|
const body = await parseBody(req);
|
|
2303
|
-
const { name } = body;
|
|
2517
|
+
const { name, visibility, description } = body;
|
|
2304
2518
|
if (!name) {
|
|
2305
2519
|
sendJson(res, { success: false, error: '群组名称不能为空' });
|
|
2306
2520
|
return;
|
|
@@ -2309,7 +2523,12 @@ async function handleRequest(req, res) {
|
|
|
2309
2523
|
await ensureGroupClient(instance);
|
|
2310
2524
|
const ops = instance.agentCP.groupOps;
|
|
2311
2525
|
const target = instance.groupTargetAid;
|
|
2312
|
-
const
|
|
2526
|
+
const options = Object.assign({}, (body.options || {}));
|
|
2527
|
+
if (visibility)
|
|
2528
|
+
options.visibility = visibility;
|
|
2529
|
+
if (description)
|
|
2530
|
+
options.description = description;
|
|
2531
|
+
const result = await ops.createGroup(target, name, options);
|
|
2313
2532
|
console.log('[ACP] createGroup 返回:', JSON.stringify(result, null, 2));
|
|
2314
2533
|
// 加入本地存储
|
|
2315
2534
|
instance.agentCP.addGroupToStore(result.group_id, name);
|
|
@@ -2407,8 +2626,9 @@ async function handleRequest(req, res) {
|
|
|
2407
2626
|
return;
|
|
2408
2627
|
}
|
|
2409
2628
|
await ensureGroupClient(instance);
|
|
2410
|
-
//
|
|
2411
|
-
|
|
2629
|
+
// 只读本地缓存,不再每次请求都去服务端拉取
|
|
2630
|
+
// 新消息通过 WebSocket 推送实时到达并由 SDK 自动存储
|
|
2631
|
+
const messages = instance.agentCP.getLocalGroupMessages(groupId);
|
|
2412
2632
|
sendJson(res, { success: true, messages });
|
|
2413
2633
|
}
|
|
2414
2634
|
catch (e) {
|
|
@@ -2617,6 +2837,28 @@ function startServer(port, apiUrl, dataDir = '') {
|
|
|
2617
2837
|
}
|
|
2618
2838
|
}).catch(() => { });
|
|
2619
2839
|
const server = http.createServer(handleRequest);
|
|
2840
|
+
// WebSocket server for browser ↔ server real-time communication
|
|
2841
|
+
const wss = new ws_1.default.Server({ noServer: true });
|
|
2842
|
+
server.on('upgrade', (req, socket, head) => {
|
|
2843
|
+
const pathname = url.parse(req.url || '', true).pathname;
|
|
2844
|
+
if (pathname === '/ws/group') {
|
|
2845
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
2846
|
+
browserWsClients.add(ws);
|
|
2847
|
+
console.log(`[WS] browser client connected, total=${browserWsClients.size}`);
|
|
2848
|
+
ws.on('close', () => {
|
|
2849
|
+
browserWsClients.delete(ws);
|
|
2850
|
+
console.log(`[WS] browser client disconnected, total=${browserWsClients.size}`);
|
|
2851
|
+
});
|
|
2852
|
+
ws.on('error', (err) => {
|
|
2853
|
+
console.error('[WS] browser client error:', err.message);
|
|
2854
|
+
browserWsClients.delete(ws);
|
|
2855
|
+
});
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
else {
|
|
2859
|
+
socket.destroy();
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2620
2862
|
// 资源清理函数
|
|
2621
2863
|
const cleanup = async () => {
|
|
2622
2864
|
console.log('\n正在关闭服务...');
|
|
@@ -2634,6 +2876,20 @@ function startServer(port, apiUrl, dataDir = '') {
|
|
|
2634
2876
|
}
|
|
2635
2877
|
for (const [aid, instance] of aidInstances) {
|
|
2636
2878
|
console.log(`[Server] 清理 AID: ${aid}`);
|
|
2879
|
+
if (instance.groupInitialized) {
|
|
2880
|
+
try {
|
|
2881
|
+
await instance.agentCP.leaveAllGroupSessions();
|
|
2882
|
+
}
|
|
2883
|
+
catch (e) {
|
|
2884
|
+
console.warn(`[Server] leaveAllGroupSessions error:`, e.message);
|
|
2885
|
+
}
|
|
2886
|
+
try {
|
|
2887
|
+
await instance.agentCP.closeGroupMessageStore();
|
|
2888
|
+
}
|
|
2889
|
+
catch (e) {
|
|
2890
|
+
console.warn(`[Server] closeGroupMessageStore error:`, e.message);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2637
2893
|
if (instance.heartbeatClient) {
|
|
2638
2894
|
instance.heartbeatClient.offline();
|
|
2639
2895
|
}
|
|
@@ -2642,6 +2898,12 @@ function startServer(port, apiUrl, dataDir = '') {
|
|
|
2642
2898
|
}
|
|
2643
2899
|
}
|
|
2644
2900
|
aidInstances.clear();
|
|
2901
|
+
// 关闭所有浏览器 WS 连接
|
|
2902
|
+
for (const client of browserWsClients) {
|
|
2903
|
+
client.close();
|
|
2904
|
+
}
|
|
2905
|
+
browserWsClients.clear();
|
|
2906
|
+
wss.close();
|
|
2645
2907
|
server.close(() => {
|
|
2646
2908
|
console.log('服务已关闭');
|
|
2647
2909
|
process.exit(0);
|
package/dist/websocket.js
CHANGED
|
@@ -409,16 +409,11 @@ class WSClient {
|
|
|
409
409
|
const { cmd, data } = message;
|
|
410
410
|
// Raw message interception: allow group protocol routing etc.
|
|
411
411
|
if (cmd === "session_message" && this.rawMessageCallback) {
|
|
412
|
-
// console.log(`[WS] handleMessage: cmd=session_message, sender=${(data as any)?.sender}, has rawMessageCallback=true`);
|
|
413
412
|
const intercepted = this.rawMessageCallback(message);
|
|
414
|
-
// console.log(`[WS] rawMessageCallback returned: ${intercepted}`);
|
|
415
413
|
if (intercepted) {
|
|
416
414
|
return; // intercepted
|
|
417
415
|
}
|
|
418
416
|
}
|
|
419
|
-
else if (cmd === "session_message") {
|
|
420
|
-
// console.log(`[WS] handleMessage: cmd=session_message, sender=${(data as any)?.sender}, has rawMessageCallback=false (no interceptor registered!)`);
|
|
421
|
-
}
|
|
422
417
|
if (cmd === "create_session_ack") {
|
|
423
418
|
const { session_id, identifying_code } = data;
|
|
424
419
|
this.emitter.emit('session', {
|