openclaw-mochat 2026.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/index.ts +19 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +41 -0
- package/src/accounts.ts +180 -0
- package/src/api.ts +453 -0
- package/src/channel.ts +253 -0
- package/src/config-schema.ts +46 -0
- package/src/delay-buffer.test.ts +81 -0
- package/src/delay-buffer.ts +123 -0
- package/src/event-store.ts +56 -0
- package/src/inbound.ts +402 -0
- package/src/poller.ts +116 -0
- package/src/runtime.ts +14 -0
- package/src/socket.ts +439 -0
- package/src/tool.ts +252 -0
package/src/poller.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ChannelLogSink } from "openclaw/plugin-sdk";
|
|
2
|
+
import { watchSession } from "./api.js";
|
|
3
|
+
import type { ResolvedMochatAccount } from "./accounts.js";
|
|
4
|
+
import { handleInboundMessage, type MochatStatusSink } from "./inbound.js";
|
|
5
|
+
|
|
6
|
+
async function sleep(ms: number, signal?: AbortSignal) {
|
|
7
|
+
if (ms <= 0) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await new Promise<void>((resolve) => {
|
|
11
|
+
const timer = setTimeout(resolve, ms);
|
|
12
|
+
if (signal) {
|
|
13
|
+
signal.addEventListener(
|
|
14
|
+
"abort",
|
|
15
|
+
() => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
resolve();
|
|
18
|
+
},
|
|
19
|
+
{ once: true },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function computeBackoffMs(params: {
|
|
26
|
+
baseDelayMs: number;
|
|
27
|
+
consecutiveErrors: number;
|
|
28
|
+
maxDelayMs: number;
|
|
29
|
+
}) {
|
|
30
|
+
const baseDelayMs = Math.max(1, params.baseDelayMs);
|
|
31
|
+
const maxDelayMs = Math.max(baseDelayMs, params.maxDelayMs);
|
|
32
|
+
const exponent = Math.max(0, params.consecutiveErrors - 1);
|
|
33
|
+
const delayMs = baseDelayMs * Math.pow(2, exponent);
|
|
34
|
+
return Math.min(delayMs, maxDelayMs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function startMochatSessionPoller(params: {
|
|
38
|
+
account: ResolvedMochatAccount;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
log?: ChannelLogSink;
|
|
41
|
+
abortSignal: AbortSignal;
|
|
42
|
+
statusSink?: MochatStatusSink;
|
|
43
|
+
}) {
|
|
44
|
+
const { account, sessionId, log, abortSignal, statusSink } = params;
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
let stopped = false;
|
|
47
|
+
let cursor = 0;
|
|
48
|
+
let consecutiveErrors = 0;
|
|
49
|
+
const maxBackoffMs = 30000;
|
|
50
|
+
|
|
51
|
+
const onAbort = () => {
|
|
52
|
+
stopped = true;
|
|
53
|
+
controller.abort();
|
|
54
|
+
};
|
|
55
|
+
if (abortSignal.aborted) {
|
|
56
|
+
onAbort();
|
|
57
|
+
} else {
|
|
58
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const run = async () => {
|
|
62
|
+
while (!stopped) {
|
|
63
|
+
try {
|
|
64
|
+
const response = await watchSession({
|
|
65
|
+
baseUrl: account.config.baseUrl,
|
|
66
|
+
clawToken: account.config.clawToken ?? "",
|
|
67
|
+
sessionId,
|
|
68
|
+
cursor,
|
|
69
|
+
timeoutMs: account.config.watchTimeoutMs,
|
|
70
|
+
limit: account.config.watchLimit,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
cursor = typeof response.cursor === "number" ? response.cursor : cursor;
|
|
75
|
+
consecutiveErrors = 0;
|
|
76
|
+
|
|
77
|
+
for (const event of response.events ?? []) {
|
|
78
|
+
if (event.type !== "message.add") {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
await handleInboundMessage({
|
|
82
|
+
account,
|
|
83
|
+
sessionId,
|
|
84
|
+
event,
|
|
85
|
+
log,
|
|
86
|
+
statusSink,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (controller.signal.aborted) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
log?.error?.(`mochat: watch failed for ${sessionId}: ${message}`);
|
|
95
|
+
statusSink?.({ lastError: message });
|
|
96
|
+
consecutiveErrors += 1;
|
|
97
|
+
const delayMs = computeBackoffMs({
|
|
98
|
+
baseDelayMs: account.config.retryDelayMs,
|
|
99
|
+
consecutiveErrors,
|
|
100
|
+
maxDelayMs: maxBackoffMs,
|
|
101
|
+
});
|
|
102
|
+
await sleep(delayMs, controller.signal);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
void run();
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
stop: () => {
|
|
111
|
+
stopped = true;
|
|
112
|
+
controller.abort();
|
|
113
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setMochatRuntime(next: PluginRuntime) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getMochatRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("Mochat runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
package/src/socket.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import type { ChannelLogSink } from "openclaw/plugin-sdk";
|
|
2
|
+
import { io, type Socket } from "socket.io-client";
|
|
3
|
+
import msgpackParser from "socket.io-msgpack-parser";
|
|
4
|
+
import {
|
|
5
|
+
getWorkspaceGroup,
|
|
6
|
+
listSessions,
|
|
7
|
+
type MochatEvent,
|
|
8
|
+
type MochatWatchResponse,
|
|
9
|
+
} from "./api.js";
|
|
10
|
+
import type { ResolvedMochatAccount } from "./accounts.js";
|
|
11
|
+
import { handleInboundMessage, type MochatStatusSink } from "./inbound.js";
|
|
12
|
+
import { recordPanelEvent } from "./event-store.js";
|
|
13
|
+
|
|
14
|
+
type SubscribeAck = {
|
|
15
|
+
sessions?: MochatWatchResponse[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SocketAck<T> = {
|
|
19
|
+
result: boolean;
|
|
20
|
+
data?: T;
|
|
21
|
+
message?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SessionCursorMap = Record<string, number | undefined>;
|
|
25
|
+
type SessionSet = Set<string>;
|
|
26
|
+
type PanelSet = Set<string>;
|
|
27
|
+
type PanelInfo = {
|
|
28
|
+
id: string;
|
|
29
|
+
type?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const PANEL_TYPE_TEXT = 0;
|
|
33
|
+
|
|
34
|
+
function resolveSocketUrl(account: ResolvedMochatAccount): string {
|
|
35
|
+
const raw = account.config.socketUrl?.trim() || account.config.baseUrl;
|
|
36
|
+
return raw.trim().replace(/\/+$/, "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function collectCursors(cursorBySession: Map<string, number>): SessionCursorMap {
|
|
40
|
+
const snapshot: SessionCursorMap = {};
|
|
41
|
+
for (const [sessionId, cursor] of cursorBySession.entries()) {
|
|
42
|
+
snapshot[sessionId] = cursor;
|
|
43
|
+
}
|
|
44
|
+
return snapshot;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeSessions(data: unknown): MochatWatchResponse[] {
|
|
48
|
+
if (!data) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(data)) {
|
|
52
|
+
return data as MochatWatchResponse[];
|
|
53
|
+
}
|
|
54
|
+
const obj = data as SubscribeAck;
|
|
55
|
+
if (Array.isArray(obj.sessions)) {
|
|
56
|
+
return obj.sessions;
|
|
57
|
+
}
|
|
58
|
+
return [data as MochatWatchResponse];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildCursorFromPayload(
|
|
62
|
+
payload: MochatWatchResponse,
|
|
63
|
+
lastCursor: number,
|
|
64
|
+
): number {
|
|
65
|
+
let nextCursor = lastCursor;
|
|
66
|
+
if (typeof payload.cursor === "number") {
|
|
67
|
+
nextCursor = Math.max(nextCursor, payload.cursor);
|
|
68
|
+
}
|
|
69
|
+
for (const event of payload.events ?? []) {
|
|
70
|
+
if (typeof event.seq === "number") {
|
|
71
|
+
nextCursor = Math.max(nextCursor, event.seq);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return nextCursor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function startMochatSocketClient(params: {
|
|
78
|
+
account: ResolvedMochatAccount;
|
|
79
|
+
log?: ChannelLogSink;
|
|
80
|
+
abortSignal: AbortSignal;
|
|
81
|
+
statusSink?: MochatStatusSink;
|
|
82
|
+
}) {
|
|
83
|
+
const { account, log, abortSignal, statusSink } = params;
|
|
84
|
+
const socketUrl = resolveSocketUrl(account);
|
|
85
|
+
const cursorBySession = new Map<string, number>();
|
|
86
|
+
const queueBySession = new Map<string, Promise<void>>();
|
|
87
|
+
const explicitSessions = account.config.sessions ?? [];
|
|
88
|
+
const explicitPanels = account.config.panels ?? [];
|
|
89
|
+
const autoDiscoverSessions = account.config.autoDiscoverSessions;
|
|
90
|
+
const autoDiscoverPanels = account.config.autoDiscoverPanels;
|
|
91
|
+
const refreshIntervalMs = account.config.refreshIntervalMs;
|
|
92
|
+
const sessionSet: SessionSet = new Set(explicitSessions.map((id) => String(id)));
|
|
93
|
+
const panelSet: PanelSet = new Set(explicitPanels.map((id) => String(id)));
|
|
94
|
+
let refreshTimer: NodeJS.Timeout | null = null;
|
|
95
|
+
let stopped = false;
|
|
96
|
+
|
|
97
|
+
const enqueue = (sessionId: string, task: () => Promise<void>) => {
|
|
98
|
+
const previous = queueBySession.get(sessionId) ?? Promise.resolve();
|
|
99
|
+
const next = previous.then(task, task);
|
|
100
|
+
queueBySession.set(sessionId, next.catch(() => {}));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const applyEvents = (
|
|
104
|
+
payload: MochatWatchResponse,
|
|
105
|
+
targetKind: "session" | "panel" = "session",
|
|
106
|
+
) => {
|
|
107
|
+
const sessionId = payload.sessionId;
|
|
108
|
+
if (!sessionId) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const lastCursor = cursorBySession.get(sessionId) ?? 0;
|
|
112
|
+
const events = (payload.events ?? []).filter(
|
|
113
|
+
(event) => typeof event.seq === "number" && event.seq > lastCursor,
|
|
114
|
+
);
|
|
115
|
+
const nextCursor = buildCursorFromPayload(payload, lastCursor);
|
|
116
|
+
cursorBySession.set(sessionId, nextCursor);
|
|
117
|
+
if (events.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
enqueue(sessionId, async () => {
|
|
122
|
+
for (const event of events) {
|
|
123
|
+
if (event.type !== "message.add") {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await handleInboundMessage({
|
|
128
|
+
account,
|
|
129
|
+
sessionId,
|
|
130
|
+
event: event as MochatEvent,
|
|
131
|
+
targetKind,
|
|
132
|
+
log,
|
|
133
|
+
statusSink,
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
log?.error?.(`mochat: socket event failed for ${sessionId}: ${String(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const subscribeSessions = (socket: Socket, sessionIds: string[]) => {
|
|
143
|
+
if (sessionIds.length === 0) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const cursors = collectCursors(cursorBySession);
|
|
147
|
+
socket.emit(
|
|
148
|
+
"com.claw.im.subscribeSessions",
|
|
149
|
+
{
|
|
150
|
+
sessionIds,
|
|
151
|
+
cursors,
|
|
152
|
+
limit: account.config.watchLimit,
|
|
153
|
+
},
|
|
154
|
+
(ack: SocketAck<SubscribeAck>) => {
|
|
155
|
+
if (!ack?.result) {
|
|
156
|
+
const message = ack?.message ?? "subscribe failed";
|
|
157
|
+
log?.error?.(`mochat: subscribe failed: ${message}`);
|
|
158
|
+
statusSink?.({ lastError: message });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
for (const session of normalizeSessions(ack.data)) {
|
|
162
|
+
applyEvents(session);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const subscribePanels = (socket: Socket, panelIds: string[]) => {
|
|
169
|
+
if (!autoDiscoverPanels && panelIds.length === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
socket.emit(
|
|
173
|
+
"com.claw.im.subscribePanels",
|
|
174
|
+
{
|
|
175
|
+
panelIds,
|
|
176
|
+
},
|
|
177
|
+
(ack: SocketAck<{ panelIds?: string[]; groupId?: string }>) => {
|
|
178
|
+
if (!ack?.result) {
|
|
179
|
+
const message = ack?.message ?? "subscribe panels failed";
|
|
180
|
+
log?.error?.(`mochat: panel subscribe failed: ${message}`);
|
|
181
|
+
statusSink?.({ lastError: message });
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const refreshSessions = async (socket: Socket) => {
|
|
188
|
+
if (!autoDiscoverSessions) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const response = await listSessions({
|
|
193
|
+
baseUrl: account.config.baseUrl,
|
|
194
|
+
clawToken: account.config.clawToken ?? "",
|
|
195
|
+
});
|
|
196
|
+
const discovered = (response.sessions ?? [])
|
|
197
|
+
.map((session) => session.sessionId)
|
|
198
|
+
.filter(Boolean)
|
|
199
|
+
.map((id) => String(id));
|
|
200
|
+
const newSessions: string[] = [];
|
|
201
|
+
for (const sessionId of discovered) {
|
|
202
|
+
if (!sessionSet.has(sessionId)) {
|
|
203
|
+
sessionSet.add(sessionId);
|
|
204
|
+
newSessions.push(sessionId);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
subscribeSessions(socket, newSessions);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
log?.error?.(`mochat: session refresh failed: ${String(err)}`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const resolveTextPanels = (panels?: PanelInfo[]) => {
|
|
214
|
+
if (!Array.isArray(panels)) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
return panels
|
|
218
|
+
.filter((panel) => {
|
|
219
|
+
if (!panel) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (typeof panel.type === "number" && panel.type !== PANEL_TYPE_TEXT) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return Boolean(panel.id);
|
|
226
|
+
})
|
|
227
|
+
.map((panel) => String(panel.id));
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const refreshPanels = async (socket: Socket) => {
|
|
231
|
+
if (!autoDiscoverPanels) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const groupInfo = await getWorkspaceGroup({
|
|
236
|
+
baseUrl: account.config.baseUrl,
|
|
237
|
+
clawToken: account.config.clawToken ?? "",
|
|
238
|
+
});
|
|
239
|
+
const rawPanels = Array.isArray(groupInfo.panels) ? groupInfo.panels : [];
|
|
240
|
+
const panels = resolveTextPanels(
|
|
241
|
+
rawPanels.map((panel) => ({
|
|
242
|
+
id: String((panel as any)?.id ?? (panel as any)?._id ?? ""),
|
|
243
|
+
type: (panel as any)?.type,
|
|
244
|
+
})),
|
|
245
|
+
);
|
|
246
|
+
const newPanels: string[] = [];
|
|
247
|
+
for (const panelId of panels) {
|
|
248
|
+
if (!panelSet.has(panelId)) {
|
|
249
|
+
panelSet.add(panelId);
|
|
250
|
+
newPanels.push(panelId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
subscribePanels(socket, newPanels);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
log?.error?.(`mochat: panel refresh failed: ${String(err)}`);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const refreshTargets = async (socket: Socket) => {
|
|
260
|
+
await Promise.all([refreshSessions(socket), refreshPanels(socket)]);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const subscribe = (socket: Socket) => {
|
|
264
|
+
subscribeSessions(socket, Array.from(sessionSet));
|
|
265
|
+
subscribePanels(socket, Array.from(panelSet));
|
|
266
|
+
if (autoDiscoverSessions || autoDiscoverPanels) {
|
|
267
|
+
void refreshTargets(socket);
|
|
268
|
+
if (refreshTimer) {
|
|
269
|
+
clearInterval(refreshTimer);
|
|
270
|
+
}
|
|
271
|
+
refreshTimer = setInterval(() => {
|
|
272
|
+
if (stopped) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
void refreshTargets(socket);
|
|
276
|
+
}, refreshIntervalMs);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const socket = io(socketUrl, {
|
|
281
|
+
path: account.config.socketPath,
|
|
282
|
+
transports: ["websocket"],
|
|
283
|
+
parser: account.config.socketDisableMsgpack ? undefined : msgpackParser,
|
|
284
|
+
auth: {
|
|
285
|
+
token: account.config.clawToken ?? "",
|
|
286
|
+
},
|
|
287
|
+
reconnection: true,
|
|
288
|
+
reconnectionAttempts:
|
|
289
|
+
account.config.maxRetryAttempts > 0 ? account.config.maxRetryAttempts : undefined,
|
|
290
|
+
reconnectionDelay: account.config.socketReconnectDelayMs,
|
|
291
|
+
reconnectionDelayMax: account.config.socketMaxReconnectDelayMs,
|
|
292
|
+
timeout: account.config.socketConnectTimeoutMs,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
socket.on("connect", () => {
|
|
296
|
+
if (stopped) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
statusSink?.({ lastError: null });
|
|
300
|
+
subscribe(socket);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
socket.on("connect_error", (err) => {
|
|
304
|
+
if (stopped) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
308
|
+
log?.error?.(`mochat: socket connect failed: ${message}`);
|
|
309
|
+
statusSink?.({ lastError: message });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
socket.on("disconnect", (reason) => {
|
|
313
|
+
if (stopped) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
log?.info?.(`mochat: socket disconnected (${reason})`);
|
|
317
|
+
if (reason !== "io client disconnect") {
|
|
318
|
+
statusSink?.({ lastError: reason });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
socket.on("claw.session.events", (payload: MochatWatchResponse) => {
|
|
323
|
+
if (stopped) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
log?.info?.(
|
|
327
|
+
`mochat: recv claw.session.events session=${payload?.sessionId ?? "unknown"}`,
|
|
328
|
+
);
|
|
329
|
+
applyEvents(payload);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
socket.on("claw.panel.events", (payload: MochatWatchResponse) => {
|
|
333
|
+
if (stopped) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
log?.info?.(
|
|
337
|
+
`mochat: recv claw.panel.events panel=${payload?.sessionId ?? "unknown"}`,
|
|
338
|
+
);
|
|
339
|
+
applyEvents(payload, "panel");
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
socket.onAny((eventName, payload) => {
|
|
343
|
+
if (stopped) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (typeof eventName !== "string" || !eventName.startsWith("notify:")) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (eventName.startsWith("notify:chat.message.")) {
|
|
351
|
+
if (!payload || typeof payload !== "object") {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const groupId = (payload as any).groupId ? String((payload as any).groupId) : "";
|
|
355
|
+
if (!groupId) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const panelId = (payload as any).converseId
|
|
359
|
+
? String((payload as any).converseId)
|
|
360
|
+
: "";
|
|
361
|
+
if (!panelId) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (panelSet.size > 0 && !panelSet.has(panelId)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const event: MochatEvent = {
|
|
369
|
+
seq: 0,
|
|
370
|
+
sessionId: panelId,
|
|
371
|
+
type: "message.add",
|
|
372
|
+
timestamp:
|
|
373
|
+
typeof (payload as any).createdAt === "string"
|
|
374
|
+
? (payload as any).createdAt
|
|
375
|
+
: new Date().toISOString(),
|
|
376
|
+
payload: {
|
|
377
|
+
messageId: String((payload as any)._id ?? (payload as any).messageId ?? ""),
|
|
378
|
+
author: (payload as any).author ? String((payload as any).author) : "",
|
|
379
|
+
authorInfo: (payload as any).authorInfo ?? undefined,
|
|
380
|
+
content: (payload as any).content,
|
|
381
|
+
meta: (payload as any).meta ?? {},
|
|
382
|
+
groupId,
|
|
383
|
+
converseId: panelId,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
enqueue(panelId, async () => {
|
|
388
|
+
try {
|
|
389
|
+
await handleInboundMessage({
|
|
390
|
+
account,
|
|
391
|
+
sessionId: panelId,
|
|
392
|
+
event,
|
|
393
|
+
targetKind: "panel",
|
|
394
|
+
log,
|
|
395
|
+
statusSink,
|
|
396
|
+
});
|
|
397
|
+
} catch (err) {
|
|
398
|
+
log?.error?.(`mochat: panel message failed for ${panelId}: ${String(err)}`);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
void recordPanelEvent({
|
|
405
|
+
accountId: account.accountId,
|
|
406
|
+
eventName,
|
|
407
|
+
payload,
|
|
408
|
+
}).catch((err) => {
|
|
409
|
+
log?.error?.(`mochat: failed to persist panel event: ${String(err)}`);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const onAbort = () => {
|
|
414
|
+
stopped = true;
|
|
415
|
+
if (refreshTimer) {
|
|
416
|
+
clearInterval(refreshTimer);
|
|
417
|
+
refreshTimer = null;
|
|
418
|
+
}
|
|
419
|
+
socket.disconnect();
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (abortSignal.aborted) {
|
|
423
|
+
onAbort();
|
|
424
|
+
} else {
|
|
425
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
stop: () => {
|
|
430
|
+
stopped = true;
|
|
431
|
+
if (refreshTimer) {
|
|
432
|
+
clearInterval(refreshTimer);
|
|
433
|
+
refreshTimer = null;
|
|
434
|
+
}
|
|
435
|
+
socket.disconnect();
|
|
436
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
}
|