@ascegu/teamily 1.0.8 → 1.0.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/package.json +2 -1
- package/src/monitor.ts +128 -300
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascegu/teamily",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"channel",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"type": "module",
|
|
30
30
|
"main": "index.ts",
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@openim/client-sdk": "^3.8.3",
|
|
32
33
|
"zod": "^4.3.6"
|
|
33
34
|
},
|
|
34
35
|
"openclaw": {
|
package/src/monitor.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { generateOperationID } from "./probe.js";
|
|
2
1
|
import type {
|
|
3
2
|
ResolvedTeamilyAccount,
|
|
4
3
|
TeamilyMessage,
|
|
@@ -8,11 +7,6 @@ import type {
|
|
|
8
7
|
} from "./types.js";
|
|
9
8
|
import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
|
|
10
9
|
|
|
11
|
-
const WS_REQ = {
|
|
12
|
-
LOGIN: 1001,
|
|
13
|
-
HEARTBEAT: 1002,
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
10
|
export type TeamilyMessageHandler = (message: TeamilyMessage) => Promise<void> | void;
|
|
17
11
|
export type TeamilyConnectionState = "connecting" | "connected" | "disconnected" | "error";
|
|
18
12
|
|
|
@@ -20,361 +14,198 @@ export interface TeamilyMonitorOptions {
|
|
|
20
14
|
account: ResolvedTeamilyAccount;
|
|
21
15
|
onMessage: TeamilyMessageHandler;
|
|
22
16
|
onStateChange?: (state: TeamilyConnectionState, error?: string) => void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SdkModule = typeof import("@openim/client-sdk");
|
|
20
|
+
type SdkInstance = ReturnType<SdkModule["getSDK"]>;
|
|
21
|
+
|
|
22
|
+
// Lazy-loaded SDK to avoid top-level dynamic import issues
|
|
23
|
+
let sdkModule: SdkModule | null = null;
|
|
24
|
+
async function loadSDK() {
|
|
25
|
+
if (!sdkModule) {
|
|
26
|
+
sdkModule = await import("@openim/client-sdk");
|
|
27
|
+
}
|
|
28
|
+
return sdkModule;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
29
|
-
* Monitor for incoming Teamily messages
|
|
32
|
+
* Monitor for incoming Teamily messages using @openim/client-sdk.
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Delegates WebSocket connection, authentication, heartbeat, and
|
|
35
|
+
* reconnection to the official OpenIM SDK.
|
|
33
36
|
*/
|
|
34
37
|
export class TeamilyMonitor {
|
|
35
38
|
private account: ResolvedTeamilyAccount;
|
|
36
39
|
private onMessage: TeamilyMessageHandler;
|
|
37
40
|
private onStateChange?: (state: TeamilyConnectionState, error?: string) => void;
|
|
38
|
-
private
|
|
39
|
-
private pingInterval: number;
|
|
40
|
-
private ws: WebSocket | null = null;
|
|
41
|
-
private pingTimer: NodeJS.Timeout | null = null;
|
|
42
|
-
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
41
|
+
private sdk: SdkInstance | null = null;
|
|
43
42
|
private state: TeamilyConnectionState = "disconnected";
|
|
44
|
-
private
|
|
45
|
-
private authenticated = false;
|
|
46
|
-
private wsImpl: typeof WebSocket;
|
|
43
|
+
private stopped = false;
|
|
47
44
|
|
|
48
45
|
constructor(options: TeamilyMonitorOptions) {
|
|
49
46
|
this.account = options.account;
|
|
50
47
|
this.onMessage = options.onMessage;
|
|
51
48
|
this.onStateChange = options.onStateChange;
|
|
52
|
-
this.reconnectInterval = options.reconnectInterval ?? 5000;
|
|
53
|
-
this.pingInterval = options.pingInterval ?? 30000;
|
|
54
|
-
this.wsImpl = options.websocketImpl ?? WebSocket;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
/**
|
|
58
|
-
* Start monitoring for messages.
|
|
59
|
-
*/
|
|
60
51
|
async start(): Promise<void> {
|
|
61
|
-
this.
|
|
62
|
-
await this.connect();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Stop monitoring and close the connection.
|
|
67
|
-
*/
|
|
68
|
-
stop(): void {
|
|
69
|
-
this.shouldReconnect = false;
|
|
70
|
-
this.authenticated = false;
|
|
71
|
-
|
|
72
|
-
if (this.reconnectTimer) {
|
|
73
|
-
clearTimeout(this.reconnectTimer);
|
|
74
|
-
this.reconnectTimer = null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (this.pingTimer) {
|
|
78
|
-
clearInterval(this.pingTimer);
|
|
79
|
-
this.pingTimer = null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (this.ws) {
|
|
83
|
-
const ws = this.ws;
|
|
84
|
-
ws.onopen = null;
|
|
85
|
-
ws.onmessage = null;
|
|
86
|
-
ws.onerror = null;
|
|
87
|
-
ws.onclose = null;
|
|
88
|
-
this.ws = null;
|
|
89
|
-
try {
|
|
90
|
-
ws.close(1000, "Monitoring stopped");
|
|
91
|
-
} catch {
|
|
92
|
-
// Ignore – socket may already be closed.
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
this.setState("disconnected");
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Connect to the Teamily WebSocket server.
|
|
101
|
-
*/
|
|
102
|
-
private async connect(): Promise<void> {
|
|
103
|
-
if (!this.shouldReconnect) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
52
|
+
this.stopped = false;
|
|
107
53
|
this.setState("connecting");
|
|
108
54
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
try {
|
|
143
|
-
this.ws.send(
|
|
144
|
-
JSON.stringify({
|
|
145
|
-
reqIdentifier: WS_REQ.LOGIN,
|
|
146
|
-
operationID: generateOperationID(),
|
|
147
|
-
sendID: this.account.userID,
|
|
148
|
-
token: this.account.token,
|
|
149
|
-
platformID: 5,
|
|
150
|
-
}),
|
|
151
|
-
);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
this.handleError(error);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Handle incoming WebSocket message.
|
|
159
|
-
*/
|
|
160
|
-
private async handleMessage(event: MessageEvent): Promise<void> {
|
|
161
|
-
try {
|
|
162
|
-
const data = JSON.parse(event.data) as {
|
|
163
|
-
reqIdentifier?: number;
|
|
164
|
-
errCode?: number;
|
|
165
|
-
errMsg?: string;
|
|
166
|
-
msgID?: string;
|
|
167
|
-
sendID?: string;
|
|
168
|
-
msgFrom?: string;
|
|
169
|
-
recvID?: string;
|
|
170
|
-
contentType?: number;
|
|
171
|
-
content?: unknown;
|
|
172
|
-
sessionType?: number;
|
|
173
|
-
sendTime?: number;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// Handle login response
|
|
177
|
-
if (data.reqIdentifier === WS_REQ.LOGIN) {
|
|
178
|
-
if (data.errCode === 0) {
|
|
179
|
-
this.authenticated = true;
|
|
180
|
-
this.setState("connected");
|
|
181
|
-
this.startPing();
|
|
182
|
-
} else {
|
|
183
|
-
this.setState("error", data.errMsg || "Authentication failed");
|
|
184
|
-
this.ws?.close();
|
|
55
|
+
const { getSDK, CbEvents } = await loadSDK();
|
|
56
|
+
const sdk = getSDK();
|
|
57
|
+
this.sdk = sdk;
|
|
58
|
+
|
|
59
|
+
// Connection events
|
|
60
|
+
sdk.on(CbEvents.OnConnecting, () => {
|
|
61
|
+
if (!this.stopped) this.setState("connecting");
|
|
62
|
+
});
|
|
63
|
+
sdk.on(CbEvents.OnConnectSuccess, () => {
|
|
64
|
+
if (!this.stopped) this.setState("connected");
|
|
65
|
+
});
|
|
66
|
+
sdk.on(CbEvents.OnConnectFailed, ({ errCode, errMsg }) => {
|
|
67
|
+
if (!this.stopped) this.setState("error", `[${errCode}] ${errMsg}`);
|
|
68
|
+
});
|
|
69
|
+
sdk.on(CbEvents.OnKickedOffline, () => {
|
|
70
|
+
if (!this.stopped) this.setState("error", "Kicked offline");
|
|
71
|
+
});
|
|
72
|
+
sdk.on(CbEvents.OnUserTokenExpired, () => {
|
|
73
|
+
if (!this.stopped) this.setState("error", "Token expired");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Incoming messages
|
|
77
|
+
sdk.on(CbEvents.OnRecvNewMessages, ({ data }) => {
|
|
78
|
+
if (this.stopped || !data) return;
|
|
79
|
+
for (const msg of data) {
|
|
80
|
+
// Skip self-sent messages
|
|
81
|
+
if (msg.sendID === this.account.userID) continue;
|
|
82
|
+
const converted = convertSdkMessage(msg, this.account.userID);
|
|
83
|
+
if (converted) {
|
|
84
|
+
// Fire-and-forget; errors are logged by the gateway dispatcher
|
|
85
|
+
void this.onMessage(converted);
|
|
185
86
|
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Ignore heartbeat responses
|
|
190
|
-
if (data.reqIdentifier === WS_REQ.HEARTBEAT) {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (!this.authenticated) {
|
|
195
|
-
return;
|
|
196
87
|
}
|
|
88
|
+
});
|
|
197
89
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await this.onMessage(message);
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error("Failed to parse Teamily message:", error);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Handle WebSocket error.
|
|
218
|
-
* Detaches event handlers before closing to prevent recursive calls
|
|
219
|
-
* (ws.close() on an errored socket can re-fire onerror → stack overflow).
|
|
220
|
-
*/
|
|
221
|
-
private handleError(error: unknown): void {
|
|
222
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
223
|
-
this.setState("error", errorMessage);
|
|
224
|
-
|
|
225
|
-
// Detach handlers and grab ref before nulling, so close() cannot recurse.
|
|
226
|
-
const ws = this.ws;
|
|
227
|
-
if (ws) {
|
|
228
|
-
ws.onopen = null;
|
|
229
|
-
ws.onmessage = null;
|
|
230
|
-
ws.onerror = null;
|
|
231
|
-
ws.onclose = null;
|
|
232
|
-
this.ws = null;
|
|
233
|
-
try {
|
|
234
|
-
ws.close();
|
|
235
|
-
} catch {
|
|
236
|
-
// Ignore – socket may already be closed/invalid.
|
|
237
|
-
}
|
|
238
|
-
// onclose was detached, so manually trigger reconnect logic.
|
|
239
|
-
this.handleClose();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Handle WebSocket connection closed.
|
|
245
|
-
*/
|
|
246
|
-
private handleClose(): void {
|
|
247
|
-
this.stopPing();
|
|
248
|
-
this.authenticated = false;
|
|
249
|
-
|
|
250
|
-
if (this.state === "disconnected" || !this.shouldReconnect) {
|
|
251
|
-
return;
|
|
90
|
+
try {
|
|
91
|
+
await sdk.login({
|
|
92
|
+
userID: this.account.userID,
|
|
93
|
+
token: this.account.token,
|
|
94
|
+
platformID: 5,
|
|
95
|
+
wsAddr: this.account.wsURL,
|
|
96
|
+
apiAddr: this.account.apiURL,
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
+
this.setState("error", msg);
|
|
101
|
+
throw err;
|
|
252
102
|
}
|
|
253
|
-
|
|
254
|
-
// Schedule reconnection
|
|
255
|
-
this.reconnectTimer = setTimeout(() => {
|
|
256
|
-
if (this.shouldReconnect) {
|
|
257
|
-
this.connect();
|
|
258
|
-
}
|
|
259
|
-
}, this.reconnectInterval);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Start heartbeat ping interval.
|
|
264
|
-
*/
|
|
265
|
-
private startPing(): void {
|
|
266
|
-
this.stopPing();
|
|
267
|
-
|
|
268
|
-
this.pingTimer = setInterval(() => {
|
|
269
|
-
this.sendPing();
|
|
270
|
-
}, this.pingInterval);
|
|
271
103
|
}
|
|
272
104
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
clearInterval(this.pingTimer);
|
|
279
|
-
this.pingTimer = null;
|
|
105
|
+
stop(): void {
|
|
106
|
+
this.stopped = true;
|
|
107
|
+
if (this.sdk) {
|
|
108
|
+
this.sdk.logout().catch(() => {});
|
|
109
|
+
this.sdk = null;
|
|
280
110
|
}
|
|
111
|
+
this.setState("disconnected");
|
|
281
112
|
}
|
|
282
113
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
*/
|
|
286
|
-
private sendPing(): void {
|
|
287
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
288
|
-
try {
|
|
289
|
-
this.ws.send(
|
|
290
|
-
JSON.stringify({
|
|
291
|
-
reqIdentifier: WS_REQ.HEARTBEAT,
|
|
292
|
-
operationID: generateOperationID(),
|
|
293
|
-
sendID: this.account.userID,
|
|
294
|
-
sendTime: Date.now(),
|
|
295
|
-
}),
|
|
296
|
-
);
|
|
297
|
-
} catch (error) {
|
|
298
|
-
console.error("Teamily ping failed:", error);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
114
|
+
getState(): TeamilyConnectionState {
|
|
115
|
+
return this.state;
|
|
301
116
|
}
|
|
302
117
|
|
|
303
|
-
/**
|
|
304
|
-
* Update and notify connection state.
|
|
305
|
-
*/
|
|
306
118
|
private setState(state: TeamilyConnectionState, error?: string): void {
|
|
307
119
|
this.state = state;
|
|
308
120
|
this.onStateChange?.(state, error);
|
|
309
121
|
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Get current connection state.
|
|
313
|
-
*/
|
|
314
|
-
getState(): TeamilyConnectionState {
|
|
315
|
-
return this.state;
|
|
316
|
-
}
|
|
317
122
|
}
|
|
318
123
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
124
|
+
// ---- SDK message conversion helpers ----
|
|
125
|
+
|
|
126
|
+
import type { MessageItem } from "@openim/client-sdk";
|
|
127
|
+
|
|
128
|
+
function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage | null {
|
|
129
|
+
const contentType = msg.contentType ?? CONTENT_TYPES.TEXT;
|
|
130
|
+
const sessionType = msg.sessionType ?? SESSION_TYPES.SINGLE;
|
|
131
|
+
|
|
132
|
+
const content = parseSdkContent(msg, contentType);
|
|
133
|
+
|
|
134
|
+
// Skip messages with no usable content
|
|
135
|
+
if (!content.text && !content.picture && !content.video && !content.audio) {
|
|
136
|
+
return null;
|
|
326
137
|
}
|
|
327
138
|
|
|
328
|
-
|
|
139
|
+
return {
|
|
140
|
+
serverMsgID: msg.serverMsgID || msg.clientMsgID || `${Date.now()}_${Math.random()}`,
|
|
141
|
+
sendID: msg.sendID || "unknown",
|
|
142
|
+
recvID: sessionType === SESSION_TYPES.GROUP ? (msg.groupID || "") : (msg.recvID || selfUserID),
|
|
143
|
+
content,
|
|
144
|
+
contentType,
|
|
145
|
+
sessionType,
|
|
146
|
+
sendTime: msg.sendTime || Date.now(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
329
149
|
|
|
150
|
+
function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage["content"] {
|
|
330
151
|
switch (contentType) {
|
|
331
|
-
case CONTENT_TYPES.TEXT:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
};
|
|
152
|
+
case CONTENT_TYPES.TEXT: {
|
|
153
|
+
// SDK puts text in textElem.content; fallback to raw content string
|
|
154
|
+
const text = msg.textElem?.content ?? tryParseTextContent(msg.content);
|
|
155
|
+
return text ? { text } : {};
|
|
156
|
+
}
|
|
335
157
|
case CONTENT_TYPES.PICTURE:
|
|
336
|
-
|
|
158
|
+
if (msg.pictureElem?.sourcePicture) {
|
|
159
|
+
return { picture: msg.pictureElem as unknown as TeamilyPictureContent };
|
|
160
|
+
}
|
|
161
|
+
return {};
|
|
337
162
|
case CONTENT_TYPES.VIDEO:
|
|
338
|
-
|
|
163
|
+
if (msg.videoElem?.videoUrl) {
|
|
164
|
+
return { video: msg.videoElem as unknown as TeamilyVideoContent };
|
|
165
|
+
}
|
|
166
|
+
return {};
|
|
339
167
|
case CONTENT_TYPES.VOICE:
|
|
340
|
-
|
|
168
|
+
if (msg.soundElem?.sourceUrl) {
|
|
169
|
+
return { audio: msg.soundElem as unknown as TeamilyAudioContent };
|
|
170
|
+
}
|
|
171
|
+
return {};
|
|
341
172
|
default:
|
|
342
173
|
return {};
|
|
343
174
|
}
|
|
344
175
|
}
|
|
345
176
|
|
|
346
|
-
/**
|
|
347
|
-
|
|
348
|
-
|
|
177
|
+
/** Try to extract text from the raw JSON content string (OpenIM text format: `{"content":"..."}`) */
|
|
178
|
+
function tryParseTextContent(raw: string | undefined): string | undefined {
|
|
179
|
+
if (!raw) return undefined;
|
|
180
|
+
try {
|
|
181
|
+
const obj = JSON.parse(raw) as { content?: string };
|
|
182
|
+
return typeof obj.content === "string" ? obj.content : undefined;
|
|
183
|
+
} catch {
|
|
184
|
+
return raw;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---- Global monitor registry ----
|
|
189
|
+
|
|
349
190
|
const monitors = new Map<string, TeamilyMonitor>();
|
|
350
191
|
|
|
351
|
-
/**
|
|
352
|
-
* Start monitoring for a Teamily account.
|
|
353
|
-
*/
|
|
354
192
|
export function startTeamilyMonitoring(
|
|
355
193
|
account: ResolvedTeamilyAccount,
|
|
356
194
|
onMessage: TeamilyMessageHandler,
|
|
357
195
|
onStateChange?: (state: TeamilyConnectionState, error?: string) => void,
|
|
358
196
|
): () => void {
|
|
359
|
-
const monitor = new TeamilyMonitor({
|
|
360
|
-
account,
|
|
361
|
-
onMessage,
|
|
362
|
-
onStateChange,
|
|
363
|
-
});
|
|
364
|
-
|
|
197
|
+
const monitor = new TeamilyMonitor({ account, onMessage, onStateChange });
|
|
365
198
|
monitors.set(account.accountId, monitor);
|
|
366
|
-
monitor.start()
|
|
199
|
+
monitor.start().catch((err) => {
|
|
200
|
+
console.error(`Teamily monitor start failed for ${account.accountId}:`, err);
|
|
201
|
+
});
|
|
367
202
|
|
|
368
|
-
// Return cleanup function
|
|
369
203
|
return () => {
|
|
370
204
|
monitor.stop();
|
|
371
205
|
monitors.delete(account.accountId);
|
|
372
206
|
};
|
|
373
207
|
}
|
|
374
208
|
|
|
375
|
-
/**
|
|
376
|
-
* Stop monitoring for a Teamily account.
|
|
377
|
-
*/
|
|
378
209
|
export function stopTeamilyMonitoring(accountId: string): void {
|
|
379
210
|
const monitor = monitors.get(accountId);
|
|
380
211
|
if (monitor) {
|
|
@@ -383,9 +214,6 @@ export function stopTeamilyMonitoring(accountId: string): void {
|
|
|
383
214
|
}
|
|
384
215
|
}
|
|
385
216
|
|
|
386
|
-
/**
|
|
387
|
-
* Get monitor for an account.
|
|
388
|
-
*/
|
|
389
217
|
export function getTeamilyMonitor(accountId: string): TeamilyMonitor | undefined {
|
|
390
218
|
return monitors.get(accountId);
|
|
391
219
|
}
|