@ascegu/teamily 1.0.19 → 1.0.22
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 +33 -35
- package/package.json +1 -1
- package/src/accounts.ts +0 -1
- package/src/channel.ts +139 -123
- package/src/config-schema.ts +0 -1
- package/src/monitor.ts +59 -5
- package/src/types.ts +0 -2
package/README.md
CHANGED
|
@@ -26,29 +26,28 @@ openclaw channel configure teamily
|
|
|
26
26
|
|
|
27
27
|
### Server Settings
|
|
28
28
|
|
|
29
|
-
| Field
|
|
30
|
-
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `wsURL` | Teamily WebSocket URL | `wss://imserver-test.teamily.ai/msg_gateway` |
|
|
29
|
+
| Field | Description | Default |
|
|
30
|
+
| -------- | --------------------- | -------------------------------------------- |
|
|
31
|
+
| `apiURL` | Teamily REST API URL | `https://imserver-test.teamily.ai/im_api` |
|
|
32
|
+
| `wsURL` | Teamily WebSocket URL | `wss://imserver-test.teamily.ai/msg_gateway` |
|
|
34
33
|
|
|
35
34
|
### Bot Account Settings
|
|
36
35
|
|
|
37
|
-
| Field | Required | Description
|
|
38
|
-
|
|
39
|
-
| `userID` | Yes | User ID for the bot account
|
|
40
|
-
| `token` | Yes | User token for authentication
|
|
41
|
-
| `nickname` | No | Display nickname
|
|
42
|
-
| `faceURL` | No | Avatar URL
|
|
36
|
+
| Field | Required | Description |
|
|
37
|
+
| ---------- | -------- | ----------------------------- |
|
|
38
|
+
| `userID` | Yes | User ID for the bot account |
|
|
39
|
+
| `token` | Yes | User token for authentication |
|
|
40
|
+
| `nickname` | No | Display nickname |
|
|
41
|
+
| `faceURL` | No | Avatar URL |
|
|
43
42
|
|
|
44
43
|
### DM Security
|
|
45
44
|
|
|
46
45
|
Per-account or channel-level DM security can be configured:
|
|
47
46
|
|
|
48
|
-
| Field
|
|
49
|
-
|
|
50
|
-
| `dm.policy`
|
|
51
|
-
| `dm.allowFrom`
|
|
47
|
+
| Field | Description |
|
|
48
|
+
| -------------- | --------------------------------------------------- |
|
|
49
|
+
| `dm.policy` | DM security policy (`pairing`, `allowlist`, `open`) |
|
|
50
|
+
| `dm.allowFrom` | List of allowed sender IDs |
|
|
52
51
|
|
|
53
52
|
### Example Configuration
|
|
54
53
|
|
|
@@ -57,7 +56,6 @@ channels:
|
|
|
57
56
|
teamily:
|
|
58
57
|
enabled: true
|
|
59
58
|
server:
|
|
60
|
-
platformUrl: https://imserver-test.teamily.ai/im_api
|
|
61
59
|
apiURL: https://imserver-test.teamily.ai/im_api
|
|
62
60
|
wsURL: wss://imserver-test.teamily.ai/msg_gateway
|
|
63
61
|
accounts:
|
|
@@ -91,12 +89,12 @@ openclaw message send teamily:user:userID --media /path/to/image.jpg
|
|
|
91
89
|
|
|
92
90
|
Supported media types are auto-detected by file extension:
|
|
93
91
|
|
|
94
|
-
| Extension
|
|
95
|
-
|
|
96
|
-
| `.jpg`, `.png`, `.gif`, etc.
|
|
97
|
-
| `.mp4`, `.mov`, `.webm`
|
|
98
|
-
| `.mp3`, `.m4a`, `.wav`
|
|
99
|
-
| `.pdf`, `.doc`, `.docx`, `.zip` | File
|
|
92
|
+
| Extension | Type |
|
|
93
|
+
| ------------------------------- | ----- |
|
|
94
|
+
| `.jpg`, `.png`, `.gif`, etc. | Image |
|
|
95
|
+
| `.mp4`, `.mov`, `.webm` | Video |
|
|
96
|
+
| `.mp3`, `.m4a`, `.wav` | Audio |
|
|
97
|
+
| `.pdf`, `.doc`, `.docx`, `.zip` | File |
|
|
100
98
|
|
|
101
99
|
## Group Chat Behavior
|
|
102
100
|
|
|
@@ -107,19 +105,19 @@ Supported media types are auto-detected by file extension:
|
|
|
107
105
|
|
|
108
106
|
## Capabilities
|
|
109
107
|
|
|
110
|
-
| Feature
|
|
111
|
-
|
|
112
|
-
| Direct messaging
|
|
113
|
-
| Group messaging
|
|
114
|
-
| Text messages
|
|
115
|
-
| Media (image/video/audio/file) | Yes
|
|
116
|
-
| @-mention gating (groups)
|
|
117
|
-
| WebSocket real-time monitoring | Yes
|
|
118
|
-
| Automatic reconnection
|
|
119
|
-
| Connection health probes
|
|
120
|
-
| Reactions
|
|
121
|
-
| Threads
|
|
122
|
-
| Polls
|
|
108
|
+
| Feature | Supported |
|
|
109
|
+
| ------------------------------ | --------- |
|
|
110
|
+
| Direct messaging | Yes |
|
|
111
|
+
| Group messaging | Yes |
|
|
112
|
+
| Text messages | Yes |
|
|
113
|
+
| Media (image/video/audio/file) | Yes |
|
|
114
|
+
| @-mention gating (groups) | Yes |
|
|
115
|
+
| WebSocket real-time monitoring | Yes |
|
|
116
|
+
| Automatic reconnection | Yes |
|
|
117
|
+
| Connection health probes | Yes |
|
|
118
|
+
| Reactions | No |
|
|
119
|
+
| Threads | No |
|
|
120
|
+
| Polls | No |
|
|
123
121
|
|
|
124
122
|
## Architecture
|
|
125
123
|
|
package/package.json
CHANGED
package/src/accounts.ts
CHANGED
package/src/channel.ts
CHANGED
|
@@ -182,9 +182,18 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
182
182
|
const urlLower = mediaUrl.toLowerCase();
|
|
183
183
|
if (urlLower.endsWith(".mp4") || urlLower.endsWith(".mov") || urlLower.endsWith(".webm")) {
|
|
184
184
|
messageId = await monitor.sendVideo(target, mediaUrl);
|
|
185
|
-
} else if (
|
|
185
|
+
} else if (
|
|
186
|
+
urlLower.endsWith(".mp3") ||
|
|
187
|
+
urlLower.endsWith(".m4a") ||
|
|
188
|
+
urlLower.endsWith(".wav")
|
|
189
|
+
) {
|
|
186
190
|
messageId = await monitor.sendAudio(target, mediaUrl);
|
|
187
|
-
} else if (
|
|
191
|
+
} else if (
|
|
192
|
+
urlLower.endsWith(".pdf") ||
|
|
193
|
+
urlLower.endsWith(".doc") ||
|
|
194
|
+
urlLower.endsWith(".docx") ||
|
|
195
|
+
urlLower.endsWith(".zip")
|
|
196
|
+
) {
|
|
188
197
|
messageId = await monitor.sendFile(target, mediaUrl);
|
|
189
198
|
} else {
|
|
190
199
|
messageId = await monitor.sendImage(target, mediaUrl);
|
|
@@ -248,140 +257,146 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
|
|
|
248
257
|
|
|
249
258
|
const stopFn = startTeamilyMonitoring(account, async (message) => {
|
|
250
259
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
260
|
+
const rt = getTeamilyRuntime();
|
|
261
|
+
const currentCfg = rt.config.loadConfig();
|
|
253
262
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
const isGroup = isGroupSession(message.sessionType);
|
|
264
|
+
const from = message.sendID;
|
|
265
|
+
const rawText = message.content?.text || "";
|
|
257
266
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
log?.info?.(
|
|
268
|
+
`[${accountId}] Incoming message: sessionType=${message.sessionType}, isGroup=${isGroup}, ` +
|
|
269
|
+
`from=${from}, recvID=${message.recvID}, isAtSelf=${message.isAtSelf ?? false}`,
|
|
270
|
+
);
|
|
262
271
|
|
|
263
|
-
|
|
272
|
+
const historyKey = isGroup ? `teamily:group:${message.recvID}` : undefined;
|
|
264
273
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
274
|
+
// In group chats, buffer non-@-mention text-only messages for context.
|
|
275
|
+
// Media messages (picture/video/audio) are always dispatched — OpenIM sends
|
|
276
|
+
// @-mention text and media as separate messages, so a PICTURE following an
|
|
277
|
+
// AT_TEXT won't have isAtSelf=true. Dispatching media keeps group image
|
|
278
|
+
// handling consistent with DM behavior.
|
|
279
|
+
const hasMedia = !!(
|
|
280
|
+
message.content?.picture ||
|
|
281
|
+
message.content?.video ||
|
|
282
|
+
message.content?.audio
|
|
283
|
+
);
|
|
284
|
+
if (isGroup && !message.isAtSelf && !hasMedia) {
|
|
285
|
+
if (historyKey && rawText) {
|
|
286
|
+
recordPendingHistoryEntryIfEnabled({
|
|
287
|
+
historyMap: groupHistories,
|
|
288
|
+
historyKey,
|
|
289
|
+
limit: historyLimit,
|
|
290
|
+
entry: {
|
|
291
|
+
sender: from,
|
|
292
|
+
body: rawText,
|
|
293
|
+
timestamp: message.sendTime,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
287
298
|
}
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
299
|
|
|
291
|
-
|
|
300
|
+
const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
|
|
292
301
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
let mediaUrl: string | undefined;
|
|
303
|
+
if (message.content?.picture?.sourcePicture?.url) {
|
|
304
|
+
mediaUrl = message.content.picture.sourcePicture.url;
|
|
305
|
+
} else if (message.content?.video?.videoUrl) {
|
|
306
|
+
mediaUrl = message.content.video.videoUrl;
|
|
307
|
+
} else if (message.content?.audio?.sourceUrl) {
|
|
308
|
+
mediaUrl = message.content.audio.sourceUrl;
|
|
309
|
+
}
|
|
301
310
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
311
|
+
// Download remote media to a local temp file so the agent recognises
|
|
312
|
+
// image-only messages (hasMediaAttachment checks MediaPath, not MediaUrl).
|
|
313
|
+
let mediaPath: string | undefined;
|
|
314
|
+
let mediaType: string | undefined;
|
|
315
|
+
if (mediaUrl) {
|
|
316
|
+
try {
|
|
317
|
+
const fetched = await rt.channel.media.fetchRemoteMedia({
|
|
318
|
+
url: mediaUrl,
|
|
319
|
+
maxBytes: MEDIA_MAX_BYTES,
|
|
320
|
+
});
|
|
321
|
+
const saved = await rt.channel.media.saveMediaBuffer(
|
|
322
|
+
fetched.buffer,
|
|
323
|
+
fetched.contentType,
|
|
324
|
+
"inbound",
|
|
325
|
+
MEDIA_MAX_BYTES,
|
|
326
|
+
);
|
|
327
|
+
mediaPath = saved.path;
|
|
328
|
+
mediaType = saved.contentType;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
log?.warn?.(`[${accountId}] Failed to download Teamily media: ${String(err)}`);
|
|
331
|
+
}
|
|
319
332
|
}
|
|
320
|
-
}
|
|
321
333
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
// For group @-mention messages, prepend buffered history as context.
|
|
335
|
+
const body =
|
|
336
|
+
isGroup && historyKey
|
|
337
|
+
? buildPendingHistoryContextFromMap({
|
|
338
|
+
historyMap: groupHistories,
|
|
339
|
+
historyKey,
|
|
340
|
+
limit: historyLimit,
|
|
341
|
+
currentMessage: rawText,
|
|
342
|
+
formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
|
|
343
|
+
})
|
|
344
|
+
: rawText;
|
|
333
345
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
346
|
+
const replyTarget = isGroup ? `group:${message.recvID}` : from;
|
|
347
|
+
const msgCtx = {
|
|
348
|
+
Body: body,
|
|
349
|
+
From: from,
|
|
350
|
+
To: account.userID,
|
|
351
|
+
SessionKey: sessionKey,
|
|
352
|
+
AccountId: accountId,
|
|
353
|
+
Provider: "teamily" as const,
|
|
354
|
+
Surface: "teamily" as const,
|
|
355
|
+
OriginatingChannel: "teamily" as const,
|
|
356
|
+
OriginatingTo: replyTarget,
|
|
357
|
+
ChatType: isGroup ? "group" : "direct",
|
|
358
|
+
MediaUrl: mediaUrl,
|
|
359
|
+
MediaPath: mediaPath,
|
|
360
|
+
MediaType: mediaType,
|
|
361
|
+
};
|
|
350
362
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
363
|
+
log?.info?.(
|
|
364
|
+
`[${accountId}] Dispatching reply: sessionKey=${sessionKey}, chatType=${isGroup ? "group" : "direct"}, replyTarget=${replyTarget}`,
|
|
365
|
+
);
|
|
354
366
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
368
|
+
ctx: msgCtx,
|
|
369
|
+
cfg: currentCfg,
|
|
370
|
+
dispatcherOptions: {
|
|
371
|
+
deliver: async (payload: { text?: string; body?: string }) => {
|
|
372
|
+
const replyText = payload?.text ?? payload?.body;
|
|
373
|
+
if (replyText) {
|
|
374
|
+
const monitor = getTeamilyMonitor(accountId);
|
|
375
|
+
if (!monitor)
|
|
376
|
+
throw new Error(`Teamily monitor not running for account ${accountId}`);
|
|
377
|
+
log?.info?.(
|
|
378
|
+
`[${accountId}] Sending reply to: ${replyTarget} (isGroup=${isGroup})`,
|
|
379
|
+
);
|
|
380
|
+
const target = normalizeTeamilyTarget(replyTarget);
|
|
381
|
+
await monitor.sendText(target, replyText);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
onReplyStart: () => {
|
|
385
|
+
log?.info?.(`Agent reply started for ${from}`);
|
|
386
|
+
},
|
|
371
387
|
},
|
|
372
|
-
}
|
|
373
|
-
});
|
|
388
|
+
});
|
|
374
389
|
|
|
375
|
-
|
|
390
|
+
log?.info?.(`[${accountId}] Dispatch completed for ${sessionKey}`);
|
|
376
391
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
392
|
+
// Clear history buffer after dispatch so context doesn't repeat.
|
|
393
|
+
if (isGroup && historyKey) {
|
|
394
|
+
clearHistoryEntriesIfEnabled({
|
|
395
|
+
historyMap: groupHistories,
|
|
396
|
+
historyKey,
|
|
397
|
+
limit: historyLimit,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
385
400
|
} catch (err) {
|
|
386
401
|
log?.error?.(
|
|
387
402
|
`[${accountId}] Error handling message from ${message.sendID}: ${err instanceof Error ? err.stack || err.message : String(err)}`,
|
|
@@ -410,7 +425,7 @@ function applyTeamilyAccountConfig(params: {
|
|
|
410
425
|
}): CoreConfig {
|
|
411
426
|
const { cfg, accountId, input } = params;
|
|
412
427
|
const existing = cfg.channels?.teamily;
|
|
413
|
-
const server = existing?.server ?? {
|
|
428
|
+
const server = existing?.server ?? { apiURL: "", wsURL: "" };
|
|
414
429
|
const accounts = existing?.accounts ?? {};
|
|
415
430
|
|
|
416
431
|
const accountUpdate: Record<string, unknown> = {};
|
|
@@ -420,7 +435,6 @@ function applyTeamilyAccountConfig(params: {
|
|
|
420
435
|
if (input.faceURL) accountUpdate.faceURL = String(input.faceURL);
|
|
421
436
|
|
|
422
437
|
const serverUpdate: Record<string, string> = {};
|
|
423
|
-
if (input.platformUrl) serverUpdate.platformUrl = String(input.platformUrl);
|
|
424
438
|
if (input.apiURL) serverUpdate.apiURL = String(input.apiURL);
|
|
425
439
|
if (input.wsURL) serverUpdate.wsURL = String(input.wsURL);
|
|
426
440
|
|
|
@@ -447,7 +461,9 @@ function requireMonitor(accountId?: string | null) {
|
|
|
447
461
|
const id = accountId || "default";
|
|
448
462
|
const monitor = getTeamilyMonitor(id);
|
|
449
463
|
if (!monitor) {
|
|
450
|
-
throw new Error(
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Teamily gateway not running for account "${id}" — outbound requires an active gateway`,
|
|
466
|
+
);
|
|
451
467
|
}
|
|
452
468
|
return monitor;
|
|
453
469
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { TeamilyConfig } from "./types.js";
|
|
|
5
5
|
|
|
6
6
|
// Server configuration schema
|
|
7
7
|
export const TeamilyServerConfigSchema = z.object({
|
|
8
|
-
platformUrl: z.string().url().default("http://localhost:10002").describe("Teamily platform URL"),
|
|
9
8
|
apiURL: z.string().url().default("http://localhost:10002").describe("Teamily REST API URL"),
|
|
10
9
|
wsURL: z.string().url().default("ws://localhost:10001").describe("Teamily WebSocket URL"),
|
|
11
10
|
});
|
package/src/monitor.ts
CHANGED
|
@@ -36,6 +36,11 @@ async function loadSDK() {
|
|
|
36
36
|
* reconnection to the official OpenIM SDK. Also exposes send methods
|
|
37
37
|
* so outbound replies flow through the same WebSocket connection.
|
|
38
38
|
*/
|
|
39
|
+
/** Returns true when the string looks like an HTTP(S) URL rather than a local file path. */
|
|
40
|
+
function isHttpUrl(s: string): boolean {
|
|
41
|
+
return s.startsWith("http://") || s.startsWith("https://");
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
export class TeamilyMonitor {
|
|
40
45
|
private account: ResolvedTeamilyAccount;
|
|
41
46
|
private onMessage: TeamilyMessageHandler;
|
|
@@ -126,9 +131,29 @@ export class TeamilyMonitor {
|
|
|
126
131
|
return result.data?.serverMsgID || result.data?.clientMsgID || "";
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Upload a local file to the OpenIM server and return its download URL.
|
|
136
|
+
* Uses the SDK's built-in uploadFile which handles multipart upload to
|
|
137
|
+
* the server's object storage. Requires Node.js 20+ for global File.
|
|
138
|
+
*/
|
|
139
|
+
async uploadLocalFile(localPath: string, contentType?: string): Promise<string> {
|
|
140
|
+
const sdk = this.requireSdk();
|
|
141
|
+
const { readFileSync } = await import("node:fs");
|
|
142
|
+
const { basename, extname } = await import("node:path");
|
|
143
|
+
const buffer = readFileSync(localPath);
|
|
144
|
+
const fileName = basename(localPath);
|
|
145
|
+
const mime = contentType || guessMimeType(extname(localPath));
|
|
146
|
+
const file = new File([buffer], fileName, { type: mime });
|
|
147
|
+
const result = await sdk.uploadFile({ file, name: fileName, contentType: mime });
|
|
148
|
+
const url = result.data?.url;
|
|
149
|
+
if (!url) throw new Error(`Upload failed for ${localPath}: no URL returned`);
|
|
150
|
+
return url;
|
|
151
|
+
}
|
|
152
|
+
|
|
129
153
|
/** Send an image message through the SDK WebSocket connection. */
|
|
130
|
-
async sendImage(target: TeamilyMessageTarget,
|
|
154
|
+
async sendImage(target: TeamilyMessageTarget, urlOrPath: string): Promise<string> {
|
|
131
155
|
const sdk = this.requireSdk();
|
|
156
|
+
const url = isHttpUrl(urlOrPath) ? urlOrPath : await this.uploadLocalFile(urlOrPath);
|
|
132
157
|
const picInfo = { uuid: "", type: "", width: 0, height: 0, size: 0, url };
|
|
133
158
|
const created = await sdk.createImageMessageByURL({
|
|
134
159
|
sourcePicture: picInfo,
|
|
@@ -145,8 +170,9 @@ export class TeamilyMonitor {
|
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
/** Send a video message through the SDK WebSocket connection. */
|
|
148
|
-
async sendVideo(target: TeamilyMessageTarget,
|
|
173
|
+
async sendVideo(target: TeamilyMessageTarget, urlOrPath: string): Promise<string> {
|
|
149
174
|
const sdk = this.requireSdk();
|
|
175
|
+
const url = isHttpUrl(urlOrPath) ? urlOrPath : await this.uploadLocalFile(urlOrPath);
|
|
150
176
|
const created = await sdk.createVideoMessageByURL({
|
|
151
177
|
videoPath: "",
|
|
152
178
|
duration: 0,
|
|
@@ -170,8 +196,9 @@ export class TeamilyMonitor {
|
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
/** Send a sound/audio message through the SDK WebSocket connection. */
|
|
173
|
-
async sendAudio(target: TeamilyMessageTarget,
|
|
199
|
+
async sendAudio(target: TeamilyMessageTarget, urlOrPath: string): Promise<string> {
|
|
174
200
|
const sdk = this.requireSdk();
|
|
201
|
+
const url = isHttpUrl(urlOrPath) ? urlOrPath : await this.uploadLocalFile(urlOrPath);
|
|
175
202
|
const created = await sdk.createSoundMessageByURL({
|
|
176
203
|
uuid: "",
|
|
177
204
|
soundPath: "",
|
|
@@ -188,11 +215,12 @@ export class TeamilyMonitor {
|
|
|
188
215
|
}
|
|
189
216
|
|
|
190
217
|
/** Send a file message through the SDK WebSocket connection. */
|
|
191
|
-
async sendFile(target: TeamilyMessageTarget,
|
|
218
|
+
async sendFile(target: TeamilyMessageTarget, urlOrPath: string, fileName?: string): Promise<string> {
|
|
192
219
|
const sdk = this.requireSdk();
|
|
220
|
+
const url = isHttpUrl(urlOrPath) ? urlOrPath : await this.uploadLocalFile(urlOrPath);
|
|
193
221
|
const created = await sdk.createFileMessageByURL({
|
|
194
222
|
filePath: "",
|
|
195
|
-
fileName: fileName ||
|
|
223
|
+
fileName: fileName || urlOrPath.split("/").pop() || "file",
|
|
196
224
|
uuid: "",
|
|
197
225
|
sourceUrl: url,
|
|
198
226
|
fileSize: 0,
|
|
@@ -218,6 +246,32 @@ export class TeamilyMonitor {
|
|
|
218
246
|
}
|
|
219
247
|
}
|
|
220
248
|
|
|
249
|
+
// ---- MIME type helper ----
|
|
250
|
+
|
|
251
|
+
const MIME_MAP: Record<string, string> = {
|
|
252
|
+
".jpg": "image/jpeg",
|
|
253
|
+
".jpeg": "image/jpeg",
|
|
254
|
+
".png": "image/png",
|
|
255
|
+
".gif": "image/gif",
|
|
256
|
+
".webp": "image/webp",
|
|
257
|
+
".bmp": "image/bmp",
|
|
258
|
+
".mp4": "video/mp4",
|
|
259
|
+
".mov": "video/quicktime",
|
|
260
|
+
".webm": "video/webm",
|
|
261
|
+
".mp3": "audio/mpeg",
|
|
262
|
+
".m4a": "audio/mp4",
|
|
263
|
+
".wav": "audio/wav",
|
|
264
|
+
".ogg": "audio/ogg",
|
|
265
|
+
".pdf": "application/pdf",
|
|
266
|
+
".doc": "application/msword",
|
|
267
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
268
|
+
".zip": "application/zip",
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
function guessMimeType(ext: string): string {
|
|
272
|
+
return MIME_MAP[ext.toLowerCase()] || "application/octet-stream";
|
|
273
|
+
}
|
|
274
|
+
|
|
221
275
|
// ---- SDK message conversion helpers ----
|
|
222
276
|
|
|
223
277
|
import type { MessageItem } from "@openim/client-sdk";
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Core configuration types for Teamily channel
|
|
2
2
|
|
|
3
3
|
export interface TeamilyServerConfig {
|
|
4
|
-
platformUrl: string;
|
|
5
4
|
apiURL: string;
|
|
6
5
|
wsURL: string;
|
|
7
6
|
}
|
|
@@ -29,7 +28,6 @@ export interface TeamilyConfig {
|
|
|
29
28
|
export interface ResolvedTeamilyAccount {
|
|
30
29
|
accountId: string;
|
|
31
30
|
enabled: boolean;
|
|
32
|
-
platformUrl: string;
|
|
33
31
|
apiURL: string;
|
|
34
32
|
wsURL: string;
|
|
35
33
|
userID: string;
|