@ascegu/teamily 1.0.9 → 1.0.11
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 +1 -1
- package/src/channel.ts +59 -55
- package/src/monitor.ts +102 -8
package/package.json
CHANGED
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: {
|
|
@@ -268,6 +237,8 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
268
237
|
|
|
269
238
|
log?.info?.(`Starting Teamily channel (account: ${accountId})`);
|
|
270
239
|
|
|
240
|
+
const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
241
|
+
|
|
271
242
|
const stopFn = startTeamilyMonitoring(account, async (message) => {
|
|
272
243
|
const rt = getTeamilyRuntime();
|
|
273
244
|
const currentCfg = rt.config.loadConfig();
|
|
@@ -286,6 +257,26 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
286
257
|
mediaUrl = message.content.audio.sourceUrl;
|
|
287
258
|
}
|
|
288
259
|
|
|
260
|
+
// Download remote media to a local temp file so the agent recognises
|
|
261
|
+
// image-only messages (hasMediaAttachment checks MediaPath, not MediaUrl).
|
|
262
|
+
let mediaPath: string | undefined;
|
|
263
|
+
let mediaType: string | undefined;
|
|
264
|
+
if (mediaUrl) {
|
|
265
|
+
try {
|
|
266
|
+
const fetched = await rt.channel.media.fetchRemoteMedia({ url: mediaUrl, maxBytes: MEDIA_MAX_BYTES });
|
|
267
|
+
const saved = await rt.channel.media.saveMediaBuffer(
|
|
268
|
+
fetched.buffer,
|
|
269
|
+
fetched.contentType,
|
|
270
|
+
"inbound",
|
|
271
|
+
MEDIA_MAX_BYTES,
|
|
272
|
+
);
|
|
273
|
+
mediaPath = saved.path;
|
|
274
|
+
mediaType = saved.contentType;
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log?.warn?.(`[${accountId}] Failed to download Teamily media: ${String(err)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
289
280
|
const msgCtx = {
|
|
290
281
|
Body: text,
|
|
291
282
|
From: from,
|
|
@@ -296,6 +287,8 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
296
287
|
OriginatingTo: from,
|
|
297
288
|
ChatType: isGroup ? "group" : "direct",
|
|
298
289
|
MediaUrl: mediaUrl,
|
|
290
|
+
MediaPath: mediaPath,
|
|
291
|
+
MediaType: mediaType,
|
|
299
292
|
};
|
|
300
293
|
|
|
301
294
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
@@ -305,8 +298,10 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
305
298
|
deliver: async (payload: { text?: string; body?: string }) => {
|
|
306
299
|
const replyText = payload?.text ?? payload?.body;
|
|
307
300
|
if (replyText) {
|
|
301
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
302
|
+
if (!monitor) throw new Error(`Teamily monitor not running for account ${accountId}`);
|
|
308
303
|
const target = normalizeTeamilyTarget(from);
|
|
309
|
-
await
|
|
304
|
+
await monitor.sendText(target, replyText);
|
|
310
305
|
}
|
|
311
306
|
},
|
|
312
307
|
onReplyStart: () => {
|
|
@@ -369,3 +364,12 @@ function applyTeamilyAccountConfig(params: {
|
|
|
369
364
|
},
|
|
370
365
|
} as CoreConfig;
|
|
371
366
|
}
|
|
367
|
+
|
|
368
|
+
function requireMonitor(accountId?: string | null) {
|
|
369
|
+
const id = accountId || "default";
|
|
370
|
+
const monitor = getTeamilyMonitor(id);
|
|
371
|
+
if (!monitor) {
|
|
372
|
+
throw new Error(`Teamily gateway not running for account "${id}" — outbound requires an active gateway`);
|
|
373
|
+
}
|
|
374
|
+
return monitor;
|
|
375
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ResolvedTeamilyAccount,
|
|
3
3
|
TeamilyMessage,
|
|
4
|
+
TeamilyMessageTarget,
|
|
4
5
|
TeamilyPictureContent,
|
|
5
6
|
TeamilyVideoContent,
|
|
6
7
|
TeamilyAudioContent,
|
|
@@ -32,7 +33,8 @@ async function loadSDK() {
|
|
|
32
33
|
* Monitor for incoming Teamily messages using @openim/client-sdk.
|
|
33
34
|
*
|
|
34
35
|
* Delegates WebSocket connection, authentication, heartbeat, and
|
|
35
|
-
* reconnection to the official OpenIM SDK.
|
|
36
|
+
* reconnection to the official OpenIM SDK. Also exposes send methods
|
|
37
|
+
* so outbound replies flow through the same WebSocket connection.
|
|
36
38
|
*/
|
|
37
39
|
export class TeamilyMonitor {
|
|
38
40
|
private account: ResolvedTeamilyAccount;
|
|
@@ -56,7 +58,6 @@ export class TeamilyMonitor {
|
|
|
56
58
|
const sdk = getSDK();
|
|
57
59
|
this.sdk = sdk;
|
|
58
60
|
|
|
59
|
-
// Connection events
|
|
60
61
|
sdk.on(CbEvents.OnConnecting, () => {
|
|
61
62
|
if (!this.stopped) this.setState("connecting");
|
|
62
63
|
});
|
|
@@ -73,15 +74,12 @@ export class TeamilyMonitor {
|
|
|
73
74
|
if (!this.stopped) this.setState("error", "Token expired");
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
// Incoming messages
|
|
77
77
|
sdk.on(CbEvents.OnRecvNewMessages, ({ data }) => {
|
|
78
78
|
if (this.stopped || !data) return;
|
|
79
79
|
for (const msg of data) {
|
|
80
|
-
// Skip self-sent messages
|
|
81
80
|
if (msg.sendID === this.account.userID) continue;
|
|
82
81
|
const converted = convertSdkMessage(msg, this.account.userID);
|
|
83
82
|
if (converted) {
|
|
84
|
-
// Fire-and-forget; errors are logged by the gateway dispatcher
|
|
85
83
|
void this.onMessage(converted);
|
|
86
84
|
}
|
|
87
85
|
}
|
|
@@ -115,6 +113,105 @@ export class TeamilyMonitor {
|
|
|
115
113
|
return this.state;
|
|
116
114
|
}
|
|
117
115
|
|
|
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 || "";
|
|
127
|
+
}
|
|
128
|
+
|
|
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 || "";
|
|
145
|
+
}
|
|
146
|
+
|
|
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
|
+
}
|
|
171
|
+
|
|
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 || "";
|
|
188
|
+
}
|
|
189
|
+
|
|
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 || "";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private requireSdk(): SdkInstance {
|
|
209
|
+
if (!this.sdk) {
|
|
210
|
+
throw new Error("Teamily SDK not connected");
|
|
211
|
+
}
|
|
212
|
+
return this.sdk;
|
|
213
|
+
}
|
|
214
|
+
|
|
118
215
|
private setState(state: TeamilyConnectionState, error?: string): void {
|
|
119
216
|
this.state = state;
|
|
120
217
|
this.onStateChange?.(state, error);
|
|
@@ -131,7 +228,6 @@ function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage
|
|
|
131
228
|
|
|
132
229
|
const content = parseSdkContent(msg, contentType);
|
|
133
230
|
|
|
134
|
-
// Skip messages with no usable content
|
|
135
231
|
if (!content.text && !content.picture && !content.video && !content.audio) {
|
|
136
232
|
return null;
|
|
137
233
|
}
|
|
@@ -150,7 +246,6 @@ function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage
|
|
|
150
246
|
function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage["content"] {
|
|
151
247
|
switch (contentType) {
|
|
152
248
|
case CONTENT_TYPES.TEXT: {
|
|
153
|
-
// SDK puts text in textElem.content; fallback to raw content string
|
|
154
249
|
const text = msg.textElem?.content ?? tryParseTextContent(msg.content);
|
|
155
250
|
return text ? { text } : {};
|
|
156
251
|
}
|
|
@@ -174,7 +269,6 @@ function parseSdkContent(msg: MessageItem, contentType: number): TeamilyMessage[
|
|
|
174
269
|
}
|
|
175
270
|
}
|
|
176
271
|
|
|
177
|
-
/** Try to extract text from the raw JSON content string (OpenIM text format: `{"content":"..."}`) */
|
|
178
272
|
function tryParseTextContent(raw: string | undefined): string | undefined {
|
|
179
273
|
if (!raw) return undefined;
|
|
180
274
|
try {
|