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 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
- pullAndStoreGroupMessages(groupId: string, limit?: number): Promise<GroupMessage[]>;
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
- const lastMsgId = this.getGroupLastMsgId(groupId);
649
+ let after = afterMsgId;
624
650
  try {
625
- const pulled = await this.groupOps.pullMessages(this._groupTargetAid, groupId, lastMsgId, limit);
626
- if (pulled.messages && pulled.messages.length > 0) {
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] pullMessages error:', e.message);
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
  */
@@ -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] !!! JSON.parse failed for incoming payload:`, e, `payload(first 200): ${payload.substring(0, 200)}`);
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
- pullMessages(targetAid: string, groupId: string, afterMsgId: number, limit?: number): Promise<PullMessagesResp>;
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>;
@@ -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
- async pullMessages(targetAid, groupId, afterMsgId, limit = 0) {
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 = { after_msg_id: afterMsgId };
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
- instance.agentCP.pullAndStoreGroupMessages(groupId, 20).catch(e => {
233
- console.error(`[Group] auto-pull messages failed for group=${groupId}:`, e);
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="输入群组名称" onkeypress="if(event.key==='Enter')doCreateGroup()">
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
- var r=await fetch('/api/aid'); var d=await r.json();
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=new Date(s.lastMessageAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
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=new Date(m.timestamp).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
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 pollGroupMessages();
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?new Date(m.timestamp*1000).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):'';
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?new Date(req.created_at*1000).toLocaleString():'';
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
- var _origPoll=poll;
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 result = await ops.createGroup(target, name, body.options);
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
- const messages = await instance.agentCP.pullAndStoreGroupMessages(groupId, 50);
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', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acp-ts",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "基于 ACP智能体通信协议 的智能体通信库,提供智能体身份管理和实时通信功能",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",