@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 CHANGED
@@ -26,29 +26,28 @@ openclaw channel configure teamily
26
26
 
27
27
  ### Server Settings
28
28
 
29
- | Field | Description | Default |
30
- |---------------|--------------------------|---------------------------|
31
- | `platformUrl` | Teamily platform URL | `https://imserver-test.teamily.ai/im_api` |
32
- | `apiURL` | Teamily REST API URL | `https://imserver-test.teamily.ai/im_api` |
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 | Description |
49
- |--------------------|------------------------------------------------------|
50
- | `dm.policy` | DM security policy (`pairing`, `allowlist`, `open`) |
51
- | `dm.allowFrom` | List of allowed sender IDs |
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 | Type |
95
- |------------------------------|-------|
96
- | `.jpg`, `.png`, `.gif`, etc. | Image |
97
- | `.mp4`, `.mov`, `.webm` | Video |
98
- | `.mp3`, `.m4a`, `.wav` | Audio |
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 | Supported |
111
- |---------------------------|-----------|
112
- | Direct messaging | Yes |
113
- | Group messaging | Yes |
114
- | Text messages | Yes |
115
- | Media (image/video/audio/file) | Yes |
116
- | @-mention gating (groups) | Yes |
117
- | WebSocket real-time monitoring | Yes |
118
- | Automatic reconnection | Yes |
119
- | Connection health probes | Yes |
120
- | Reactions | No |
121
- | Threads | No |
122
- | Polls | No |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascegu/teamily",
3
- "version": "1.0.19",
3
+ "version": "1.0.22",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
package/src/accounts.ts CHANGED
@@ -38,7 +38,6 @@ export function resolveTeamilyAccount(
38
38
  return {
39
39
  accountId: targetAccountId,
40
40
  enabled: true,
41
- platformUrl: config.server.platformUrl,
42
41
  apiURL: config.server.apiURL,
43
42
  wsURL: config.server.wsURL,
44
43
  userID: account.userID,
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 (urlLower.endsWith(".mp3") || urlLower.endsWith(".m4a") || urlLower.endsWith(".wav")) {
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 (urlLower.endsWith(".pdf") || urlLower.endsWith(".doc") || urlLower.endsWith(".docx") || urlLower.endsWith(".zip")) {
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
- const rt = getTeamilyRuntime();
252
- const currentCfg = rt.config.loadConfig();
260
+ const rt = getTeamilyRuntime();
261
+ const currentCfg = rt.config.loadConfig();
253
262
 
254
- const isGroup = isGroupSession(message.sessionType);
255
- const from = message.sendID;
256
- const rawText = message.content?.text || "";
263
+ const isGroup = isGroupSession(message.sessionType);
264
+ const from = message.sendID;
265
+ const rawText = message.content?.text || "";
257
266
 
258
- log?.info?.(
259
- `[${accountId}] Incoming message: sessionType=${message.sessionType}, isGroup=${isGroup}, ` +
260
- `from=${from}, recvID=${message.recvID}, isAtSelf=${message.isAtSelf ?? false}`,
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
- const historyKey = isGroup ? `teamily:group:${message.recvID}` : undefined;
272
+ const historyKey = isGroup ? `teamily:group:${message.recvID}` : undefined;
264
273
 
265
- // In group chats, buffer non-@-mention text-only messages for context.
266
- // Media messages (picture/video/audio) are always dispatched — OpenIM sends
267
- // @-mention text and media as separate messages, so a PICTURE following an
268
- // AT_TEXT won't have isAtSelf=true. Dispatching media keeps group image
269
- // handling consistent with DM behavior.
270
- const hasMedia = !!(
271
- message.content?.picture ||
272
- message.content?.video ||
273
- message.content?.audio
274
- );
275
- if (isGroup && !message.isAtSelf && !hasMedia) {
276
- if (historyKey && rawText) {
277
- recordPendingHistoryEntryIfEnabled({
278
- historyMap: groupHistories,
279
- historyKey,
280
- limit: historyLimit,
281
- entry: {
282
- sender: from,
283
- body: rawText,
284
- timestamp: message.sendTime,
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
- const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
300
+ const sessionKey = isGroup ? `teamily:group:${message.recvID}` : `teamily:${from}`;
292
301
 
293
- let mediaUrl: string | undefined;
294
- if (message.content?.picture?.sourcePicture?.url) {
295
- mediaUrl = message.content.picture.sourcePicture.url;
296
- } else if (message.content?.video?.videoUrl) {
297
- mediaUrl = message.content.video.videoUrl;
298
- } else if (message.content?.audio?.sourceUrl) {
299
- mediaUrl = message.content.audio.sourceUrl;
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
- // Download remote media to a local temp file so the agent recognises
303
- // image-only messages (hasMediaAttachment checks MediaPath, not MediaUrl).
304
- let mediaPath: string | undefined;
305
- let mediaType: string | undefined;
306
- if (mediaUrl) {
307
- try {
308
- const fetched = await rt.channel.media.fetchRemoteMedia({ url: mediaUrl, maxBytes: MEDIA_MAX_BYTES });
309
- const saved = await rt.channel.media.saveMediaBuffer(
310
- fetched.buffer,
311
- fetched.contentType,
312
- "inbound",
313
- MEDIA_MAX_BYTES,
314
- );
315
- mediaPath = saved.path;
316
- mediaType = saved.contentType;
317
- } catch (err) {
318
- log?.warn?.(`[${accountId}] Failed to download Teamily media: ${String(err)}`);
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
- // For group @-mention messages, prepend buffered history as context.
323
- const body =
324
- isGroup && historyKey
325
- ? buildPendingHistoryContextFromMap({
326
- historyMap: groupHistories,
327
- historyKey,
328
- limit: historyLimit,
329
- currentMessage: rawText,
330
- formatEntry: (entry) => `${entry.sender}: ${entry.body}`,
331
- })
332
- : rawText;
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
- const replyTarget = isGroup ? `group:${message.recvID}` : from;
335
- const msgCtx = {
336
- Body: body,
337
- From: from,
338
- To: account.userID,
339
- SessionKey: sessionKey,
340
- AccountId: accountId,
341
- Provider: "teamily" as const,
342
- Surface: "teamily" as const,
343
- OriginatingChannel: "teamily" as const,
344
- OriginatingTo: replyTarget,
345
- ChatType: isGroup ? "group" : "direct",
346
- MediaUrl: mediaUrl,
347
- MediaPath: mediaPath,
348
- MediaType: mediaType,
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
- log?.info?.(
352
- `[${accountId}] Dispatching reply: sessionKey=${sessionKey}, chatType=${isGroup ? "group" : "direct"}, replyTarget=${replyTarget}`,
353
- );
363
+ log?.info?.(
364
+ `[${accountId}] Dispatching reply: sessionKey=${sessionKey}, chatType=${isGroup ? "group" : "direct"}, replyTarget=${replyTarget}`,
365
+ );
354
366
 
355
- await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
356
- ctx: msgCtx,
357
- cfg: currentCfg,
358
- dispatcherOptions: {
359
- deliver: async (payload: { text?: string; body?: string }) => {
360
- const replyText = payload?.text ?? payload?.body;
361
- if (replyText) {
362
- const monitor = getTeamilyMonitor(accountId);
363
- if (!monitor) throw new Error(`Teamily monitor not running for account ${accountId}`);
364
- log?.info?.(`[${accountId}] Sending reply to: ${replyTarget} (isGroup=${isGroup})`);
365
- const target = normalizeTeamilyTarget(replyTarget);
366
- await monitor.sendText(target, replyText);
367
- }
368
- },
369
- onReplyStart: () => {
370
- log?.info?.(`Agent reply started for ${from}`);
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
- log?.info?.(`[${accountId}] Dispatch completed for ${sessionKey}`);
390
+ log?.info?.(`[${accountId}] Dispatch completed for ${sessionKey}`);
376
391
 
377
- // Clear history buffer after dispatch so context doesn't repeat.
378
- if (isGroup && historyKey) {
379
- clearHistoryEntriesIfEnabled({
380
- historyMap: groupHistories,
381
- historyKey,
382
- limit: historyLimit,
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 ?? { platformUrl: "", apiURL: "", wsURL: "" };
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(`Teamily gateway not running for account "${id}" — outbound requires an active gateway`);
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
  }
@@ -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, url: string): Promise<string> {
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, url: string): Promise<string> {
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, url: string): Promise<string> {
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, url: string, fileName?: string): Promise<string> {
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 || url.split("/").pop() || "file",
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;