acp-ts 1.2.0 → 1.2.1
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/assets/topic-context-partitioning.md +871 -0
- package/dist/agentcp.d.ts +3 -3
- package/dist/agentcp.js +21 -8
- package/dist/group/client.js +3 -1
- package/dist/group/cursor_store.d.ts +9 -4
- package/dist/group/cursor_store.js +39 -10
- package/dist/group/message_store.d.ts +6 -5
- package/dist/group/message_store.js +66 -51
- package/dist/interfaces.d.ts +2 -2
- package/dist/server.js +31 -12
- package/package.json +1 -1
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);
|
package/dist/group/client.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
66
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
88
|
+
const raw = await fs.promises.readFile(filePath, 'utf-8');
|
|
88
89
|
const items = [];
|
|
89
|
-
|
|
90
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
|
182
|
-
if (msg.msg_id
|
|
186
|
+
for (const msg of sorted) {
|
|
187
|
+
if (existingIds.has(msg.msg_id))
|
|
183
188
|
continue;
|
|
184
189
|
data.messages.push(msg);
|
|
185
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
|
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() {
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -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().
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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) {
|