acp-ts 1.2.0 → 1.2.2

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
@@ -99,7 +99,7 @@ declare class AgentCP implements IAgentCP {
99
99
  * 处理批量推送消息:排序、存储、ACK 最后一条。
100
100
  * 返回排序后的消息列表,供上层使用(如推送给浏览器)。
101
101
  */
102
- processAndAckBatch(groupId: string, batch: GroupMessageBatch): GroupMessage[];
102
+ processAndAckBatch(groupId: string, batch: GroupMessageBatch): Promise<GroupMessage[]>;
103
103
  /**
104
104
  * 异步 ACK 消息(不阻塞调用方)
105
105
  */
@@ -158,11 +158,11 @@ declare class AgentCP implements IAgentCP {
158
158
  /**
159
159
  * 添加群消息到本地存储
160
160
  */
161
- addGroupMessageToStore(groupId: string, msg: GroupMessage): void;
161
+ addGroupMessageToStore(groupId: string, msg: GroupMessage): Promise<void>;
162
162
  /**
163
163
  * 批量添加群消息到本地存储
164
164
  */
165
- addGroupMessagesToStore(groupId: string, msgs: GroupMessage[]): void;
165
+ addGroupMessagesToStore(groupId: string, msgs: GroupMessage[]): Promise<void>;
166
166
  /**
167
167
  * 获取本地存储中群组的最新消息ID(用于增量拉取)
168
168
  */
package/dist/agentcp.js CHANGED
@@ -445,7 +445,9 @@ class AgentCP {
445
445
  },
446
446
  onGroupMessageBatch: (groupId, batch) => {
447
447
  console.log(`[Group][DefaultHandler] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}]`);
448
- this.processAndAckBatch(groupId, batch);
448
+ this.processAndAckBatch(groupId, batch).catch(e => {
449
+ console.error(`[Group][DefaultHandler] processAndAckBatch failed: group=${groupId}`, e);
450
+ });
449
451
  },
450
452
  onGroupEvent(groupId, evt) {
451
453
  console.log(`[Group][DefaultHandler] onGroupEvent: group=${groupId} event=${evt.event_type}`);
@@ -456,9 +458,9 @@ class AgentCP {
456
458
  * 处理批量推送消息:排序、存储、ACK 最后一条。
457
459
  * 返回排序后的消息列表,供上层使用(如推送给浏览器)。
458
460
  */
459
- processAndAckBatch(groupId, batch) {
461
+ async processAndAckBatch(groupId, batch) {
460
462
  const sorted = [...batch.messages].sort((a, b) => a.msg_id - b.msg_id);
461
- this.addGroupMessagesToStore(groupId, sorted);
463
+ await this.addGroupMessagesToStore(groupId, sorted);
462
464
  // ACK batch 中最后一条消息
463
465
  if (sorted.length > 0) {
464
466
  const lastMsgId = sorted[sorted.length - 1].msg_id;
@@ -561,6 +563,17 @@ class AgentCP {
561
563
  this.groupMessageStore.getOrCreateGroup(membership.group_id, this._groupTargetAid, name);
562
564
  }
563
565
  }
566
+ // 清理本地有但服务端已不存在的群组
567
+ if (this.groupMessageStore) {
568
+ const serverGroupIds = new Set(result.groups.map(g => g.group_id));
569
+ const localGroups = this.groupMessageStore.getGroupList();
570
+ for (const local of localGroups) {
571
+ if (!serverGroupIds.has(local.groupId)) {
572
+ await this.groupMessageStore.deleteGroup(local.groupId);
573
+ console.log(`[Group] syncGroupList: 清理本地已退出群组 ${local.groupId}`);
574
+ }
575
+ }
576
+ }
564
577
  return groups;
565
578
  }
566
579
  /**
@@ -605,18 +618,18 @@ class AgentCP {
605
618
  /**
606
619
  * 添加群消息到本地存储
607
620
  */
608
- addGroupMessageToStore(groupId, msg) {
621
+ async addGroupMessageToStore(groupId, msg) {
609
622
  if (!this.groupMessageStore)
610
623
  return;
611
- this.groupMessageStore.addMessage(groupId, msg);
624
+ await this.groupMessageStore.addMessage(groupId, msg);
612
625
  }
613
626
  /**
614
627
  * 批量添加群消息到本地存储
615
628
  */
616
- addGroupMessagesToStore(groupId, msgs) {
629
+ async addGroupMessagesToStore(groupId, msgs) {
617
630
  if (!this.groupMessageStore)
618
631
  return;
619
- this.groupMessageStore.addMessages(groupId, msgs);
632
+ await this.groupMessageStore.addMessages(groupId, msgs);
620
633
  }
621
634
  /**
622
635
  * 获取本地存储中群组的最新消息ID(用于增量拉取)
@@ -659,7 +672,7 @@ class AgentCP {
659
672
  metadata: (_f = m.metadata) !== null && _f !== void 0 ? _f : null,
660
673
  });
661
674
  });
662
- this.addGroupMessagesToStore(groupId, msgs);
675
+ await this.addGroupMessagesToStore(groupId, msgs);
663
676
  // ACK 这批消息中的最后一条
664
677
  const lastMsgId = msgs[msgs.length - 1].msg_id;
665
678
  await this.groupOps.ackMessages(this._groupTargetAid, groupId, lastMsgId);
@@ -160,7 +160,9 @@ class ACPGroupClient {
160
160
  this._pendingReqs.clear();
161
161
  if (this._cursorStore != null) {
162
162
  try {
163
- this._cursorStore.close();
163
+ this._cursorStore.close().catch(e => {
164
+ console.error("[ACPGroupClient] cursor store close error:", e);
165
+ });
164
166
  }
165
167
  catch (e) {
166
168
  console.error("[ACPGroupClient] cursor store close error:", e);
@@ -10,8 +10,8 @@ export interface CursorStore {
10
10
  saveEventCursor(groupId: string, eventCursor: number): void;
11
11
  loadCursor(groupId: string): [number, number];
12
12
  removeCursor(groupId: string): void;
13
- flush(): void;
14
- close(): void;
13
+ flush(): Promise<void>;
14
+ close(): Promise<void>;
15
15
  }
16
16
  /**
17
17
  * In-memory + JSON file cursor store.
@@ -24,13 +24,18 @@ export declare class LocalCursorStore implements CursorStore {
24
24
  private _cursors;
25
25
  private _filePath;
26
26
  private _dirty;
27
+ private _flushTimer;
28
+ private _flushPromise;
29
+ private _loaded;
27
30
  constructor(filePath?: string);
31
+ init(): Promise<void>;
28
32
  saveMsgCursor(groupId: string, msgCursor: number): void;
29
33
  saveEventCursor(groupId: string, eventCursor: number): void;
30
34
  loadCursor(groupId: string): [number, number];
31
35
  removeCursor(groupId: string): void;
32
- flush(): void;
33
- close(): void;
36
+ flush(): Promise<void>;
37
+ close(): Promise<void>;
38
+ private _flushDebounced;
34
39
  private _write;
35
40
  private _load;
36
41
  }
@@ -17,9 +17,15 @@ class LocalCursorStore {
17
17
  constructor(filePath = "") {
18
18
  this._cursors = {};
19
19
  this._dirty = false;
20
+ this._flushTimer = null;
21
+ this._flushPromise = null;
22
+ this._loaded = false;
20
23
  this._filePath = filePath;
21
- if (filePath && websocket_1.isNodeEnvironment) {
22
- this._load();
24
+ }
25
+ async init() {
26
+ if (this._filePath && websocket_1.isNodeEnvironment && !this._loaded) {
27
+ await this._load();
28
+ this._loaded = true;
23
29
  }
24
30
  }
25
31
  saveMsgCursor(groupId, msgCursor) {
@@ -29,6 +35,7 @@ class LocalCursorStore {
29
35
  if (msgCursor > this._cursors[groupId].msg_cursor) {
30
36
  this._cursors[groupId].msg_cursor = msgCursor;
31
37
  this._dirty = true;
38
+ this._flushDebounced();
32
39
  }
33
40
  }
34
41
  saveEventCursor(groupId, eventCursor) {
@@ -38,6 +45,7 @@ class LocalCursorStore {
38
45
  if (eventCursor > this._cursors[groupId].event_cursor) {
39
46
  this._cursors[groupId].event_cursor = eventCursor;
40
47
  this._dirty = true;
48
+ this._flushDebounced();
41
49
  }
42
50
  }
43
51
  loadCursor(groupId) {
@@ -51,37 +59,58 @@ class LocalCursorStore {
51
59
  if (groupId in this._cursors) {
52
60
  delete this._cursors[groupId];
53
61
  this._dirty = true;
62
+ this._flushDebounced();
54
63
  }
55
64
  }
56
- flush() {
65
+ async flush() {
57
66
  if (!this._filePath || !websocket_1.isNodeEnvironment) {
58
67
  return;
59
68
  }
60
69
  if (!this._dirty) {
61
70
  return;
62
71
  }
63
- this._write();
72
+ await this._write();
73
+ }
74
+ async close() {
75
+ if (this._flushTimer) {
76
+ clearTimeout(this._flushTimer);
77
+ this._flushTimer = null;
78
+ }
79
+ await this.flush();
64
80
  }
65
- close() {
66
- this.flush();
81
+ _flushDebounced() {
82
+ if (this._flushTimer)
83
+ return;
84
+ this._flushTimer = setTimeout(async () => {
85
+ this._flushTimer = null;
86
+ try {
87
+ await this.flush();
88
+ }
89
+ catch (e) {
90
+ console.error('[CursorStore] debounced flush failed:', e);
91
+ }
92
+ }, 500);
67
93
  }
68
- _write() {
94
+ async _write() {
69
95
  try {
70
96
  const fs = require('fs');
71
- fs.writeFileSync(this._filePath, JSON.stringify(this._cursors, null, 2), 'utf-8');
97
+ const content = JSON.stringify(this._cursors, null, 2);
98
+ const tmpPath = this._filePath + '.tmp';
99
+ await fs.promises.writeFile(tmpPath, content, 'utf-8');
100
+ await fs.promises.rename(tmpPath, this._filePath);
72
101
  this._dirty = false;
73
102
  }
74
103
  catch (e) {
75
104
  console.error(`[CursorStore] write to ${this._filePath} failed:`, e);
76
105
  }
77
106
  }
78
- _load() {
107
+ async _load() {
79
108
  try {
80
109
  const fs = require('fs');
81
110
  if (!fs.existsSync(this._filePath)) {
82
111
  return;
83
112
  }
84
- const content = fs.readFileSync(this._filePath, 'utf-8');
113
+ const content = await fs.promises.readFile(this._filePath, 'utf-8');
85
114
  if (content) {
86
115
  this._cursors = JSON.parse(content);
87
116
  }
@@ -27,6 +27,7 @@ export declare class GroupMessageStore {
27
27
  private maxEventsPerGroup;
28
28
  private groups;
29
29
  private _indexDirty;
30
+ private _flushIndexPromise;
30
31
  constructor(options: {
31
32
  persistMessages: boolean;
32
33
  basePath: string;
@@ -45,16 +46,16 @@ export declare class GroupMessageStore {
45
46
  getGroupList(): GroupRecord[];
46
47
  getGroup(groupId: string): GroupRecord | null;
47
48
  deleteGroup(groupId: string): Promise<boolean>;
48
- addMessage(groupId: string, msg: GroupMessage): void;
49
- addMessages(groupId: string, msgs: GroupMessage[]): void;
49
+ addMessage(groupId: string, msg: GroupMessage): Promise<void>;
50
+ addMessages(groupId: string, msgs: GroupMessage[]): Promise<void>;
50
51
  getMessages(groupId: string, options?: {
51
52
  afterMsgId?: number;
52
53
  beforeMsgId?: number;
53
54
  limit?: number;
54
55
  }): GroupMessage[];
55
56
  getLatestMessages(groupId: string, limit?: number): GroupMessage[];
56
- addEvent(groupId: string, evt: GroupEvent): void;
57
- addEvents(groupId: string, evts: GroupEvent[]): void;
57
+ addEvent(groupId: string, evt: GroupEvent): Promise<void>;
58
+ addEvents(groupId: string, evts: GroupEvent[]): Promise<void>;
58
59
  getEvents(groupId: string, options?: {
59
60
  afterEventId?: number;
60
61
  limit?: number;
@@ -63,7 +64,7 @@ export declare class GroupMessageStore {
63
64
  private writeJsonl;
64
65
  private flushMessages;
65
66
  private flushEvents;
66
- private flushIndexAsync;
67
+ private flushIndexQueued;
67
68
  flushIndex(): Promise<void>;
68
69
  flush(ownerAid: string): Promise<void>;
69
70
  flushAll(): Promise<void>;
@@ -17,6 +17,7 @@ class GroupMessageStore {
17
17
  this.ownerAid = '';
18
18
  this.groups = new Map();
19
19
  this._indexDirty = false;
20
+ this._flushIndexPromise = null;
20
21
  this.persistMessages = options.persistMessages;
21
22
  this.basePath = options.basePath;
22
23
  this.maxMessagesPerGroup = (_a = options.maxMessagesPerGroup) !== null && _a !== void 0 ? _a : 5000;
@@ -43,12 +44,12 @@ class GroupMessageStore {
43
44
  const path = require('path');
44
45
  return path.join(this.getGroupDir(aid, groupId), 'events.jsonl');
45
46
  }
46
- ensureDir(dir) {
47
+ async ensureDir(dir) {
47
48
  if (!websocket_1.isNodeEnvironment)
48
49
  return;
49
50
  const fs = require('fs');
50
51
  if (!fs.existsSync(dir)) {
51
- fs.mkdirSync(dir, { recursive: true });
52
+ await fs.promises.mkdir(dir, { recursive: true });
52
53
  }
53
54
  }
54
55
  // ---- load ----
@@ -63,13 +64,13 @@ class GroupMessageStore {
63
64
  if (!fs.existsSync(indexPath))
64
65
  return;
65
66
  try {
66
- const raw = fs.readFileSync(indexPath, 'utf-8');
67
+ const raw = await fs.promises.readFile(indexPath, 'utf-8');
67
68
  const records = JSON.parse(raw);
68
69
  if (!Array.isArray(records))
69
70
  return;
70
71
  for (const r of records) {
71
- const messages = this.readJsonl(this.getMessagesPath(ownerAid, r.groupId));
72
- const events = this.readJsonl(this.getEventsPath(ownerAid, r.groupId));
72
+ const messages = await this.readJsonl(this.getMessagesPath(ownerAid, r.groupId));
73
+ const events = await this.readJsonl(this.getEventsPath(ownerAid, r.groupId));
73
74
  this.groups.set(r.groupId, { record: r, messages, events });
74
75
  }
75
76
  }
@@ -77,27 +78,31 @@ class GroupMessageStore {
77
78
  console.error('[GroupMessageStore] 加载索引失败:', e);
78
79
  }
79
80
  }
80
- readJsonl(filePath) {
81
+ async readJsonl(filePath) {
81
82
  if (!websocket_1.isNodeEnvironment)
82
83
  return [];
83
84
  const fs = require('fs');
84
85
  if (!fs.existsSync(filePath))
85
86
  return [];
86
87
  try {
87
- const raw = fs.readFileSync(filePath, 'utf-8');
88
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
88
89
  const items = [];
89
- for (const line of raw.split('\n')) {
90
- const l = line.trim();
90
+ const lines = raw.split('\n');
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const l = lines[i].trim();
91
93
  if (!l)
92
94
  continue;
93
95
  try {
94
96
  items.push(JSON.parse(l));
95
97
  }
96
- catch (_a) { }
98
+ catch (lineErr) {
99
+ console.warn(`[GroupMessageStore] JSONL 解析失败 (${filePath} 行${i + 1}): ${l.substring(0, 80)}`, lineErr);
100
+ }
97
101
  }
98
102
  return items;
99
103
  }
100
- catch (_b) {
104
+ catch (e) {
105
+ console.warn(`[GroupMessageStore] 读取 JSONL 文件失败 (${filePath}):`, e);
101
106
  return [];
102
107
  }
103
108
  }
@@ -114,7 +119,7 @@ class GroupMessageStore {
114
119
  data = { record, messages: [], events: [] };
115
120
  this.groups.set(groupId, data);
116
121
  this._indexDirty = true;
117
- this.flushIndexAsync();
122
+ this.flushIndexQueued();
118
123
  }
119
124
  return data.record;
120
125
  }
@@ -149,11 +154,10 @@ class GroupMessageStore {
149
154
  return true;
150
155
  }
151
156
  // ---- message storage ----
152
- addMessage(groupId, msg) {
157
+ async addMessage(groupId, msg) {
153
158
  const data = this.groups.get(groupId);
154
159
  if (!data)
155
160
  return;
156
- // dedup: skip if msg_id already seen
157
161
  if (msg.msg_id <= data.record.lastMsgId)
158
162
  return;
159
163
  data.messages.push(msg);
@@ -161,28 +165,32 @@ class GroupMessageStore {
161
165
  data.record.messageCount = data.messages.length;
162
166
  data.record.lastMessageAt = msg.timestamp || Date.now();
163
167
  this._indexDirty = true;
164
- // truncate if over limit
165
168
  if (data.messages.length > this.maxMessagesPerGroup) {
166
169
  const excess = data.messages.length - this.maxMessagesPerGroup;
167
170
  data.messages.splice(0, excess);
168
171
  data.record.messageCount = data.messages.length;
169
- this.flushMessages(groupId);
172
+ await this.flushMessages(groupId);
170
173
  }
171
174
  else {
172
- this.appendJsonl(this.getMessagesPath(this.ownerAid, groupId), msg);
175
+ await this.appendJsonl(this.getMessagesPath(this.ownerAid, groupId), msg);
173
176
  }
174
- this.flushIndexAsync();
177
+ this.flushIndexQueued();
175
178
  }
176
- addMessages(groupId, msgs) {
179
+ async addMessages(groupId, msgs) {
177
180
  const data = this.groups.get(groupId);
178
181
  if (!data)
179
182
  return;
183
+ const sorted = [...msgs].sort((a, b) => a.msg_id - b.msg_id);
184
+ const existingIds = new Set(data.messages.map(m => m.msg_id));
180
185
  let added = 0;
181
- for (const msg of msgs) {
182
- if (msg.msg_id <= data.record.lastMsgId)
186
+ for (const msg of sorted) {
187
+ if (existingIds.has(msg.msg_id))
183
188
  continue;
184
189
  data.messages.push(msg);
185
- data.record.lastMsgId = msg.msg_id;
190
+ existingIds.add(msg.msg_id);
191
+ if (msg.msg_id > data.record.lastMsgId) {
192
+ data.record.lastMsgId = msg.msg_id;
193
+ }
186
194
  data.record.lastMessageAt = msg.timestamp || Date.now();
187
195
  added++;
188
196
  }
@@ -194,12 +202,9 @@ class GroupMessageStore {
194
202
  const excess = data.messages.length - this.maxMessagesPerGroup;
195
203
  data.messages.splice(0, excess);
196
204
  data.record.messageCount = data.messages.length;
197
- this.flushMessages(groupId);
198
- }
199
- else {
200
- this.flushMessages(groupId);
201
205
  }
202
- this.flushIndexAsync();
206
+ await this.flushMessages(groupId);
207
+ this.flushIndexQueued();
203
208
  }
204
209
  getMessages(groupId, options) {
205
210
  const data = this.groups.get(groupId);
@@ -224,7 +229,7 @@ class GroupMessageStore {
224
229
  return data.messages.slice(-limit);
225
230
  }
226
231
  // ---- event storage ----
227
- addEvent(groupId, evt) {
232
+ async addEvent(groupId, evt) {
228
233
  const data = this.groups.get(groupId);
229
234
  if (!data)
230
235
  return;
@@ -238,14 +243,14 @@ class GroupMessageStore {
238
243
  const excess = data.events.length - this.maxEventsPerGroup;
239
244
  data.events.splice(0, excess);
240
245
  data.record.eventCount = data.events.length;
241
- this.flushEvents(groupId);
246
+ await this.flushEvents(groupId);
242
247
  }
243
248
  else {
244
- this.appendJsonl(this.getEventsPath(this.ownerAid, groupId), evt);
249
+ await this.appendJsonl(this.getEventsPath(this.ownerAid, groupId), evt);
245
250
  }
246
- this.flushIndexAsync();
251
+ this.flushIndexQueued();
247
252
  }
248
- addEvents(groupId, evts) {
253
+ async addEvents(groupId, evts) {
249
254
  const data = this.groups.get(groupId);
250
255
  if (!data)
251
256
  return;
@@ -266,8 +271,8 @@ class GroupMessageStore {
266
271
  data.events.splice(0, excess);
267
272
  data.record.eventCount = data.events.length;
268
273
  }
269
- this.flushEvents(groupId);
270
- this.flushIndexAsync();
274
+ await this.flushEvents(groupId);
275
+ this.flushIndexQueued();
271
276
  }
272
277
  getEvents(groupId, options) {
273
278
  const data = this.groups.get(groupId);
@@ -283,47 +288,54 @@ class GroupMessageStore {
283
288
  return result;
284
289
  }
285
290
  // ---- persistence ----
286
- appendJsonl(filePath, item) {
291
+ async appendJsonl(filePath, item) {
287
292
  if (!this.persistMessages || !websocket_1.isNodeEnvironment || !this.ownerAid)
288
293
  return;
289
294
  const fs = require('fs');
290
295
  const path = require('path');
291
- this.ensureDir(path.dirname(filePath));
296
+ await this.ensureDir(path.dirname(filePath));
292
297
  try {
293
- fs.appendFileSync(filePath, JSON.stringify(item) + '\n');
298
+ await fs.promises.appendFile(filePath, JSON.stringify(item) + '\n');
294
299
  }
295
300
  catch (e) {
296
301
  console.error(`[GroupMessageStore] 追加写入失败 (${filePath}):`, e);
297
302
  }
298
303
  }
299
- writeJsonl(filePath, items) {
304
+ async writeJsonl(filePath, items) {
300
305
  if (!this.persistMessages || !websocket_1.isNodeEnvironment || !this.ownerAid)
301
306
  return;
302
307
  const fs = require('fs');
303
308
  const path = require('path');
304
- this.ensureDir(path.dirname(filePath));
309
+ await this.ensureDir(path.dirname(filePath));
305
310
  try {
306
311
  const lines = items.map(i => JSON.stringify(i)).join('\n');
307
- fs.writeFileSync(filePath, lines ? lines + '\n' : '');
312
+ const content = lines ? lines + '\n' : '';
313
+ const tmpPath = filePath + '.tmp';
314
+ await fs.promises.writeFile(tmpPath, content);
315
+ await fs.promises.rename(tmpPath, filePath);
308
316
  }
309
317
  catch (e) {
310
318
  console.error(`[GroupMessageStore] 全量写入失败 (${filePath}):`, e);
311
319
  }
312
320
  }
313
- flushMessages(groupId) {
321
+ async flushMessages(groupId) {
314
322
  const data = this.groups.get(groupId);
315
323
  if (!data || !this.ownerAid)
316
324
  return;
317
- this.writeJsonl(this.getMessagesPath(this.ownerAid, groupId), data.messages);
325
+ await this.writeJsonl(this.getMessagesPath(this.ownerAid, groupId), data.messages);
318
326
  }
319
- flushEvents(groupId) {
327
+ async flushEvents(groupId) {
320
328
  const data = this.groups.get(groupId);
321
329
  if (!data || !this.ownerAid)
322
330
  return;
323
- this.writeJsonl(this.getEventsPath(this.ownerAid, groupId), data.events);
331
+ await this.writeJsonl(this.getEventsPath(this.ownerAid, groupId), data.events);
324
332
  }
325
- flushIndexAsync() {
326
- this.flushIndex().catch(() => { });
333
+ flushIndexQueued() {
334
+ var _a;
335
+ const prev = (_a = this._flushIndexPromise) !== null && _a !== void 0 ? _a : Promise.resolve();
336
+ this._flushIndexPromise = prev.then(() => this.flushIndex()).catch(e => {
337
+ console.error('[GroupMessageStore] 索引刷盘失败:', e);
338
+ });
327
339
  }
328
340
  async flushIndex() {
329
341
  if (!this.persistMessages || !websocket_1.isNodeEnvironment || !this.ownerAid)
@@ -332,11 +344,14 @@ class GroupMessageStore {
332
344
  return;
333
345
  const fs = require('fs');
334
346
  const dir = this.getGroupsDir(this.ownerAid);
335
- this.ensureDir(dir);
347
+ await this.ensureDir(dir);
336
348
  const records = Array.from(this.groups.values())
337
349
  .map(d => d.record);
338
350
  try {
339
- fs.writeFileSync(this.getIndexPath(this.ownerAid), JSON.stringify(records, null, 2));
351
+ const indexPath = this.getIndexPath(this.ownerAid);
352
+ const tmpPath = indexPath + '.tmp';
353
+ await fs.promises.writeFile(tmpPath, JSON.stringify(records, null, 2));
354
+ await fs.promises.rename(tmpPath, indexPath);
340
355
  this._indexDirty = false;
341
356
  }
342
357
  catch (e) {
@@ -348,9 +363,9 @@ class GroupMessageStore {
348
363
  return;
349
364
  this.ownerAid = ownerAid;
350
365
  await this.flushIndex();
351
- for (const [groupId, data] of this.groups) {
352
- this.flushMessages(groupId);
353
- this.flushEvents(groupId);
366
+ for (const [groupId] of this.groups) {
367
+ await this.flushMessages(groupId);
368
+ await this.flushEvents(groupId);
354
369
  }
355
370
  }
356
371
  async flushAll() {
@@ -175,11 +175,11 @@ interface IAgentCP {
175
175
  /**
176
176
  * 添加群消息到本地存储
177
177
  */
178
- addGroupMessageToStore(groupId: string, msg: GroupMessage): void;
178
+ addGroupMessageToStore(groupId: string, msg: GroupMessage): Promise<void>;
179
179
  /**
180
180
  * 批量添加群消息到本地存储
181
181
  */
182
- addGroupMessagesToStore(groupId: string, msgs: GroupMessage[]): void;
182
+ addGroupMessagesToStore(groupId: string, msgs: GroupMessage[]): Promise<void>;
183
183
  /**
184
184
  * 获取本地存储中群组的最新消息ID
185
185
  */
package/dist/server.js CHANGED
@@ -135,11 +135,27 @@ async function doEnsureOnline(aid) {
135
135
  instance.agentWS.acceptInviteFromHeartbeat(invite.sessionId, invite.inviterAgentId, invite.inviteCode);
136
136
  }
137
137
  });
138
- // 心跳重连成功后,自动触发 WebSocket 重连
138
+ // 心跳重连成功后,自动触发 WebSocket 重连 + 群组重新注册
139
139
  hb.onReconnect(() => {
140
140
  if (instance.agentWS) {
141
141
  console.log('[Server] 心跳重连成功,触发 WebSocket 重连...');
142
- instance.agentWS.reconnect().catch((err) => {
142
+ instance.agentWS.reconnect().then(async () => {
143
+ // WebSocket 重连成功后,重新注册所有在线群组
144
+ // 断线期间 group.ap 会将在线状态过期,必须重新 register_online 才能收到推送
145
+ const onlineGroups = instance.agentCP.getOnlineGroups();
146
+ if (onlineGroups.length > 0) {
147
+ console.log(`[Server] WebSocket 重连成功,重新注册 ${onlineGroups.length} 个在线群组...`);
148
+ for (const groupId of onlineGroups) {
149
+ try {
150
+ await instance.agentCP.joinGroupSession(groupId);
151
+ console.log(`[Server] 群组重新注册成功: ${groupId}`);
152
+ }
153
+ catch (e) {
154
+ console.warn(`[Server] 群组重新注册失败: ${groupId}`, e.message || e);
155
+ }
156
+ }
157
+ }
158
+ }).catch((err) => {
143
159
  console.error('[Server] WebSocket 重连失败:', err);
144
160
  });
145
161
  }
@@ -327,16 +343,19 @@ async function ensureGroupClient(instance) {
327
343
  },
328
344
  onGroupMessageBatch(groupId, batch) {
329
345
  console.log(`[Group] onGroupMessageBatch: group=${groupId} count=${batch.count} range=[${batch.start_msg_id}, ${batch.latest_msg_id}]`);
330
- // 存储 + ACK(统一由 agentcp 处理)
331
- const sorted = instance.agentCP.processAndAckBatch(groupId, batch);
332
- // 推送消息列表给浏览器
333
- broadcastToBrowser({
334
- type: 'group_message_batch',
335
- group_id: groupId,
336
- messages: sorted,
337
- count: batch.count,
338
- start_msg_id: batch.start_msg_id,
339
- latest_msg_id: batch.latest_msg_id,
346
+ // 存储 + ACK(统一由 agentcp 处理),注意 processAndAckBatch 是 async
347
+ instance.agentCP.processAndAckBatch(groupId, batch).then((sorted) => {
348
+ // 推送消息列表给浏览器
349
+ broadcastToBrowser({
350
+ type: 'group_message_batch',
351
+ group_id: groupId,
352
+ messages: sorted,
353
+ count: batch.count,
354
+ start_msg_id: batch.start_msg_id,
355
+ latest_msg_id: batch.latest_msg_id,
356
+ });
357
+ }).catch((e) => {
358
+ console.error(`[Group] processAndAckBatch failed: group=${groupId}`, e);
340
359
  });
341
360
  },
342
361
  onGroupEvent(groupId, evt) {
@@ -1386,8 +1405,9 @@ const chatHtml = `<!DOCTYPE html>
1386
1405
  } else {
1387
1406
  D.input.disabled=false; D.input.placeholder='输入消息...';
1388
1407
  }
1408
+ var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1389
1409
  D.msgs.innerHTML=html;
1390
- D.msgs.scrollTop=D.msgs.scrollHeight;
1410
+ if(wasAtBottom) D.msgs.scrollTop=D.msgs.scrollHeight;
1391
1411
  }
1392
1412
 
1393
1413
  function updateDot(st){
@@ -1724,8 +1744,9 @@ const chatHtml = `<!DOCTYPE html>
1724
1744
  '<div class="msg-meta">'+(sent?'我':escH(name))+' · '+t+'</div>' +
1725
1745
  '</div></div>';
1726
1746
  }).join('');
1747
+ var wasAtBottom=D.msgs.scrollHeight-D.msgs.scrollTop-D.msgs.clientHeight<150;
1727
1748
  D.msgs.innerHTML=html;
1728
- D.msgs.scrollTop=D.msgs.scrollHeight;
1749
+ if(wasAtBottom) D.msgs.scrollTop=D.msgs.scrollHeight;
1729
1750
  // 异步加载未缓存的 agent info,加载完成后重新渲染以更新头像
1730
1751
  var unique=needFetch.filter(function(v,i,a){ return a.indexOf(v)===i; });
1731
1752
  unique.forEach(function(aid){
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acp-ts",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "基于 ACP智能体通信协议 的智能体通信库,提供智能体身份管理和实时通信功能",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",