@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 +1 -1
- package/src/channel.ts +21 -5
- package/src/monitor.ts +34 -8
- package/src/upload.ts +1 -1
- package/src/send.ts +0 -271
package/package.json
CHANGED
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 {
|
|
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,
|
|
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 =
|
|
377
|
-
|
|
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(
|
|
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 = {
|
|
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(
|
|
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(
|
|
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(
|
|
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) ?
|
|
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
|
|
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
|
-
}
|