@ascegu/teamily 1.0.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ascegu/teamily",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
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
- await sendMessageTeamily({
95
- account,
96
- target,
97
- text: PAIRING_APPROVED_MESSAGE,
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
- return { ok: true, to: target.id };
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 account = resolveTeamilyAccount(ctx.cfg as CoreConfig, accountId);
162
+ const monitor = requireMonitor(accountId);
163
163
  const target = normalizeTeamilyTarget(to);
164
-
165
- const result = await sendMessageTeamily({
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, text, accountId } = ctx;
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 account = resolveTeamilyAccount(ctx.cfg as CoreConfig, accountId);
173
+ const monitor = requireMonitor(accountId);
185
174
  const target = normalizeTeamilyTarget(to);
186
175
 
187
- // Determine media type from URL or assume image
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
- mediaType = "video";
192
- } else if (
193
- urlLower.endsWith(".mp3") ||
194
- urlLower.endsWith(".m4a") ||
195
- urlLower.endsWith(".wav")
196
- ) {
197
- mediaType = "audio";
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
- const result = await sendMediaTeamily({
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 sendMessageTeamily({ account, target, text: replyText });
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,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 {