@ascegu/teamily 1.0.23 → 1.0.24

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.23",
3
+ "version": "1.0.24",
4
4
  "description": "OpenClaw Teamily channel plugin - Team instant messaging server integration",
5
5
  "keywords": [
6
6
  "channel",
package/src/channel.ts CHANGED
@@ -30,7 +30,12 @@ import {
30
30
  } from "./accounts.js";
31
31
  import { TeamilyConfigSchema } from "./config-schema.js";
32
32
  import type { CoreConfig } from "./config-schema.js";
33
- import { getTeamilyMonitor, startTeamilyMonitoring, stopTeamilyMonitoring, type TeamilyMonitor } from "./monitor.js";
33
+ import {
34
+ getTeamilyMonitor,
35
+ startTeamilyMonitoring,
36
+ stopTeamilyMonitoring,
37
+ type TeamilyMonitor,
38
+ } from "./monitor.js";
34
39
  import { normalizeTeamilyTarget, normalizeTeamilyAllowEntry } from "./normalize.js";
35
40
  import { probeTeamily } from "./probe.js";
36
41
  import { getTeamilyRuntime } from "./runtime.js";
@@ -65,6 +70,11 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
65
70
  threads: false,
66
71
  polls: false,
67
72
  },
73
+ agentPrompt: {
74
+ messageToolHints: () => [
75
+ "- To send a local file or image to the user, use MEDIA:./relative-path in your reply (e.g. MEDIA:./image.png). The path must be relative to the workspace. You can also use the message tool with media/path/filePath. Avoid absolute paths and ~ paths — they are blocked for security.",
76
+ ],
77
+ },
68
78
  reload: { configPrefixes: ["channels.teamily"] },
69
79
  setup: {
70
80
  resolveAccountId: ({ accountId, input }) => {
@@ -192,7 +202,12 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
192
202
  const fileName = media.fileName || path.basename(mediaUrl) || "media";
193
203
  const contentType = media.contentType || guessContentType(mediaUrl);
194
204
  const messageId = await sendMediaBuffer(
195
- monitor, target, media.buffer, fileName, contentType, mediaUrl,
205
+ monitor,
206
+ target,
207
+ media.buffer,
208
+ fileName,
209
+ contentType,
210
+ mediaUrl,
196
211
  );
197
212
  return { channel: "teamily" as const, messageId };
198
213
  }
@@ -373,9 +388,10 @@ export const teamilyPlugin: ChannelPlugin<ResolvedTeamilyAccount> = {
373
388
  if (!monitor)
374
389
  throw new Error(`Teamily monitor not running for account ${accountId}`);
375
390
  const target = normalizeTeamilyTarget(replyTarget);
376
- const replyText = (payload as Record<string, unknown>)?.text as string | undefined
377
- ?? (payload as Record<string, unknown>)?.body as string | undefined
378
- ?? "";
391
+ const replyText =
392
+ ((payload as Record<string, unknown>)?.text as string | undefined) ??
393
+ ((payload as Record<string, unknown>)?.body as string | undefined) ??
394
+ "";
379
395
 
380
396
  // Send media attachments (first with caption, rest without).
381
397
  const mediaUrls = resolveOutboundMediaUrls(payload as Record<string, unknown>);
package/src/monitor.ts CHANGED
@@ -209,10 +209,22 @@ export class TeamilyMonitor {
209
209
  // Like Telegram's InputFile(buffer), pass a buffer and let the SDK upload.
210
210
 
211
211
  /** Send an image from a local buffer. SDK handles the upload to object storage. */
212
- async sendImageBuffer(target: TeamilyMessageTarget, buffer: Buffer, fileName: string, contentType: string): Promise<string> {
212
+ async sendImageBuffer(
213
+ target: TeamilyMessageTarget,
214
+ buffer: Buffer,
215
+ fileName: string,
216
+ contentType: string,
217
+ ): Promise<string> {
213
218
  const sdk = this.requireSdk();
214
219
  const file = new File([new Uint8Array(buffer)], fileName, { type: contentType });
215
- const picInfo = { uuid: "", type: contentType, width: 0, height: 0, size: buffer.length, url: "" };
220
+ const picInfo = {
221
+ uuid: "",
222
+ type: contentType,
223
+ width: 0,
224
+ height: 0,
225
+ size: buffer.length,
226
+ url: "",
227
+ };
216
228
  const created = await sdk.createImageMessageByFile({
217
229
  sourcePicture: picInfo,
218
230
  bigPicture: picInfo,
@@ -229,7 +241,12 @@ export class TeamilyMonitor {
229
241
  }
230
242
 
231
243
  /** Send a video from a local buffer. SDK handles the upload to object storage. */
232
- async sendVideoBuffer(target: TeamilyMessageTarget, buffer: Buffer, fileName: string, contentType: string): Promise<string> {
244
+ async sendVideoBuffer(
245
+ target: TeamilyMessageTarget,
246
+ buffer: Buffer,
247
+ fileName: string,
248
+ contentType: string,
249
+ ): Promise<string> {
233
250
  const sdk = this.requireSdk();
234
251
  const videoFile = new File([new Uint8Array(buffer)], fileName, { type: contentType });
235
252
  // snapshotFile is required by the SDK type but we have no thumbnail; use an empty file.
@@ -259,7 +276,12 @@ export class TeamilyMonitor {
259
276
  }
260
277
 
261
278
  /** Send an audio/sound from a local buffer. SDK handles the upload to object storage. */
262
- async sendAudioBuffer(target: TeamilyMessageTarget, buffer: Buffer, fileName: string, contentType: string): Promise<string> {
279
+ async sendAudioBuffer(
280
+ target: TeamilyMessageTarget,
281
+ buffer: Buffer,
282
+ fileName: string,
283
+ contentType: string,
284
+ ): Promise<string> {
263
285
  const sdk = this.requireSdk();
264
286
  const file = new File([new Uint8Array(buffer)], fileName, { type: contentType });
265
287
  const created = await sdk.createSoundMessageByFile({
@@ -279,7 +301,12 @@ export class TeamilyMonitor {
279
301
  }
280
302
 
281
303
  /** Send a document/file from a local buffer. SDK handles the upload to object storage. */
282
- async sendFileBuffer(target: TeamilyMessageTarget, buffer: Buffer, fileName: string, contentType: string): Promise<string> {
304
+ async sendFileBuffer(
305
+ target: TeamilyMessageTarget,
306
+ buffer: Buffer,
307
+ fileName: string,
308
+ contentType: string,
309
+ ): Promise<string> {
283
310
  const sdk = this.requireSdk();
284
311
  const file = new File([new Uint8Array(buffer)], fileName, { type: contentType });
285
312
  const created = await sdk.createFileMessageByFile({
@@ -328,13 +355,12 @@ function convertSdkMessage(msg: MessageItem, selfUserID: string): TeamilyMessage
328
355
  // Determine whether this message @-mentions the bot
329
356
  const isAtSelf =
330
357
  contentType === CONTENT_TYPES.AT_TEXT &&
331
- (msg.atTextElem?.isAtSelf === true ||
332
- (msg.atTextElem?.atUserList ?? []).includes(selfUserID));
358
+ (msg.atTextElem?.isAtSelf === true || (msg.atTextElem?.atUserList ?? []).includes(selfUserID));
333
359
 
334
360
  return {
335
361
  serverMsgID: msg.serverMsgID || msg.clientMsgID || `${Date.now()}_${Math.random()}`,
336
362
  sendID: msg.sendID || "unknown",
337
- recvID: isGroupSession(sessionType) ? (msg.groupID || "") : (msg.recvID || selfUserID),
363
+ recvID: isGroupSession(sessionType) ? msg.groupID || "" : msg.recvID || selfUserID,
338
364
  content,
339
365
  contentType,
340
366
  sessionType,
package/src/upload.ts CHANGED
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
 
3
3
  /** Returns true if the media URL is a local file path rather than a remote URL. */
4
4
  export function isLocalMediaPath(mediaUrl: string): boolean {
5
- return !(/^https?:\/\//i.test(mediaUrl));
5
+ return !/^https?:\/\//i.test(mediaUrl);
6
6
  }
7
7
 
8
8
  /** Detect the media category from a file extension. */
package/src/send.ts DELETED
@@ -1,271 +0,0 @@
1
- import { generateOperationID } from "./probe.js";
2
- import type { ResolvedTeamilyAccount, TeamilyMessageTarget } from "./types.js";
3
- import { CONTENT_TYPES, SESSION_TYPES } from "./types.js";
4
-
5
- export interface SendTeamilyMessageParams {
6
- account: ResolvedTeamilyAccount;
7
- target: TeamilyMessageTarget;
8
- text: string;
9
- replyToId?: string;
10
- fetchImpl?: typeof fetch;
11
- }
12
-
13
- export interface SendTeamilyMediaParams {
14
- account: ResolvedTeamilyAccount;
15
- target: TeamilyMessageTarget;
16
- mediaUrl: string;
17
- mediaType: "image" | "video" | "audio" | "file";
18
- caption?: string;
19
- fetchImpl?: typeof fetch;
20
- }
21
-
22
- export interface TeamilySendResult {
23
- success: boolean;
24
- messageId?: string;
25
- serverMsgID?: string;
26
- clientMsgID?: string;
27
- error?: string;
28
- }
29
-
30
- /**
31
- * Send a text message via Teamily REST API.
32
- *
33
- * @param params - Send parameters
34
- * @returns Send result with message ID or error
35
- */
36
- export async function sendMessageTeamily(
37
- params: SendTeamilyMessageParams,
38
- ): Promise<TeamilySendResult> {
39
- const { account, target, text, replyToId, fetchImpl = fetch } = params;
40
-
41
- const url = `${account.apiURL}/msg/send_msg`;
42
-
43
- const payload = buildSendMessagePayload({
44
- sendID: account.userID,
45
- target,
46
- content: { content: text },
47
- contentType: CONTENT_TYPES.TEXT,
48
- replyToId,
49
- });
50
-
51
- try {
52
- const response = await fetchImpl(url, {
53
- method: "POST",
54
- headers: {
55
- "Content-Type": "application/json",
56
- operationID: generateOperationID(),
57
- token: account.token,
58
- },
59
- body: JSON.stringify(payload),
60
- });
61
-
62
- if (!response.ok) {
63
- const errorText = await response.text();
64
- return {
65
- success: false,
66
- error: `HTTP ${response.status}: ${errorText}`,
67
- };
68
- }
69
-
70
- const data = (await response.json()) as {
71
- errCode: number;
72
- errMsg: string;
73
- data?: {
74
- serverMsgID: string;
75
- clientMsgID: string;
76
- sendTime: number;
77
- };
78
- };
79
-
80
- if (data.errCode !== 0) {
81
- return {
82
- success: false,
83
- error: data.errMsg || "Send failed",
84
- };
85
- }
86
-
87
- return {
88
- success: true,
89
- messageId: data.data?.serverMsgID,
90
- serverMsgID: data.data?.serverMsgID,
91
- clientMsgID: data.data?.clientMsgID,
92
- };
93
- } catch (error) {
94
- return {
95
- success: false,
96
- error: error instanceof Error ? error.message : String(error),
97
- };
98
- }
99
- }
100
-
101
- /**
102
- * Send a media message via Teamily REST API.
103
- *
104
- * @param params - Send parameters
105
- * @returns Send result with message ID or error
106
- */
107
- export async function sendMediaTeamily(params: SendTeamilyMediaParams): Promise<TeamilySendResult> {
108
- const { account, target, mediaUrl, mediaType, caption, fetchImpl = fetch } = params;
109
-
110
- const url = `${account.apiURL}/msg/send_msg`;
111
-
112
- let content: Record<string, unknown>;
113
- let contentType: number;
114
-
115
- switch (mediaType) {
116
- case "image":
117
- contentType = CONTENT_TYPES.PICTURE;
118
- content = {
119
- sourcePicture: {
120
- uuid: generateOperationID(),
121
- type: "public",
122
- width: 0,
123
- height: 0,
124
- url: mediaUrl,
125
- },
126
- };
127
- break;
128
-
129
- case "video":
130
- contentType = CONTENT_TYPES.VIDEO;
131
- content = {
132
- videoPath: mediaUrl,
133
- videoUUID: generateOperationID(),
134
- videoUrl: mediaUrl,
135
- videoType: "mp4",
136
- videoSize: 0,
137
- duration: 0,
138
- snapshotUUID: `${generateOperationID()}_snap`,
139
- snapshotUrl: "",
140
- snapshotSize: 0,
141
- snapshotWidth: 0,
142
- snapshotHeight: 0,
143
- };
144
- break;
145
-
146
- case "audio":
147
- contentType = CONTENT_TYPES.VOICE;
148
- content = {
149
- uuid: generateOperationID(),
150
- soundPath: mediaUrl,
151
- sourceUrl: mediaUrl,
152
- dataSize: 0,
153
- duration: 0,
154
- soundType: "mp3",
155
- };
156
- break;
157
-
158
- case "file":
159
- contentType = CONTENT_TYPES.FILE;
160
- content = {
161
- uuid: generateOperationID(),
162
- fileName: mediaUrl.split("/").pop() || "file",
163
- fileSize: 0,
164
- sourceUrl: mediaUrl,
165
- fileType: "application/octet-stream",
166
- };
167
- break;
168
-
169
- default:
170
- return {
171
- success: false,
172
- error: `Unsupported media type: ${mediaType}`,
173
- };
174
- }
175
-
176
- // Add caption to text content for media messages
177
- if (caption) {
178
- content.text = caption;
179
- }
180
-
181
- const payload = buildSendMessagePayload({
182
- sendID: account.userID,
183
- target,
184
- content,
185
- contentType,
186
- });
187
-
188
- try {
189
- const response = await fetchImpl(url, {
190
- method: "POST",
191
- headers: {
192
- "Content-Type": "application/json",
193
- operationID: generateOperationID(),
194
- token: account.token,
195
- },
196
- body: JSON.stringify(payload),
197
- });
198
-
199
- if (!response.ok) {
200
- const errorText = await response.text();
201
- return {
202
- success: false,
203
- error: `HTTP ${response.status}: ${errorText}`,
204
- };
205
- }
206
-
207
- const data = (await response.json()) as {
208
- errCode: number;
209
- errMsg: string;
210
- data?: {
211
- serverMsgID: string;
212
- clientMsgID: string;
213
- sendTime: number;
214
- };
215
- };
216
-
217
- if (data.errCode !== 0) {
218
- return {
219
- success: false,
220
- error: data.errMsg || "Send failed",
221
- };
222
- }
223
-
224
- return {
225
- success: true,
226
- messageId: data.data?.serverMsgID,
227
- serverMsgID: data.data?.serverMsgID,
228
- clientMsgID: data.data?.clientMsgID,
229
- };
230
- } catch (error) {
231
- return {
232
- success: false,
233
- error: error instanceof Error ? error.message : String(error),
234
- };
235
- }
236
- }
237
-
238
- /**
239
- * Build the send message payload for Teamily API.
240
- */
241
- function buildSendMessagePayload(params: {
242
- sendID: string;
243
- target: TeamilyMessageTarget;
244
- content: Record<string, unknown>;
245
- contentType: number;
246
- replyToId?: string;
247
- }): Record<string, unknown> {
248
- const { sendID, target, content, contentType, replyToId } = params;
249
-
250
- const payload: Record<string, unknown> = {
251
- sendID,
252
- recvID: target.type === "user" ? target.id : "",
253
- groupID: target.type === "group" ? target.id : "",
254
- content,
255
- contentType,
256
- sessionType: target.type === "group" ? SESSION_TYPES.GROUP : SESSION_TYPES.SINGLE,
257
- isOnlineOnly: false,
258
- };
259
-
260
- if (replyToId) {
261
- payload.quote = {
262
- text: "",
263
- content: {},
264
- isReact: false,
265
- userID: "",
266
- msgID: replyToId,
267
- };
268
- }
269
-
270
- return payload;
271
- }