@ascegu/teamily 1.0.3 → 1.0.5
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/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/src/accounts.ts +51 -0
- package/src/channel.ts +411 -0
- package/src/config-schema.ts +67 -0
- package/src/monitor.ts +370 -0
- package/src/normalize.ts +90 -0
- package/src/probe.ts +86 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +273 -0
- package/src/types.ts +116 -0
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ResolvedTeamilyAccount,
|
|
3
|
+
TeamilyMessage,
|
|
4
|
+
TeamilyPictureContent,
|
|
5
|
+
TeamilyVideoContent,
|
|
6
|
+
TeamilyAudioContent,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
|
|
9
|
+
import { generateOperationID } from "./probe.js";
|
|
10
|
+
|
|
11
|
+
const WS_REQ = {
|
|
12
|
+
LOGIN: 1001,
|
|
13
|
+
HEARTBEAT: 1002,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type TeamilyMessageHandler = (message: TeamilyMessage) => Promise<void> | void;
|
|
17
|
+
export type TeamilyConnectionState = "connecting" | "connected" | "disconnected" | "error";
|
|
18
|
+
|
|
19
|
+
export interface TeamilyMonitorOptions {
|
|
20
|
+
account: ResolvedTeamilyAccount;
|
|
21
|
+
onMessage: TeamilyMessageHandler;
|
|
22
|
+
onStateChange?: (state: TeamilyConnectionState, error?: string) => void;
|
|
23
|
+
reconnectInterval?: number;
|
|
24
|
+
pingInterval?: number;
|
|
25
|
+
websocketImpl?: typeof WebSocket;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Monitor for incoming Teamily messages via WebSocket.
|
|
30
|
+
*
|
|
31
|
+
* This class manages a WebSocket connection to the Teamily server
|
|
32
|
+
* and handles incoming messages, reconnections, and heartbeat pings.
|
|
33
|
+
*/
|
|
34
|
+
export class TeamilyMonitor {
|
|
35
|
+
private account: ResolvedTeamilyAccount;
|
|
36
|
+
private onMessage: TeamilyMessageHandler;
|
|
37
|
+
private onStateChange?: (state: TeamilyConnectionState, error?: string) => void;
|
|
38
|
+
private reconnectInterval: number;
|
|
39
|
+
private pingInterval: number;
|
|
40
|
+
private ws: WebSocket | null = null;
|
|
41
|
+
private pingTimer: NodeJS.Timeout | null = null;
|
|
42
|
+
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
43
|
+
private state: TeamilyConnectionState = "disconnected";
|
|
44
|
+
private shouldReconnect = true;
|
|
45
|
+
private authenticated = false;
|
|
46
|
+
private wsImpl: typeof WebSocket;
|
|
47
|
+
|
|
48
|
+
constructor(options: TeamilyMonitorOptions) {
|
|
49
|
+
this.account = options.account;
|
|
50
|
+
this.onMessage = options.onMessage;
|
|
51
|
+
this.onStateChange = options.onStateChange;
|
|
52
|
+
this.reconnectInterval = options.reconnectInterval ?? 5000;
|
|
53
|
+
this.pingInterval = options.pingInterval ?? 30000;
|
|
54
|
+
this.wsImpl = options.websocketImpl ?? WebSocket;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Start monitoring for messages.
|
|
59
|
+
*/
|
|
60
|
+
async start(): Promise<void> {
|
|
61
|
+
this.shouldReconnect = true;
|
|
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
|
+
this.ws.close(1000, "Monitoring stopped");
|
|
84
|
+
this.ws = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.setState("disconnected");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Connect to the Teamily WebSocket server.
|
|
92
|
+
*/
|
|
93
|
+
private async connect(): Promise<void> {
|
|
94
|
+
if (!this.shouldReconnect) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.setState("connecting");
|
|
99
|
+
|
|
100
|
+
const wsUrl = new URL(this.account.wsURL);
|
|
101
|
+
wsUrl.searchParams.set("token", this.account.token);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const ws = new this.wsImpl(wsUrl.toString()) as WebSocket & { isMock?: boolean };
|
|
105
|
+
this.ws = ws;
|
|
106
|
+
|
|
107
|
+
ws.onopen = () => this.handleOpen();
|
|
108
|
+
ws.onmessage = (event) => this.handleMessage(event);
|
|
109
|
+
ws.onerror = (error) => this.handleError(error);
|
|
110
|
+
ws.onclose = () => this.handleClose();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
this.handleError(error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle WebSocket connection opened.
|
|
118
|
+
*/
|
|
119
|
+
private handleOpen(): void {
|
|
120
|
+
this.sendAuth();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send authentication message after WebSocket connection opens.
|
|
125
|
+
*/
|
|
126
|
+
private sendAuth(): void {
|
|
127
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
this.ws.send(JSON.stringify({
|
|
132
|
+
reqIdentifier: WS_REQ.LOGIN,
|
|
133
|
+
operationID: generateOperationID(),
|
|
134
|
+
sendID: this.account.userID,
|
|
135
|
+
token: this.account.token,
|
|
136
|
+
platformID: 5,
|
|
137
|
+
}));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.handleError(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Handle incoming WebSocket message.
|
|
145
|
+
*/
|
|
146
|
+
private async handleMessage(event: MessageEvent): Promise<void> {
|
|
147
|
+
try {
|
|
148
|
+
const data = JSON.parse(event.data) as {
|
|
149
|
+
reqIdentifier?: number;
|
|
150
|
+
errCode?: number;
|
|
151
|
+
errMsg?: string;
|
|
152
|
+
msgID?: string;
|
|
153
|
+
sendID?: string;
|
|
154
|
+
msgFrom?: string;
|
|
155
|
+
recvID?: string;
|
|
156
|
+
contentType?: number;
|
|
157
|
+
content?: unknown;
|
|
158
|
+
sessionType?: number;
|
|
159
|
+
sendTime?: number;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Handle login response
|
|
163
|
+
if (data.reqIdentifier === WS_REQ.LOGIN) {
|
|
164
|
+
if (data.errCode === 0) {
|
|
165
|
+
this.authenticated = true;
|
|
166
|
+
this.setState("connected");
|
|
167
|
+
this.startPing();
|
|
168
|
+
} else {
|
|
169
|
+
this.setState("error", data.errMsg || "Authentication failed");
|
|
170
|
+
this.ws?.close();
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Ignore heartbeat responses
|
|
176
|
+
if (data.reqIdentifier === WS_REQ.HEARTBEAT) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!this.authenticated) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const contentType = data.contentType || CONTENT_TYPES.TEXT;
|
|
185
|
+
|
|
186
|
+
const message: TeamilyMessage = {
|
|
187
|
+
serverMsgID: data.msgID || `${Date.now()}_${Math.random()}`,
|
|
188
|
+
sendID: data.sendID || data.msgFrom || "unknown",
|
|
189
|
+
recvID: data.recvID || this.account.userID,
|
|
190
|
+
content: parseMessageContent(data.content, contentType),
|
|
191
|
+
contentType,
|
|
192
|
+
sessionType: data.sessionType || SESSION_TYPES.SINGLE,
|
|
193
|
+
sendTime: data.sendTime || Date.now(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await this.onMessage(message);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error("Failed to parse Teamily message:", error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handle WebSocket error.
|
|
204
|
+
*/
|
|
205
|
+
private handleError(error: unknown): void {
|
|
206
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
207
|
+
this.setState("error", errorMessage);
|
|
208
|
+
|
|
209
|
+
// Close the connection so it can be re-established
|
|
210
|
+
if (this.ws) {
|
|
211
|
+
this.ws.close();
|
|
212
|
+
this.ws = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Handle WebSocket connection closed.
|
|
218
|
+
*/
|
|
219
|
+
private handleClose(): void {
|
|
220
|
+
this.stopPing();
|
|
221
|
+
this.authenticated = false;
|
|
222
|
+
|
|
223
|
+
if (this.state === "disconnected" || !this.shouldReconnect) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Schedule reconnection
|
|
228
|
+
this.reconnectTimer = setTimeout(() => {
|
|
229
|
+
if (this.shouldReconnect) {
|
|
230
|
+
this.connect();
|
|
231
|
+
}
|
|
232
|
+
}, this.reconnectInterval);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Start heartbeat ping interval.
|
|
237
|
+
*/
|
|
238
|
+
private startPing(): void {
|
|
239
|
+
this.stopPing();
|
|
240
|
+
|
|
241
|
+
this.pingTimer = setInterval(() => {
|
|
242
|
+
this.sendPing();
|
|
243
|
+
}, this.pingInterval);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Stop heartbeat ping.
|
|
248
|
+
*/
|
|
249
|
+
private stopPing(): void {
|
|
250
|
+
if (this.pingTimer) {
|
|
251
|
+
clearInterval(this.pingTimer);
|
|
252
|
+
this.pingTimer = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Send ping to keep connection alive.
|
|
258
|
+
*/
|
|
259
|
+
private sendPing(): void {
|
|
260
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
261
|
+
try {
|
|
262
|
+
this.ws.send(JSON.stringify({
|
|
263
|
+
reqIdentifier: WS_REQ.HEARTBEAT,
|
|
264
|
+
operationID: generateOperationID(),
|
|
265
|
+
sendID: this.account.userID,
|
|
266
|
+
sendTime: Date.now(),
|
|
267
|
+
}));
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error("Teamily ping failed:", error);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update and notify connection state.
|
|
276
|
+
*/
|
|
277
|
+
private setState(state: TeamilyConnectionState, error?: string): void {
|
|
278
|
+
this.state = state;
|
|
279
|
+
this.onStateChange?.(state, error);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get current connection state.
|
|
284
|
+
*/
|
|
285
|
+
getState(): TeamilyConnectionState {
|
|
286
|
+
return this.state;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse raw OpenIM message content into normalized internal format.
|
|
292
|
+
* OpenIM text messages use `{ content: "text" }`, not `{ text: "..." }`.
|
|
293
|
+
*/
|
|
294
|
+
function parseMessageContent(
|
|
295
|
+
raw: unknown,
|
|
296
|
+
contentType: number,
|
|
297
|
+
): TeamilyMessage["content"] {
|
|
298
|
+
if (!raw) {
|
|
299
|
+
return {};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const obj = (
|
|
303
|
+
typeof raw === "string" ? JSON.parse(raw) : raw
|
|
304
|
+
) as Record<string, unknown>;
|
|
305
|
+
|
|
306
|
+
switch (contentType) {
|
|
307
|
+
case CONTENT_TYPES.TEXT:
|
|
308
|
+
return {
|
|
309
|
+
text:
|
|
310
|
+
typeof obj.content === "string"
|
|
311
|
+
? obj.content
|
|
312
|
+
: String(obj.content ?? ""),
|
|
313
|
+
};
|
|
314
|
+
case CONTENT_TYPES.PICTURE:
|
|
315
|
+
return { picture: obj as unknown as TeamilyPictureContent };
|
|
316
|
+
case CONTENT_TYPES.VIDEO:
|
|
317
|
+
return { video: obj as unknown as TeamilyVideoContent };
|
|
318
|
+
case CONTENT_TYPES.VOICE:
|
|
319
|
+
return { audio: obj as unknown as TeamilyAudioContent };
|
|
320
|
+
default:
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Global monitor instances per account ID.
|
|
327
|
+
*/
|
|
328
|
+
const monitors = new Map<string, TeamilyMonitor>();
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Start monitoring for a Teamily account.
|
|
332
|
+
*/
|
|
333
|
+
export function startTeamilyMonitoring(
|
|
334
|
+
account: ResolvedTeamilyAccount,
|
|
335
|
+
onMessage: TeamilyMessageHandler,
|
|
336
|
+
onStateChange?: (state: TeamilyConnectionState, error?: string) => void
|
|
337
|
+
): () => void {
|
|
338
|
+
const monitor = new TeamilyMonitor({
|
|
339
|
+
account,
|
|
340
|
+
onMessage,
|
|
341
|
+
onStateChange,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
monitors.set(account.accountId, monitor);
|
|
345
|
+
monitor.start();
|
|
346
|
+
|
|
347
|
+
// Return cleanup function
|
|
348
|
+
return () => {
|
|
349
|
+
monitor.stop();
|
|
350
|
+
monitors.delete(account.accountId);
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Stop monitoring for a Teamily account.
|
|
356
|
+
*/
|
|
357
|
+
export function stopTeamilyMonitoring(accountId: string): void {
|
|
358
|
+
const monitor = monitors.get(accountId);
|
|
359
|
+
if (monitor) {
|
|
360
|
+
monitor.stop();
|
|
361
|
+
monitors.delete(accountId);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get monitor for an account.
|
|
367
|
+
*/
|
|
368
|
+
export function getTeamilyMonitor(accountId: string): TeamilyMonitor | undefined {
|
|
369
|
+
return monitors.get(accountId);
|
|
370
|
+
}
|
package/src/normalize.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { TeamilyMessageTarget } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize Teamily messaging target from raw string input.
|
|
5
|
+
*
|
|
6
|
+
* Supports formats:
|
|
7
|
+
* - "user:12345" or "u:12345" -> user 12345
|
|
8
|
+
* - "group:67890" or "g:67890" -> group 67890
|
|
9
|
+
* - "12345" -> user 12345 (default)
|
|
10
|
+
*
|
|
11
|
+
* @param raw - Raw target string
|
|
12
|
+
* @returns Normalized target with type and ID
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeTeamilyTarget(raw: string): TeamilyMessageTarget {
|
|
15
|
+
const trimmed = raw.trim();
|
|
16
|
+
if (!trimmed) {
|
|
17
|
+
throw new Error("Target cannot be empty");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lowered = trimmed.toLowerCase();
|
|
21
|
+
|
|
22
|
+
// Remove "teamily:" prefix if present
|
|
23
|
+
const withoutPrefix = lowered.startsWith("teamily:")
|
|
24
|
+
? trimmed.slice("teamily:".length).trim()
|
|
25
|
+
: trimmed;
|
|
26
|
+
|
|
27
|
+
// Group target
|
|
28
|
+
if (
|
|
29
|
+
withoutPrefix.startsWith("group:") ||
|
|
30
|
+
withoutPrefix.startsWith("g:")
|
|
31
|
+
) {
|
|
32
|
+
const groupId = withoutPrefix
|
|
33
|
+
.replace(/^(group:|g:)/i, "")
|
|
34
|
+
.trim();
|
|
35
|
+
if (!groupId) {
|
|
36
|
+
throw new Error("Group ID cannot be empty");
|
|
37
|
+
}
|
|
38
|
+
return { type: "group", id: groupId };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// User target
|
|
42
|
+
const userId = withoutPrefix
|
|
43
|
+
.replace(/^(user:|u:)/i, "")
|
|
44
|
+
.trim();
|
|
45
|
+
if (!userId) {
|
|
46
|
+
throw new Error("User ID cannot be empty");
|
|
47
|
+
}
|
|
48
|
+
return { type: "user", id: userId };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Normalize Teamily allow list entry from raw string input.
|
|
53
|
+
*
|
|
54
|
+
* @param entry - Raw allow list entry
|
|
55
|
+
* @returns Normalized user ID or group ID
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeTeamilyAllowEntry(entry: string): string {
|
|
58
|
+
const trimmed = entry.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
throw new Error("Allow entry cannot be empty");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const target = normalizeTeamilyTarget(trimmed);
|
|
64
|
+
return target.type === "group" ? `group:${target.id}` : target.id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a string looks like a Teamily target ID.
|
|
69
|
+
*
|
|
70
|
+
* @param raw - String to check
|
|
71
|
+
* @returns True if it looks like a Teamily target
|
|
72
|
+
*/
|
|
73
|
+
export function looksLikeTeamilyTargetId(raw: string): boolean {
|
|
74
|
+
const trimmed = raw.trim();
|
|
75
|
+
if (!trimmed) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const lowered = trimmed.toLowerCase();
|
|
80
|
+
|
|
81
|
+
// Check for explicit prefixes
|
|
82
|
+
if (lowered.startsWith("teamily:") || lowered.startsWith("user:") || lowered.startsWith("u:") ||
|
|
83
|
+
lowered.startsWith("group:") || lowered.startsWith("g:")) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Assume it's a user ID if it doesn't start with a known channel prefix
|
|
88
|
+
const knownPrefixes = ["telegram:", "discord:", "slack:", "matrix:", "signal:", "whatsapp:"];
|
|
89
|
+
return !knownPrefixes.some((prefix) => lowered.startsWith(prefix));
|
|
90
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ResolvedTeamilyAccount } from "./types.js";
|
|
3
|
+
import type { TeamilyProbeResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique operation ID for Teamily API requests.
|
|
7
|
+
*/
|
|
8
|
+
export function generateOperationID(): string {
|
|
9
|
+
return `${Date.now()}_${randomUUID().replace(/-/g, "").substring(0, 16)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Probe Teamily server to verify connection and authentication.
|
|
14
|
+
*
|
|
15
|
+
* @param account - Resolved Teamily account
|
|
16
|
+
* @param fetchImpl - Fetch implementation (for testing)
|
|
17
|
+
* @returns Probe result with connection status
|
|
18
|
+
*/
|
|
19
|
+
export async function probeTeamily(
|
|
20
|
+
account: ResolvedTeamilyAccount,
|
|
21
|
+
fetchImpl: typeof fetch = fetch
|
|
22
|
+
): Promise<TeamilyProbeResult> {
|
|
23
|
+
try {
|
|
24
|
+
const url = `${account.apiURL}/user/get_users_info`;
|
|
25
|
+
|
|
26
|
+
const response = await fetchImpl(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"operationID": generateOperationID(),
|
|
31
|
+
"token": account.token,
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
userIDs: [account.userID],
|
|
35
|
+
}),
|
|
36
|
+
signal: AbortSignal.timeout(5000),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const errorText = await response.text();
|
|
41
|
+
return {
|
|
42
|
+
connected: false,
|
|
43
|
+
error: `HTTP ${response.status}: ${errorText}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = await response.json() as {
|
|
48
|
+
errCode: number;
|
|
49
|
+
errMsg: string;
|
|
50
|
+
data?: Array<{ userID: string }>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (data.errCode !== 0) {
|
|
54
|
+
return {
|
|
55
|
+
connected: false,
|
|
56
|
+
error: data.errMsg || "Unknown error",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if the user exists in the response
|
|
61
|
+
const userExists =
|
|
62
|
+
Array.isArray(data.data) && data.data.some((u) => u.userID === account.userID);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
connected: true,
|
|
66
|
+
userExists,
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof Error) {
|
|
70
|
+
if (error.name === "AbortError") {
|
|
71
|
+
return {
|
|
72
|
+
connected: false,
|
|
73
|
+
error: "Connection timeout",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
connected: false,
|
|
78
|
+
error: error.message,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
connected: false,
|
|
83
|
+
error: String(error),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let teamilyRuntime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setTeamilyRuntime(runtime: PluginRuntime): void {
|
|
6
|
+
teamilyRuntime = runtime;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getTeamilyRuntime(): PluginRuntime {
|
|
10
|
+
if (!teamilyRuntime) {
|
|
11
|
+
throw new Error("Teamily runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return teamilyRuntime;
|
|
14
|
+
}
|