@ascegu/teamily 1.0.8 → 1.0.10
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/channel.ts +35 -55
- package/src/monitor.ts +211 -289
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ascegu/teamily",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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/channel.ts
CHANGED
|
@@ -21,11 +21,10 @@ import {
|
|
|
21
21
|
} from "./accounts.js";
|
|
22
22
|
import { TeamilyConfigSchema } from "./config-schema.js";
|
|
23
23
|
import type { CoreConfig } from "./config-schema.js";
|
|
24
|
-
import { startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
24
|
+
import { getTeamilyMonitor, startTeamilyMonitoring, stopTeamilyMonitoring } from "./monitor.js";
|
|
25
25
|
import { normalizeTeamilyTarget, normalizeTeamilyAllowEntry } from "./normalize.js";
|
|
26
26
|
import { probeTeamily } from "./probe.js";
|
|
27
27
|
import { getTeamilyRuntime } from "./runtime.js";
|
|
28
|
-
import { sendMessageTeamily, sendMediaTeamily } from "./send.js";
|
|
29
28
|
import type { ResolvedTeamilyAccount } from "./types.js";
|
|
30
29
|
import { SESSION_TYPES } from "./types.js";
|
|
31
30
|
|
|
@@ -89,13 +88,12 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
89
88
|
notifyApproval: async ({ id, cfg }) => {
|
|
90
89
|
try {
|
|
91
90
|
const accountId = resolveDefaultTeamilyAccountId(cfg as CoreConfig);
|
|
92
|
-
const account = resolveTeamilyAccount(cfg as CoreConfig, accountId);
|
|
93
91
|
const target = normalizeTeamilyTarget(id);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
target,
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
93
|
+
if (monitor) {
|
|
94
|
+
await monitor.sendText(target, PAIRING_APPROVED_MESSAGE);
|
|
95
|
+
}
|
|
96
|
+
// If monitor isn't running, skip silently — pairing was still approved
|
|
99
97
|
} catch {
|
|
100
98
|
// Silently fail on notification
|
|
101
99
|
}
|
|
@@ -152,71 +150,42 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
152
150
|
}
|
|
153
151
|
try {
|
|
154
152
|
const target = normalizeTeamilyTarget(to);
|
|
155
|
-
|
|
153
|
+
// Preserve the full target format so sendText/sendMedia can distinguish user vs group
|
|
154
|
+
const resolved = target.type === "group" ? `group:${target.id}` : target.id;
|
|
155
|
+
return { ok: true, to: resolved };
|
|
156
156
|
} catch (err) {
|
|
157
157
|
return { ok: false, error: err instanceof Error ? err : new Error(String(err)) };
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
160
|
sendText: async (ctx: ChannelOutboundContext) => {
|
|
161
161
|
const { to, text, accountId } = ctx;
|
|
162
|
-
const
|
|
162
|
+
const monitor = requireMonitor(accountId);
|
|
163
163
|
const target = normalizeTeamilyTarget(to);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
account,
|
|
167
|
-
target,
|
|
168
|
-
text,
|
|
169
|
-
replyToId: ctx.replyToId || undefined,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
if (!result.success) {
|
|
173
|
-
throw new Error(result.error || "Failed to send message");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
164
|
+
const messageId = await monitor.sendText(target, text);
|
|
165
|
+
return { channel: "teamily" as const, messageId };
|
|
177
166
|
},
|
|
178
167
|
sendMedia: async (ctx: ChannelOutboundContext) => {
|
|
179
|
-
const { to,
|
|
168
|
+
const { to, accountId } = ctx;
|
|
180
169
|
const mediaUrl = ctx.mediaUrl;
|
|
181
170
|
if (!mediaUrl) {
|
|
182
171
|
throw new Error("Media URL is required");
|
|
183
172
|
}
|
|
184
|
-
const
|
|
173
|
+
const monitor = requireMonitor(accountId);
|
|
185
174
|
const target = normalizeTeamilyTarget(to);
|
|
186
175
|
|
|
187
|
-
|
|
188
|
-
let mediaType: "image" | "video" | "audio" | "file" = "image";
|
|
176
|
+
let messageId: string;
|
|
189
177
|
const urlLower = mediaUrl.toLowerCase();
|
|
190
178
|
if (urlLower.endsWith(".mp4") || urlLower.endsWith(".mov") || urlLower.endsWith(".webm")) {
|
|
191
|
-
|
|
192
|
-
} else if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
} else if (
|
|
199
|
-
urlLower.endsWith(".pdf") ||
|
|
200
|
-
urlLower.endsWith(".doc") ||
|
|
201
|
-
urlLower.endsWith(".docx") ||
|
|
202
|
-
urlLower.endsWith(".zip")
|
|
203
|
-
) {
|
|
204
|
-
mediaType = "file";
|
|
179
|
+
messageId = await monitor.sendVideo(target, mediaUrl);
|
|
180
|
+
} else if (urlLower.endsWith(".mp3") || urlLower.endsWith(".m4a") || urlLower.endsWith(".wav")) {
|
|
181
|
+
messageId = await monitor.sendAudio(target, mediaUrl);
|
|
182
|
+
} else if (urlLower.endsWith(".pdf") || urlLower.endsWith(".doc") || urlLower.endsWith(".docx") || urlLower.endsWith(".zip")) {
|
|
183
|
+
messageId = await monitor.sendFile(target, mediaUrl);
|
|
184
|
+
} else {
|
|
185
|
+
messageId = await monitor.sendImage(target, mediaUrl);
|
|
205
186
|
}
|
|
206
187
|
|
|
207
|
-
|
|
208
|
-
account,
|
|
209
|
-
target,
|
|
210
|
-
mediaUrl,
|
|
211
|
-
mediaType,
|
|
212
|
-
caption: text,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (!result.success) {
|
|
216
|
-
throw new Error(result.error || "Failed to send media");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return { channel: "teamily" as const, messageId: result.messageId ?? "" };
|
|
188
|
+
return { channel: "teamily" as const, messageId };
|
|
220
189
|
},
|
|
221
190
|
},
|
|
222
191
|
status: {
|
|
@@ -305,8 +274,10 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
305
274
|
deliver: async (payload: { text?: string; body?: string }) => {
|
|
306
275
|
const replyText = payload?.text ?? payload?.body;
|
|
307
276
|
if (replyText) {
|
|
277
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
278
|
+
if (!monitor) throw new Error(`Teamily monitor not running for account ${accountId}`);
|
|
308
279
|
const target = normalizeTeamilyTarget(from);
|
|
309
|
-
await
|
|
280
|
+
await monitor.sendText(target, replyText);
|
|
310
281
|
}
|
|
311
282
|
},
|
|
312
283
|
onReplyStart: () => {
|
|
@@ -369,3 +340,12 @@ function applyTeamilyAccountConfig(params: {
|
|
|
369
340
|
},
|
|
370
341
|
} as CoreConfig;
|
|
371
342
|
}
|
|
343
|
+
|
|
344
|
+
function requireMonitor(accountId?: string | null) {
|
|
345
|
+
const id = accountId || "default";
|
|
346
|
+
const monitor = getTeamilyMonitor(id);
|
|
347
|
+
if (!monitor) {
|
|
348
|
+
throw new Error(`Teamily gateway not running for account "${id}" — outbound requires an active gateway`);
|
|
349
|
+
}
|
|
350
|
+
return monitor;
|
|
351
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import { generateOperationID } from "./probe.js";
|
|
2
1
|
import type {
|
|
3
2
|
ResolvedTeamilyAccount,
|
|
4
3
|
TeamilyMessage,
|
|
4
|
+
TeamilyMessageTarget,
|
|
5
5
|
TeamilyPictureContent,
|
|
6
6
|
TeamilyVideoContent,
|
|
7
7
|
TeamilyAudioContent,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
|
|
10
10
|
|
|
11
|
-
const WS_REQ = {
|
|
12
|
-
LOGIN: 1001,
|
|
13
|
-
HEARTBEAT: 1002,
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
11
|
export type TeamilyMessageHandler = (message: TeamilyMessage) => Promise<void> | void;
|
|
17
12
|
export type TeamilyConnectionState = "connecting" | "connected" | "disconnected" | "error";
|
|
18
13
|
|
|
@@ -20,361 +15,291 @@ export interface TeamilyMonitorOptions {
|
|
|
20
15
|
account: ResolvedTeamilyAccount;
|
|
21
16
|
onMessage: TeamilyMessageHandler;
|
|
22
17
|
onStateChange?: (state: TeamilyConnectionState, error?: string) => void;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SdkModule = typeof import("@openim/client-sdk");
|
|
21
|
+
type SdkInstance = ReturnType<SdkModule["getSDK"]>;
|
|
22
|
+
|
|
23
|
+
// Lazy-loaded SDK to avoid top-level dynamic import issues
|
|
24
|
+
let sdkModule: SdkModule | null = null;
|
|
25
|
+
async function loadSDK() {
|
|
26
|
+
if (!sdkModule) {
|
|
27
|
+
sdkModule = await import("@openim/client-sdk");
|
|
28
|
+
}
|
|
29
|
+
return sdkModule;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
|
-
* Monitor for incoming Teamily messages
|
|
33
|
+
* Monitor for incoming Teamily messages using @openim/client-sdk.
|
|
30
34
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
35
|
+
* Delegates WebSocket connection, authentication, heartbeat, and
|
|
36
|
+
* reconnection to the official OpenIM SDK. Also exposes send methods
|
|
37
|
+
* so outbound replies flow through the same WebSocket connection.
|
|
33
38
|
*/
|
|
34
39
|
export class TeamilyMonitor {
|
|
35
40
|
private account: ResolvedTeamilyAccount;
|
|
36
41
|
private onMessage: TeamilyMessageHandler;
|
|
37
42
|
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;
|
|
43
|
+
private sdk: SdkInstance | null = null;
|
|
43
44
|
private state: TeamilyConnectionState = "disconnected";
|
|
44
|
-
private
|
|
45
|
-
private authenticated = false;
|
|
46
|
-
private wsImpl: typeof WebSocket;
|
|
45
|
+
private stopped = false;
|
|
47
46
|
|
|
48
47
|
constructor(options: TeamilyMonitorOptions) {
|
|
49
48
|
this.account = options.account;
|
|
50
49
|
this.onMessage = options.onMessage;
|
|
51
50
|
this.onStateChange = options.onStateChange;
|
|
52
|
-
this.reconnectInterval = options.reconnectInterval ?? 5000;
|
|
53
|
-
this.pingInterval = options.pingInterval ?? 30000;
|
|
54
|
-
this.wsImpl = options.websocketImpl ?? WebSocket;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
/**
|
|
58
|
-
* Start monitoring for messages.
|
|
59
|
-
*/
|
|
60
53
|
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
|
-
|
|
54
|
+
this.stopped = false;
|
|
107
55
|
this.setState("connecting");
|
|
108
56
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
57
|
+
const { getSDK, CbEvents } = await loadSDK();
|
|
58
|
+
const sdk = getSDK();
|
|
59
|
+
this.sdk = sdk;
|
|
60
|
+
|
|
61
|
+
sdk.on(CbEvents.OnConnecting, () => {
|
|
62
|
+
if (!this.stopped) this.setState("connecting");
|
|
63
|
+
});
|
|
64
|
+
sdk.on(CbEvents.OnConnectSuccess, () => {
|
|
65
|
+
if (!this.stopped) this.setState("connected");
|
|
66
|
+
});
|
|
67
|
+
sdk.on(CbEvents.OnConnectFailed, ({ errCode, errMsg }) => {
|
|
68
|
+
if (!this.stopped) this.setState("error", `[${errCode}] ${errMsg}`);
|
|
69
|
+
});
|
|
70
|
+
sdk.on(CbEvents.OnKickedOffline, () => {
|
|
71
|
+
if (!this.stopped) this.setState("error", "Kicked offline");
|
|
72
|
+
});
|
|
73
|
+
sdk.on(CbEvents.OnUserTokenExpired, () => {
|
|
74
|
+
if (!this.stopped) this.setState("error", "Token expired");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
sdk.on(CbEvents.OnRecvNewMessages, ({ data }) => {
|
|
78
|
+
if (this.stopped || !data) return;
|
|
79
|
+
for (const msg of data) {
|
|
80
|
+
if (msg.sendID === this.account.userID) continue;
|
|
81
|
+
const converted = convertSdkMessage(msg, this.account.userID);
|
|
82
|
+
if (converted) {
|
|
83
|
+
void this.onMessage(converted);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
114
87
|
|
|
115
88
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
} catch (
|
|
124
|
-
|
|
89
|
+
await sdk.login({
|
|
90
|
+
userID: this.account.userID,
|
|
91
|
+
token: this.account.token,
|
|
92
|
+
platformID: 5,
|
|
93
|
+
wsAddr: this.account.wsURL,
|
|
94
|
+
apiAddr: this.account.apiURL,
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
this.setState("error", msg);
|
|
99
|
+
throw err;
|
|
125
100
|
}
|
|
126
101
|
}
|
|
127
102
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Send authentication message after WebSocket connection opens.
|
|
137
|
-
*/
|
|
138
|
-
private sendAuth(): void {
|
|
139
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
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);
|
|
103
|
+
stop(): void {
|
|
104
|
+
this.stopped = true;
|
|
105
|
+
if (this.sdk) {
|
|
106
|
+
this.sdk.logout().catch(() => {});
|
|
107
|
+
this.sdk = null;
|
|
154
108
|
}
|
|
109
|
+
this.setState("disconnected");
|
|
155
110
|
}
|
|
156
111
|
|
|
157
|
-
|
|
158
|
-
|
|
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();
|
|
185
|
-
}
|
|
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
|
-
}
|
|
197
|
-
|
|
198
|
-
const contentType = data.contentType || CONTENT_TYPES.TEXT;
|
|
199
|
-
|
|
200
|
-
const message: TeamilyMessage = {
|
|
201
|
-
serverMsgID: data.msgID || `${Date.now()}_${Math.random()}`,
|
|
202
|
-
sendID: data.sendID || data.msgFrom || "unknown",
|
|
203
|
-
recvID: data.recvID || this.account.userID,
|
|
204
|
-
content: parseMessageContent(data.content, contentType),
|
|
205
|
-
contentType,
|
|
206
|
-
sessionType: data.sessionType || SESSION_TYPES.SINGLE,
|
|
207
|
-
sendTime: data.sendTime || Date.now(),
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
await this.onMessage(message);
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error("Failed to parse Teamily message:", error);
|
|
213
|
-
}
|
|
112
|
+
getState(): TeamilyConnectionState {
|
|
113
|
+
return this.state;
|
|
214
114
|
}
|
|
215
115
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
}
|
|
116
|
+
/** Send a text message through the SDK WebSocket connection. */
|
|
117
|
+
async sendText(target: TeamilyMessageTarget, text: string): Promise<string> {
|
|
118
|
+
const sdk = this.requireSdk();
|
|
119
|
+
const created = await sdk.createTextMessage(text);
|
|
120
|
+
const message = created.data;
|
|
121
|
+
const result = await sdk.sendMessage({
|
|
122
|
+
recvID: target.type === "user" ? target.id : "",
|
|
123
|
+
groupID: target.type === "group" ? target.id : "",
|
|
124
|
+
message,
|
|
125
|
+
});
|
|
126
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
241
127
|
}
|
|
242
128
|
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}, this.reconnectInterval);
|
|
129
|
+
/** Send an image message through the SDK WebSocket connection. */
|
|
130
|
+
async sendImage(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
131
|
+
const sdk = this.requireSdk();
|
|
132
|
+
const picInfo = { uuid: "", type: "", width: 0, height: 0, size: 0, url };
|
|
133
|
+
const created = await sdk.createImageMessageByURL({
|
|
134
|
+
sourcePicture: picInfo,
|
|
135
|
+
bigPicture: picInfo,
|
|
136
|
+
snapshotPicture: { ...picInfo, url: "" },
|
|
137
|
+
sourcePath: "",
|
|
138
|
+
});
|
|
139
|
+
const result = await sdk.sendMessage({
|
|
140
|
+
recvID: target.type === "user" ? target.id : "",
|
|
141
|
+
groupID: target.type === "group" ? target.id : "",
|
|
142
|
+
message: created.data,
|
|
143
|
+
});
|
|
144
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
260
145
|
}
|
|
261
146
|
|
|
262
|
-
/**
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
147
|
+
/** Send a video message through the SDK WebSocket connection. */
|
|
148
|
+
async sendVideo(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
149
|
+
const sdk = this.requireSdk();
|
|
150
|
+
const created = await sdk.createVideoMessageByURL({
|
|
151
|
+
videoPath: "",
|
|
152
|
+
duration: 0,
|
|
153
|
+
videoType: "mp4",
|
|
154
|
+
snapshotPath: "",
|
|
155
|
+
videoUUID: "",
|
|
156
|
+
videoUrl: url,
|
|
157
|
+
videoSize: 0,
|
|
158
|
+
snapshotUUID: "",
|
|
159
|
+
snapshotSize: 0,
|
|
160
|
+
snapshotUrl: "",
|
|
161
|
+
snapshotWidth: 0,
|
|
162
|
+
snapshotHeight: 0,
|
|
163
|
+
});
|
|
164
|
+
const result = await sdk.sendMessage({
|
|
165
|
+
recvID: target.type === "user" ? target.id : "",
|
|
166
|
+
groupID: target.type === "group" ? target.id : "",
|
|
167
|
+
message: created.data,
|
|
168
|
+
});
|
|
169
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
170
|
+
}
|
|
267
171
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
172
|
+
/** Send a sound/audio message through the SDK WebSocket connection. */
|
|
173
|
+
async sendAudio(target: TeamilyMessageTarget, url: string): Promise<string> {
|
|
174
|
+
const sdk = this.requireSdk();
|
|
175
|
+
const created = await sdk.createSoundMessageByURL({
|
|
176
|
+
uuid: "",
|
|
177
|
+
soundPath: "",
|
|
178
|
+
sourceUrl: url,
|
|
179
|
+
dataSize: 0,
|
|
180
|
+
duration: 0,
|
|
181
|
+
});
|
|
182
|
+
const result = await sdk.sendMessage({
|
|
183
|
+
recvID: target.type === "user" ? target.id : "",
|
|
184
|
+
groupID: target.type === "group" ? target.id : "",
|
|
185
|
+
message: created.data,
|
|
186
|
+
});
|
|
187
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
271
188
|
}
|
|
272
189
|
|
|
273
|
-
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
190
|
+
/** Send a file message through the SDK WebSocket connection. */
|
|
191
|
+
async sendFile(target: TeamilyMessageTarget, url: string, fileName?: string): Promise<string> {
|
|
192
|
+
const sdk = this.requireSdk();
|
|
193
|
+
const created = await sdk.createFileMessageByURL({
|
|
194
|
+
filePath: "",
|
|
195
|
+
fileName: fileName || url.split("/").pop() || "file",
|
|
196
|
+
uuid: "",
|
|
197
|
+
sourceUrl: url,
|
|
198
|
+
fileSize: 0,
|
|
199
|
+
});
|
|
200
|
+
const result = await sdk.sendMessage({
|
|
201
|
+
recvID: target.type === "user" ? target.id : "",
|
|
202
|
+
groupID: target.type === "group" ? target.id : "",
|
|
203
|
+
message: created.data,
|
|
204
|
+
});
|
|
205
|
+
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
281
206
|
}
|
|
282
207
|
|
|
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
|
-
}
|
|
208
|
+
private requireSdk(): SdkInstance {
|
|
209
|
+
if (!this.sdk) {
|
|
210
|
+
throw new Error("Teamily SDK not connected");
|
|
300
211
|
}
|
|
212
|
+
return this.sdk;
|
|
301
213
|
}
|
|
302
214
|
|
|
303
|
-
/**
|
|
304
|
-
* Update and notify connection state.
|
|
305
|
-
*/
|
|
306
215
|
private setState(state: TeamilyConnectionState, error?: string): void {
|
|
307
216
|
this.state = state;
|
|
308
217
|
this.onStateChange?.(state, error);
|
|
309
218
|
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Get current connection state.
|
|
313
|
-
*/
|
|
314
|
-
getState(): TeamilyConnectionState {
|
|
315
|
-
return this.state;
|
|
316
|
-
}
|
|
317
219
|
}
|
|
318
220
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
221
|
+
// ---- SDK message conversion helpers ----
|
|
222
|
+
|
|
223
|
+
import type { MessageItem } from "@openim/client-sdk";
|
|
224
|
+
|
|
225
|
+
function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage | null {
|
|
226
|
+
const contentType = msg.contentType ?? CONTENT_TYPES.TEXT;
|
|
227
|
+
const sessionType = msg.sessionType ?? SESSION_TYPES.SINGLE;
|
|
228
|
+
|
|
229
|
+
const content = parseSdkContent(msg, contentType);
|
|
230
|
+
|
|
231
|
+
if (!content.text && !content.picture && !content.video && !content.audio) {
|
|
232
|
+
return null;
|
|
326
233
|
}
|
|
327
234
|
|
|
328
|
-
|
|
235
|
+
return {
|
|
236
|
+
serverMsgID: msg.serverMsgID || msg.clientMsgID || `${Date.now()}_${Math.random()}`,
|
|
237
|
+
sendID: msg.sendID || "unknown",
|
|
238
|
+
recvID: sessionType === SESSION_TYPES.GROUP ? (msg.groupID || "") : (msg.recvID || selfUserID),
|
|
239
|
+
content,
|
|
240
|
+
contentType,
|
|
241
|
+
sessionType,
|
|
242
|
+
sendTime: msg.sendTime || Date.now(),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
329
245
|
|
|
246
|
+
function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage["content"] {
|
|
330
247
|
switch (contentType) {
|
|
331
|
-
case CONTENT_TYPES.TEXT:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
248
|
+
case CONTENT_TYPES.TEXT: {
|
|
249
|
+
const text = msg.textElem?.content ?? tryParseTextContent(msg.content);
|
|
250
|
+
return text ? { text } : {};
|
|
251
|
+
}
|
|
335
252
|
case CONTENT_TYPES.PICTURE:
|
|
336
|
-
|
|
253
|
+
if (msg.pictureElem?.sourcePicture) {
|
|
254
|
+
return { picture: msg.pictureElem as unknown as TeamilyPictureContent };
|
|
255
|
+
}
|
|
256
|
+
return {};
|
|
337
257
|
case CONTENT_TYPES.VIDEO:
|
|
338
|
-
|
|
258
|
+
if (msg.videoElem?.videoUrl) {
|
|
259
|
+
return { video: msg.videoElem as unknown as TeamilyVideoContent };
|
|
260
|
+
}
|
|
261
|
+
return {};
|
|
339
262
|
case CONTENT_TYPES.VOICE:
|
|
340
|
-
|
|
263
|
+
if (msg.soundElem?.sourceUrl) {
|
|
264
|
+
return { audio: msg.soundElem as unknown as TeamilyAudioContent };
|
|
265
|
+
}
|
|
266
|
+
return {};
|
|
341
267
|
default:
|
|
342
268
|
return {};
|
|
343
269
|
}
|
|
344
270
|
}
|
|
345
271
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
272
|
+
function tryParseTextContent(raw: string | undefined): string | undefined {
|
|
273
|
+
if (!raw) return undefined;
|
|
274
|
+
try {
|
|
275
|
+
const obj = JSON.parse(raw) as { content?: string };
|
|
276
|
+
return typeof obj.content === "string" ? obj.content : undefined;
|
|
277
|
+
} catch {
|
|
278
|
+
return raw;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- Global monitor registry ----
|
|
283
|
+
|
|
349
284
|
const monitors = new Map<string, TeamilyMonitor>();
|
|
350
285
|
|
|
351
|
-
/**
|
|
352
|
-
* Start monitoring for a Teamily account.
|
|
353
|
-
*/
|
|
354
286
|
export function startTeamilyMonitoring(
|
|
355
287
|
account: ResolvedTeamilyAccount,
|
|
356
288
|
onMessage: TeamilyMessageHandler,
|
|
357
289
|
onStateChange?: (state: TeamilyConnectionState, error?: string) => void,
|
|
358
290
|
): () => void {
|
|
359
|
-
const monitor = new TeamilyMonitor({
|
|
360
|
-
account,
|
|
361
|
-
onMessage,
|
|
362
|
-
onStateChange,
|
|
363
|
-
});
|
|
364
|
-
|
|
291
|
+
const monitor = new TeamilyMonitor({ account, onMessage, onStateChange });
|
|
365
292
|
monitors.set(account.accountId, monitor);
|
|
366
|
-
monitor.start()
|
|
293
|
+
monitor.start().catch((err) => {
|
|
294
|
+
console.error(`Teamily monitor start failed for ${account.accountId}:`, err);
|
|
295
|
+
});
|
|
367
296
|
|
|
368
|
-
// Return cleanup function
|
|
369
297
|
return () => {
|
|
370
298
|
monitor.stop();
|
|
371
299
|
monitors.delete(account.accountId);
|
|
372
300
|
};
|
|
373
301
|
}
|
|
374
302
|
|
|
375
|
-
/**
|
|
376
|
-
* Stop monitoring for a Teamily account.
|
|
377
|
-
*/
|
|
378
303
|
export function stopTeamilyMonitoring(accountId: string): void {
|
|
379
304
|
const monitor = monitors.get(accountId);
|
|
380
305
|
if (monitor) {
|
|
@@ -383,9 +308,6 @@ export function stopTeamilyMonitoring(accountId: string): void {
|
|
|
383
308
|
}
|
|
384
309
|
}
|
|
385
310
|
|
|
386
|
-
/**
|
|
387
|
-
* Get monitor for an account.
|
|
388
|
-
*/
|
|
389
311
|
export function getTeamilyMonitor(accountId: string): TeamilyMonitor | undefined {
|
|
390
312
|
return monitors.get(accountId);
|
|
391
313
|
}
|