botmsg 0.1.0
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 +984 -0
- package/dist/index.js +2617 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2617 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// src/config/loader.ts
|
|
12
|
+
import { readFileSync, existsSync } from "fs";
|
|
13
|
+
import { extname } from "path";
|
|
14
|
+
import YAML from "yaml";
|
|
15
|
+
|
|
16
|
+
// src/config/schema.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
var PlatformEnum = z.enum([
|
|
19
|
+
"dingtalk",
|
|
20
|
+
"wecom",
|
|
21
|
+
"feishu",
|
|
22
|
+
"lark",
|
|
23
|
+
"slack",
|
|
24
|
+
"discord",
|
|
25
|
+
"telegram"
|
|
26
|
+
]);
|
|
27
|
+
var DingTalkBotSchema = z.object({
|
|
28
|
+
platform: z.literal("dingtalk"),
|
|
29
|
+
webhook: z.string().url(),
|
|
30
|
+
secret: z.string().optional(),
|
|
31
|
+
security: z.enum(["keyword", "ip", "sign"]).default("keyword")
|
|
32
|
+
});
|
|
33
|
+
var WeComBotSchema = z.object({
|
|
34
|
+
platform: z.literal("wecom"),
|
|
35
|
+
webhook: z.string().url()
|
|
36
|
+
});
|
|
37
|
+
var FeishuBotSchema = z.object({
|
|
38
|
+
platform: z.literal("feishu"),
|
|
39
|
+
webhook: z.string().url(),
|
|
40
|
+
secret: z.string().optional()
|
|
41
|
+
});
|
|
42
|
+
var LarkBotSchema = z.object({
|
|
43
|
+
platform: z.literal("lark"),
|
|
44
|
+
webhook: z.string().url(),
|
|
45
|
+
secret: z.string().optional()
|
|
46
|
+
});
|
|
47
|
+
var SlackBotSchema = z.object({
|
|
48
|
+
platform: z.literal("slack"),
|
|
49
|
+
webhook: z.string().url()
|
|
50
|
+
});
|
|
51
|
+
var DiscordBotSchema = z.object({
|
|
52
|
+
platform: z.literal("discord"),
|
|
53
|
+
webhook: z.string().url(),
|
|
54
|
+
username: z.string().optional(),
|
|
55
|
+
avatar_url: z.string().url().optional()
|
|
56
|
+
});
|
|
57
|
+
var TelegramBotSchema = z.object({
|
|
58
|
+
platform: z.literal("telegram"),
|
|
59
|
+
token: z.string(),
|
|
60
|
+
chat_id: z.string(),
|
|
61
|
+
parse_mode: z.enum(["MarkdownV2", "HTML", ""]).default("")
|
|
62
|
+
});
|
|
63
|
+
var BotConfigSchema = z.discriminatedUnion("platform", [
|
|
64
|
+
DingTalkBotSchema,
|
|
65
|
+
WeComBotSchema,
|
|
66
|
+
FeishuBotSchema,
|
|
67
|
+
LarkBotSchema,
|
|
68
|
+
SlackBotSchema,
|
|
69
|
+
DiscordBotSchema,
|
|
70
|
+
TelegramBotSchema
|
|
71
|
+
]);
|
|
72
|
+
var SettingsSchema = z.object({
|
|
73
|
+
log_level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
74
|
+
audit_log: z.string().optional(),
|
|
75
|
+
max_retries: z.number().int().min(0).max(10).default(3),
|
|
76
|
+
dry_run: z.boolean().default(false)
|
|
77
|
+
});
|
|
78
|
+
var FullConfigSchema = z.object({
|
|
79
|
+
bots: z.record(z.string(), BotConfigSchema),
|
|
80
|
+
settings: SettingsSchema.default({})
|
|
81
|
+
});
|
|
82
|
+
var MessageTypeEnum = z.enum([
|
|
83
|
+
"text",
|
|
84
|
+
"markdown",
|
|
85
|
+
"link",
|
|
86
|
+
"image",
|
|
87
|
+
"card",
|
|
88
|
+
"news",
|
|
89
|
+
"file"
|
|
90
|
+
]);
|
|
91
|
+
var AtSchema = z.object({
|
|
92
|
+
users: z.array(z.string()).optional(),
|
|
93
|
+
mobiles: z.array(z.string()).optional(),
|
|
94
|
+
all: z.boolean().default(false)
|
|
95
|
+
});
|
|
96
|
+
var LinkContentSchema = z.object({
|
|
97
|
+
title: z.string(),
|
|
98
|
+
text: z.string(),
|
|
99
|
+
url: z.string().url(),
|
|
100
|
+
pic_url: z.string().url().optional()
|
|
101
|
+
});
|
|
102
|
+
var CardButtonSchema = z.object({
|
|
103
|
+
title: z.string(),
|
|
104
|
+
url: z.string().url()
|
|
105
|
+
});
|
|
106
|
+
var CardContentSchema = z.object({
|
|
107
|
+
title: z.string(),
|
|
108
|
+
text: z.string(),
|
|
109
|
+
buttons: z.array(CardButtonSchema).optional(),
|
|
110
|
+
single_button_text: z.string().optional(),
|
|
111
|
+
single_button_url: z.string().url().optional()
|
|
112
|
+
});
|
|
113
|
+
var NewsItemSchema = z.object({
|
|
114
|
+
title: z.string(),
|
|
115
|
+
description: z.string().optional(),
|
|
116
|
+
url: z.string().url(),
|
|
117
|
+
pic_url: z.string().url().optional()
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// src/errors.ts
|
|
121
|
+
var BotMsgError = class extends Error {
|
|
122
|
+
constructor(message, code, platform, details) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.code = code;
|
|
125
|
+
this.platform = platform;
|
|
126
|
+
this.details = details;
|
|
127
|
+
this.name = "BotMsgError";
|
|
128
|
+
}
|
|
129
|
+
code;
|
|
130
|
+
platform;
|
|
131
|
+
details;
|
|
132
|
+
};
|
|
133
|
+
var ConfigError = class extends BotMsgError {
|
|
134
|
+
constructor(message) {
|
|
135
|
+
super(message, "CONFIG_ERROR");
|
|
136
|
+
this.name = "ConfigError";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var PlatformError = class extends BotMsgError {
|
|
140
|
+
constructor(message, platform, details) {
|
|
141
|
+
super(message, "PLATFORM_ERROR", platform, details);
|
|
142
|
+
this.name = "PlatformError";
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
var BotNotFoundError = class extends BotMsgError {
|
|
146
|
+
constructor(botName) {
|
|
147
|
+
super(`Bot "${botName}" not found in configuration`, "BOT_NOT_FOUND");
|
|
148
|
+
this.name = "BotNotFoundError";
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// src/logger.ts
|
|
153
|
+
var LOG_LEVELS = {
|
|
154
|
+
debug: 0,
|
|
155
|
+
info: 1,
|
|
156
|
+
warn: 2,
|
|
157
|
+
error: 3
|
|
158
|
+
};
|
|
159
|
+
var Logger = class {
|
|
160
|
+
level;
|
|
161
|
+
constructor(level = "info") {
|
|
162
|
+
this.level = LOG_LEVELS[level];
|
|
163
|
+
}
|
|
164
|
+
setLevel(level) {
|
|
165
|
+
this.level = LOG_LEVELS[level];
|
|
166
|
+
}
|
|
167
|
+
log(level, message, ctx) {
|
|
168
|
+
if (LOG_LEVELS[level] < this.level) return;
|
|
169
|
+
const entry = {
|
|
170
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
171
|
+
level: level.toUpperCase(),
|
|
172
|
+
message,
|
|
173
|
+
...ctx
|
|
174
|
+
};
|
|
175
|
+
console.error(JSON.stringify(entry));
|
|
176
|
+
}
|
|
177
|
+
debug(message, ctx) {
|
|
178
|
+
this.log("debug", message, ctx);
|
|
179
|
+
}
|
|
180
|
+
info(message, ctx) {
|
|
181
|
+
this.log("info", message, ctx);
|
|
182
|
+
}
|
|
183
|
+
warn(message, ctx) {
|
|
184
|
+
this.log("warn", message, ctx);
|
|
185
|
+
}
|
|
186
|
+
error(message, ctx) {
|
|
187
|
+
this.log("error", message, ctx);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var logger = new Logger(
|
|
191
|
+
process.env.BOTMSG_LOG_LEVEL || "info"
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// src/config/loader.ts
|
|
195
|
+
var PLATFORM_NAMES = [
|
|
196
|
+
"dingtalk",
|
|
197
|
+
"wecom",
|
|
198
|
+
"feishu",
|
|
199
|
+
"lark",
|
|
200
|
+
"slack",
|
|
201
|
+
"discord",
|
|
202
|
+
"telegram"
|
|
203
|
+
];
|
|
204
|
+
function parseEnvBots() {
|
|
205
|
+
const bots = {};
|
|
206
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
207
|
+
if (!key.startsWith("BOTMSG_") || !value) continue;
|
|
208
|
+
const parts = key.slice("BOTMSG_".length).split("_");
|
|
209
|
+
if (parts.length < 3) continue;
|
|
210
|
+
const platform = parts[0].toLowerCase();
|
|
211
|
+
if (!PLATFORM_NAMES.includes(platform)) continue;
|
|
212
|
+
if (["LOG", "AUDIT", "CONFIG", "DRY", "MAX"].includes(parts[0])) continue;
|
|
213
|
+
const instance = parts[1].toLowerCase();
|
|
214
|
+
const field = parts.slice(2).join("_").toLowerCase();
|
|
215
|
+
const botName = `${platform}-${instance}`;
|
|
216
|
+
if (!bots[botName]) bots[botName] = {};
|
|
217
|
+
bots[botName].platform = platform;
|
|
218
|
+
bots[botName][field] = value;
|
|
219
|
+
}
|
|
220
|
+
const result = {};
|
|
221
|
+
for (const [name, raw] of Object.entries(bots)) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = BotConfigSchema.parse(raw);
|
|
224
|
+
result[name] = parsed;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
logger.warn(`Skipping invalid bot config for "${name}"`, {
|
|
227
|
+
error: err instanceof Error ? err.message : String(err),
|
|
228
|
+
raw
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
function parseEnvSettings() {
|
|
235
|
+
const settings = {};
|
|
236
|
+
if (process.env.BOTMSG_LOG_LEVEL) {
|
|
237
|
+
settings.log_level = process.env.BOTMSG_LOG_LEVEL;
|
|
238
|
+
}
|
|
239
|
+
if (process.env.BOTMSG_AUDIT_LOG) {
|
|
240
|
+
settings.audit_log = process.env.BOTMSG_AUDIT_LOG;
|
|
241
|
+
}
|
|
242
|
+
if (process.env.BOTMSG_MAX_RETRIES) {
|
|
243
|
+
settings.max_retries = parseInt(process.env.BOTMSG_MAX_RETRIES, 10);
|
|
244
|
+
}
|
|
245
|
+
if (process.env.BOTMSG_DRY_RUN) {
|
|
246
|
+
settings.dry_run = process.env.BOTMSG_DRY_RUN === "true";
|
|
247
|
+
}
|
|
248
|
+
return settings;
|
|
249
|
+
}
|
|
250
|
+
function loadConfigFile(filePath) {
|
|
251
|
+
if (!existsSync(filePath)) {
|
|
252
|
+
throw new ConfigError(`Config file not found: ${filePath}`);
|
|
253
|
+
}
|
|
254
|
+
const content = readFileSync(filePath, "utf-8");
|
|
255
|
+
const ext = extname(filePath).toLowerCase();
|
|
256
|
+
let raw;
|
|
257
|
+
if (ext === ".yaml" || ext === ".yml") {
|
|
258
|
+
raw = YAML.parse(content);
|
|
259
|
+
} else if (ext === ".json") {
|
|
260
|
+
raw = JSON.parse(content);
|
|
261
|
+
} else {
|
|
262
|
+
throw new ConfigError(`Unsupported config file format: ${ext}`);
|
|
263
|
+
}
|
|
264
|
+
return FullConfigSchema.parse(raw);
|
|
265
|
+
}
|
|
266
|
+
function loadConfig() {
|
|
267
|
+
const configPath = process.env.BOTMSG_CONFIG;
|
|
268
|
+
if (configPath) {
|
|
269
|
+
try {
|
|
270
|
+
const fileConfig = loadConfigFile(configPath);
|
|
271
|
+
logger.info(`Loaded config from file: ${configPath}`, {
|
|
272
|
+
botCount: Object.keys(fileConfig.bots).length
|
|
273
|
+
});
|
|
274
|
+
const envBots = parseEnvBots();
|
|
275
|
+
const mergedBots = { ...envBots, ...fileConfig.bots };
|
|
276
|
+
const envSettings = parseEnvSettings();
|
|
277
|
+
const mergedSettings = SettingsSchema.parse({
|
|
278
|
+
...envSettings,
|
|
279
|
+
...fileConfig.settings
|
|
280
|
+
});
|
|
281
|
+
return { bots: mergedBots, settings: mergedSettings };
|
|
282
|
+
} catch (err) {
|
|
283
|
+
if (err instanceof ConfigError) throw err;
|
|
284
|
+
logger.warn(`Failed to parse config file, falling back to env vars`, {
|
|
285
|
+
error: err instanceof Error ? err.message : String(err)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const bots = parseEnvBots();
|
|
290
|
+
const settings = SettingsSchema.parse(parseEnvSettings());
|
|
291
|
+
logger.info(`Loaded config from environment variables`, {
|
|
292
|
+
botCount: Object.keys(bots).length
|
|
293
|
+
});
|
|
294
|
+
return { bots, settings };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/platforms/dingtalk.ts
|
|
298
|
+
import { createHmac } from "crypto";
|
|
299
|
+
|
|
300
|
+
// src/platforms/base.ts
|
|
301
|
+
var PlatformAdapter = class {
|
|
302
|
+
config;
|
|
303
|
+
botName;
|
|
304
|
+
constructor(botName, config) {
|
|
305
|
+
this.botName = botName;
|
|
306
|
+
this.config = config;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Execute HTTP POST with error handling
|
|
310
|
+
*/
|
|
311
|
+
async httpPost(url, body, headers) {
|
|
312
|
+
const startTime = Date.now();
|
|
313
|
+
try {
|
|
314
|
+
const response = await fetch(url, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
"Content-Type": "application/json",
|
|
318
|
+
...headers
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify(body)
|
|
321
|
+
});
|
|
322
|
+
const text = await response.text();
|
|
323
|
+
let data;
|
|
324
|
+
try {
|
|
325
|
+
data = JSON.parse(text);
|
|
326
|
+
} catch {
|
|
327
|
+
data = text;
|
|
328
|
+
}
|
|
329
|
+
return { status: response.status, data };
|
|
330
|
+
} catch (err) {
|
|
331
|
+
const latencyMs = Date.now() - startTime;
|
|
332
|
+
throw new PlatformError(
|
|
333
|
+
`HTTP request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
334
|
+
this.platformName,
|
|
335
|
+
{ latencyMs }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Generate a unique message ID for audit tracking
|
|
341
|
+
*/
|
|
342
|
+
generateMessageId() {
|
|
343
|
+
const ts = Date.now().toString(36);
|
|
344
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
345
|
+
return `msg_${ts}_${rand}`;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Wrap send with timing and result normalization
|
|
349
|
+
*/
|
|
350
|
+
async withTiming(sendFn) {
|
|
351
|
+
const startTime = Date.now();
|
|
352
|
+
try {
|
|
353
|
+
const result = await sendFn();
|
|
354
|
+
return {
|
|
355
|
+
...result,
|
|
356
|
+
platform: this.platformName,
|
|
357
|
+
bot: this.botName,
|
|
358
|
+
latencyMs: Date.now() - startTime
|
|
359
|
+
};
|
|
360
|
+
} catch (err) {
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
platform: this.platformName,
|
|
364
|
+
bot: this.botName,
|
|
365
|
+
latencyMs: Date.now() - startTime,
|
|
366
|
+
error: err instanceof Error ? err.message : String(err)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/platforms/dingtalk.ts
|
|
373
|
+
var DingTalkAdapter = class extends PlatformAdapter {
|
|
374
|
+
get platformName() {
|
|
375
|
+
return "dingtalk";
|
|
376
|
+
}
|
|
377
|
+
get cfg() {
|
|
378
|
+
return this.config;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Build signed webhook URL if HMAC-SHA256 security is enabled
|
|
382
|
+
*/
|
|
383
|
+
getSignedUrl() {
|
|
384
|
+
const { webhook, secret, security } = this.cfg;
|
|
385
|
+
if (security === "sign" && secret) {
|
|
386
|
+
const timestamp = Date.now();
|
|
387
|
+
const stringToSign = `${timestamp}
|
|
388
|
+
${secret}`;
|
|
389
|
+
const hmac = createHmac("sha256", secret).update(stringToSign).digest("base64");
|
|
390
|
+
const sign = encodeURIComponent(hmac);
|
|
391
|
+
return `${webhook}×tamp=${timestamp}&sign=${sign}`;
|
|
392
|
+
}
|
|
393
|
+
return webhook;
|
|
394
|
+
}
|
|
395
|
+
buildPayload(options) {
|
|
396
|
+
const at = this.buildAt(options.at);
|
|
397
|
+
switch (options.type) {
|
|
398
|
+
case "text":
|
|
399
|
+
return {
|
|
400
|
+
msgtype: "text",
|
|
401
|
+
text: { content: options.content },
|
|
402
|
+
...Object.keys(at).length > 0 ? { at } : {}
|
|
403
|
+
};
|
|
404
|
+
case "markdown":
|
|
405
|
+
return {
|
|
406
|
+
msgtype: "markdown",
|
|
407
|
+
markdown: {
|
|
408
|
+
title: options.title || "Message",
|
|
409
|
+
text: options.content
|
|
410
|
+
},
|
|
411
|
+
...Object.keys(at).length > 0 ? { at } : {}
|
|
412
|
+
};
|
|
413
|
+
case "link":
|
|
414
|
+
return {
|
|
415
|
+
msgtype: "link",
|
|
416
|
+
link: {
|
|
417
|
+
title: options.title || "Link",
|
|
418
|
+
text: options.content,
|
|
419
|
+
messageUrl: options.linkUrl || "",
|
|
420
|
+
picUrl: options.picUrl || ""
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
case "card": {
|
|
424
|
+
const hasButtons = options.buttons && options.buttons.length > 0;
|
|
425
|
+
if (hasButtons) {
|
|
426
|
+
return {
|
|
427
|
+
msgtype: "actionCard",
|
|
428
|
+
actionCard: {
|
|
429
|
+
title: options.title || "Card",
|
|
430
|
+
text: options.content,
|
|
431
|
+
btnOrientation: "0",
|
|
432
|
+
btns: options.buttons.map((btn) => ({
|
|
433
|
+
title: btn.title,
|
|
434
|
+
actionURL: btn.url
|
|
435
|
+
}))
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
msgtype: "actionCard",
|
|
441
|
+
actionCard: {
|
|
442
|
+
title: options.title || "Card",
|
|
443
|
+
text: options.content,
|
|
444
|
+
singleTitle: options.singleButtonText || "View",
|
|
445
|
+
singleURL: options.singleButtonUrl || ""
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
case "news":
|
|
450
|
+
return {
|
|
451
|
+
msgtype: "feedCard",
|
|
452
|
+
feedCard: {
|
|
453
|
+
links: (options.newsItems || []).map((item) => ({
|
|
454
|
+
title: item.title,
|
|
455
|
+
messageURL: item.url,
|
|
456
|
+
picURL: item.picUrl || ""
|
|
457
|
+
}))
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
case "image":
|
|
461
|
+
return {
|
|
462
|
+
msgtype: "markdown",
|
|
463
|
+
markdown: {
|
|
464
|
+
title: options.title || "Image",
|
|
465
|
+
text: ``
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
case "file":
|
|
469
|
+
return {
|
|
470
|
+
msgtype: "link",
|
|
471
|
+
link: {
|
|
472
|
+
title: options.title || "File",
|
|
473
|
+
text: options.content || "A file was shared",
|
|
474
|
+
messageUrl: options.linkUrl || "",
|
|
475
|
+
picUrl: ""
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
default:
|
|
479
|
+
return {
|
|
480
|
+
msgtype: "text",
|
|
481
|
+
text: { content: options.content }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async send(options) {
|
|
486
|
+
return this.withTiming(async () => {
|
|
487
|
+
const url = this.getSignedUrl();
|
|
488
|
+
const payload = this.buildPayload(options);
|
|
489
|
+
const { status, data } = await this.httpPost(url, payload);
|
|
490
|
+
const result = data;
|
|
491
|
+
if (status === 200 && result.errcode === 0) {
|
|
492
|
+
return { success: true, messageId: this.generateMessageId(), raw: result };
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
success: false,
|
|
496
|
+
error: `DingTalk error [${result.errcode}]: ${result.errmsg}`,
|
|
497
|
+
raw: result
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async sendFile(filePath, _caption) {
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
platform: "dingtalk",
|
|
505
|
+
bot: this.botName,
|
|
506
|
+
latencyMs: 0,
|
|
507
|
+
error: "DingTalk webhook does not support file uploads. Use link type instead."
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
buildAt(at) {
|
|
511
|
+
if (!at) return {};
|
|
512
|
+
const result = {};
|
|
513
|
+
if (at.all) result.isAtAll = true;
|
|
514
|
+
if (at.mobiles && at.mobiles.length > 0) result.atMobiles = at.mobiles;
|
|
515
|
+
if (at.users && at.users.length > 0) result.atUserIds = at.users;
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/platforms/wecom.ts
|
|
521
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
522
|
+
import { createHash } from "crypto";
|
|
523
|
+
var WeComAdapter = class extends PlatformAdapter {
|
|
524
|
+
get platformName() {
|
|
525
|
+
return "wecom";
|
|
526
|
+
}
|
|
527
|
+
get cfg() {
|
|
528
|
+
return this.config;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Extract the webhook key from URL for file upload
|
|
532
|
+
*/
|
|
533
|
+
getWebhookKey() {
|
|
534
|
+
const url = new URL(this.cfg.webhook);
|
|
535
|
+
return url.searchParams.get("key") || "";
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Upload file to get media_id (for file messages)
|
|
539
|
+
*/
|
|
540
|
+
async uploadFile(filePath) {
|
|
541
|
+
const key = this.getWebhookKey();
|
|
542
|
+
const uploadUrl = `https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=${key}&type=file`;
|
|
543
|
+
const fileBuffer = readFileSync2(filePath);
|
|
544
|
+
const fileName = filePath.split(/[/\\]/).pop() || "file";
|
|
545
|
+
const formData = new FormData();
|
|
546
|
+
formData.append("media", new Blob([fileBuffer]), fileName);
|
|
547
|
+
const response = await fetch(uploadUrl, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
body: formData
|
|
550
|
+
});
|
|
551
|
+
const data = await response.json();
|
|
552
|
+
if (data.errcode !== 0) {
|
|
553
|
+
throw new PlatformError(
|
|
554
|
+
`WeCom file upload failed: [${data.errcode}] ${data.errmsg}`,
|
|
555
|
+
"wecom"
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
return data.media_id;
|
|
559
|
+
}
|
|
560
|
+
buildPayload(options) {
|
|
561
|
+
switch (options.type) {
|
|
562
|
+
case "text":
|
|
563
|
+
return {
|
|
564
|
+
msgtype: "text",
|
|
565
|
+
text: {
|
|
566
|
+
content: options.content,
|
|
567
|
+
...this.buildMention(options.at)
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
case "markdown":
|
|
571
|
+
return {
|
|
572
|
+
msgtype: "markdown",
|
|
573
|
+
markdown: {
|
|
574
|
+
content: options.content
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
case "image": {
|
|
578
|
+
if (options.imageBase64) {
|
|
579
|
+
const md5 = createHash("md5").update(Buffer.from(options.imageBase64, "base64")).digest("hex");
|
|
580
|
+
return {
|
|
581
|
+
msgtype: "image",
|
|
582
|
+
image: {
|
|
583
|
+
base64: options.imageBase64,
|
|
584
|
+
md5
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
msgtype: "news",
|
|
590
|
+
news: {
|
|
591
|
+
articles: [
|
|
592
|
+
{
|
|
593
|
+
title: options.title || "Image",
|
|
594
|
+
description: options.content || "",
|
|
595
|
+
url: options.imageUrl || options.content,
|
|
596
|
+
picurl: options.imageUrl || options.content
|
|
597
|
+
}
|
|
598
|
+
]
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
case "link":
|
|
603
|
+
return {
|
|
604
|
+
msgtype: "news",
|
|
605
|
+
news: {
|
|
606
|
+
articles: [
|
|
607
|
+
{
|
|
608
|
+
title: options.title || "Link",
|
|
609
|
+
description: options.content,
|
|
610
|
+
url: options.linkUrl || "",
|
|
611
|
+
picurl: options.picUrl || ""
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
case "news":
|
|
617
|
+
return {
|
|
618
|
+
msgtype: "news",
|
|
619
|
+
news: {
|
|
620
|
+
articles: (options.newsItems || []).map((item) => ({
|
|
621
|
+
title: item.title,
|
|
622
|
+
description: item.description || "",
|
|
623
|
+
url: item.url,
|
|
624
|
+
picurl: item.picUrl || ""
|
|
625
|
+
}))
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
case "file":
|
|
629
|
+
return {
|
|
630
|
+
msgtype: "file",
|
|
631
|
+
file: {
|
|
632
|
+
media_id: options.fileMediaId || ""
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
case "card":
|
|
636
|
+
return {
|
|
637
|
+
msgtype: "template_card",
|
|
638
|
+
template_card: {
|
|
639
|
+
card_type: "text_notice",
|
|
640
|
+
main_title: {
|
|
641
|
+
title: options.title || "Card"
|
|
642
|
+
},
|
|
643
|
+
emphasis_content: {
|
|
644
|
+
title: options.content.split("\n")[0] || "",
|
|
645
|
+
desc: options.content
|
|
646
|
+
},
|
|
647
|
+
jump_list: (options.buttons || []).map((btn) => ({
|
|
648
|
+
type: 1,
|
|
649
|
+
url: btn.url,
|
|
650
|
+
title: btn.title
|
|
651
|
+
})),
|
|
652
|
+
card_action: options.singleButtonUrl ? { type: 1, url: options.singleButtonUrl } : options.buttons?.[0] ? { type: 1, url: options.buttons[0].url } : void 0
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
default:
|
|
656
|
+
return {
|
|
657
|
+
msgtype: "text",
|
|
658
|
+
text: { content: options.content }
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async send(options) {
|
|
663
|
+
return this.withTiming(async () => {
|
|
664
|
+
const payload = this.buildPayload(options);
|
|
665
|
+
const { status, data } = await this.httpPost(this.cfg.webhook, payload);
|
|
666
|
+
const result = data;
|
|
667
|
+
if (status === 200 && result.errcode === 0) {
|
|
668
|
+
return { success: true, messageId: this.generateMessageId(), raw: result };
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
error: `WeCom error [${result.errcode}]: ${result.errmsg}`,
|
|
673
|
+
raw: result
|
|
674
|
+
};
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
async sendFile(filePath, _caption) {
|
|
678
|
+
return this.withTiming(async () => {
|
|
679
|
+
try {
|
|
680
|
+
const mediaId = await this.uploadFile(filePath);
|
|
681
|
+
const payload = {
|
|
682
|
+
msgtype: "file",
|
|
683
|
+
file: { media_id: mediaId }
|
|
684
|
+
};
|
|
685
|
+
const { status, data } = await this.httpPost(this.cfg.webhook, payload);
|
|
686
|
+
const result = data;
|
|
687
|
+
if (status === 200 && result.errcode === 0) {
|
|
688
|
+
return { success: true, messageId: this.generateMessageId(), raw: result };
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
success: false,
|
|
692
|
+
error: `WeCom send file failed: [${result.errcode}] ${result.errmsg}`,
|
|
693
|
+
raw: result
|
|
694
|
+
};
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
error: err instanceof Error ? err.message : String(err)
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
buildMention(at) {
|
|
704
|
+
if (!at) return {};
|
|
705
|
+
const result = {};
|
|
706
|
+
if (at.users && at.users.length > 0) {
|
|
707
|
+
result.mentioned_list = at.all ? ["@all"] : at.users;
|
|
708
|
+
} else if (at.all) {
|
|
709
|
+
result.mentioned_list = ["@all"];
|
|
710
|
+
}
|
|
711
|
+
if (at.mobiles && at.mobiles.length > 0) {
|
|
712
|
+
result.mentioned_mobile_list = at.mobiles;
|
|
713
|
+
}
|
|
714
|
+
return result;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/platforms/feishu.ts
|
|
719
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
720
|
+
var FeishuAdapter = class extends PlatformAdapter {
|
|
721
|
+
baseUrl = "https://open.feishu.cn";
|
|
722
|
+
get platformName() {
|
|
723
|
+
return "feishu";
|
|
724
|
+
}
|
|
725
|
+
get cfg() {
|
|
726
|
+
return this.config;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Generate HMAC-SHA256 signature for Feishu
|
|
730
|
+
* Note: Feishu uses string_to_sign as KEY and empty string as DATA
|
|
731
|
+
*/
|
|
732
|
+
generateSign() {
|
|
733
|
+
const { secret } = this.cfg;
|
|
734
|
+
if (!secret) return null;
|
|
735
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
736
|
+
const stringToSign = `${timestamp}
|
|
737
|
+
${secret}`;
|
|
738
|
+
const sign = createHmac2("sha256", stringToSign).update("").digest("base64");
|
|
739
|
+
return { timestamp, sign };
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Add signature fields to payload if security is enabled
|
|
743
|
+
*/
|
|
744
|
+
addSignature(payload) {
|
|
745
|
+
const signData = this.generateSign();
|
|
746
|
+
if (signData) {
|
|
747
|
+
payload.timestamp = signData.timestamp;
|
|
748
|
+
payload.sign = signData.sign;
|
|
749
|
+
}
|
|
750
|
+
return payload;
|
|
751
|
+
}
|
|
752
|
+
buildPayload(options) {
|
|
753
|
+
let payload;
|
|
754
|
+
switch (options.type) {
|
|
755
|
+
case "text": {
|
|
756
|
+
let textContent = options.content;
|
|
757
|
+
if (options.at) {
|
|
758
|
+
if (options.at.all) {
|
|
759
|
+
textContent += '\n<at user_id="all">\u6240\u6709\u4EBA</at>';
|
|
760
|
+
}
|
|
761
|
+
if (options.at.users) {
|
|
762
|
+
for (const userId of options.at.users) {
|
|
763
|
+
textContent += `
|
|
764
|
+
<at user_id="${userId}">${userId}</at>`;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
payload = {
|
|
769
|
+
msg_type: "text",
|
|
770
|
+
content: { text: textContent }
|
|
771
|
+
};
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
case "markdown": {
|
|
775
|
+
payload = {
|
|
776
|
+
msg_type: "interactive",
|
|
777
|
+
card: {
|
|
778
|
+
schema: "2.0",
|
|
779
|
+
header: {
|
|
780
|
+
title: {
|
|
781
|
+
tag: "plain_text",
|
|
782
|
+
content: options.title || "Message"
|
|
783
|
+
},
|
|
784
|
+
template: "blue"
|
|
785
|
+
},
|
|
786
|
+
body: {
|
|
787
|
+
elements: [
|
|
788
|
+
{
|
|
789
|
+
tag: "markdown",
|
|
790
|
+
content: options.content
|
|
791
|
+
}
|
|
792
|
+
]
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
case "link":
|
|
799
|
+
payload = {
|
|
800
|
+
msg_type: "post",
|
|
801
|
+
content: {
|
|
802
|
+
post: {
|
|
803
|
+
zh_cn: {
|
|
804
|
+
title: options.title || "Link",
|
|
805
|
+
content: [
|
|
806
|
+
[
|
|
807
|
+
{ tag: "text", text: options.content + "\n" },
|
|
808
|
+
{
|
|
809
|
+
tag: "a",
|
|
810
|
+
text: options.linkUrl || "Open Link",
|
|
811
|
+
href: options.linkUrl || ""
|
|
812
|
+
}
|
|
813
|
+
]
|
|
814
|
+
]
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
break;
|
|
820
|
+
case "image":
|
|
821
|
+
payload = {
|
|
822
|
+
msg_type: "image",
|
|
823
|
+
content: {
|
|
824
|
+
image_key: options.content
|
|
825
|
+
// For Feishu, content should be image_key
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
break;
|
|
829
|
+
case "card": {
|
|
830
|
+
const elements = [
|
|
831
|
+
{ tag: "markdown", content: options.content }
|
|
832
|
+
];
|
|
833
|
+
if (options.buttons && options.buttons.length > 0) {
|
|
834
|
+
elements.push({
|
|
835
|
+
tag: "action",
|
|
836
|
+
actions: options.buttons.map((btn) => ({
|
|
837
|
+
tag: "button",
|
|
838
|
+
text: { tag: "plain_text", content: btn.title },
|
|
839
|
+
type: "primary",
|
|
840
|
+
behaviors: [
|
|
841
|
+
{ type: "open_url", default_url: btn.url }
|
|
842
|
+
]
|
|
843
|
+
}))
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
payload = {
|
|
847
|
+
msg_type: "interactive",
|
|
848
|
+
card: {
|
|
849
|
+
schema: "2.0",
|
|
850
|
+
header: {
|
|
851
|
+
title: {
|
|
852
|
+
tag: "plain_text",
|
|
853
|
+
content: options.title || "Card"
|
|
854
|
+
},
|
|
855
|
+
template: "blue"
|
|
856
|
+
},
|
|
857
|
+
body: { elements }
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case "news": {
|
|
863
|
+
const contentRows = (options.newsItems || []).map((item) => [
|
|
864
|
+
{ tag: "a", text: item.title, href: item.url }
|
|
865
|
+
]);
|
|
866
|
+
payload = {
|
|
867
|
+
msg_type: "post",
|
|
868
|
+
content: {
|
|
869
|
+
post: {
|
|
870
|
+
zh_cn: {
|
|
871
|
+
title: options.title || "News",
|
|
872
|
+
content: contentRows
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
case "file":
|
|
880
|
+
payload = {
|
|
881
|
+
msg_type: "text",
|
|
882
|
+
content: {
|
|
883
|
+
text: options.content || `[File: ${options.filePath || "unknown"}]`
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
break;
|
|
887
|
+
default:
|
|
888
|
+
payload = {
|
|
889
|
+
msg_type: "text",
|
|
890
|
+
content: { text: options.content }
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
return this.addSignature(payload);
|
|
894
|
+
}
|
|
895
|
+
async send(options) {
|
|
896
|
+
return this.withTiming(async () => {
|
|
897
|
+
const payload = this.buildPayload(options);
|
|
898
|
+
const { status, data } = await this.httpPost(this.cfg.webhook, payload);
|
|
899
|
+
const result = data;
|
|
900
|
+
if (status === 200 && result.code === 0) {
|
|
901
|
+
return { success: true, messageId: this.generateMessageId(), raw: result };
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
success: false,
|
|
905
|
+
error: `Feishu error [${result.code}]: ${result.msg}`,
|
|
906
|
+
raw: result
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
async sendFile(filePath, _caption) {
|
|
911
|
+
return {
|
|
912
|
+
success: false,
|
|
913
|
+
platform: "feishu",
|
|
914
|
+
bot: this.botName,
|
|
915
|
+
latencyMs: 0,
|
|
916
|
+
error: "Feishu webhook does not support file uploads. Use image_key or share_chat instead."
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/platforms/lark.ts
|
|
922
|
+
var LarkAdapter = class extends FeishuAdapter {
|
|
923
|
+
baseUrl = "https://open.larksuite.com";
|
|
924
|
+
get platformName() {
|
|
925
|
+
return "lark";
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/platforms/slack.ts
|
|
930
|
+
var SlackAdapter = class extends PlatformAdapter {
|
|
931
|
+
get platformName() {
|
|
932
|
+
return "slack";
|
|
933
|
+
}
|
|
934
|
+
get cfg() {
|
|
935
|
+
return this.config;
|
|
936
|
+
}
|
|
937
|
+
buildPayload(options) {
|
|
938
|
+
switch (options.type) {
|
|
939
|
+
case "text":
|
|
940
|
+
return { text: options.content };
|
|
941
|
+
case "markdown": {
|
|
942
|
+
const blocks = [];
|
|
943
|
+
if (options.title) {
|
|
944
|
+
blocks.push({
|
|
945
|
+
type: "header",
|
|
946
|
+
text: { type: "plain_text", text: options.title }
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
blocks.push({
|
|
950
|
+
type: "section",
|
|
951
|
+
text: { type: "mrkdwn", text: options.content }
|
|
952
|
+
});
|
|
953
|
+
return {
|
|
954
|
+
text: options.title || options.content.slice(0, 100),
|
|
955
|
+
blocks
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
case "link": {
|
|
959
|
+
const blocks = [
|
|
960
|
+
{
|
|
961
|
+
type: "section",
|
|
962
|
+
text: {
|
|
963
|
+
type: "mrkdwn",
|
|
964
|
+
text: `*${options.title || "Link"}*
|
|
965
|
+
${options.content}`
|
|
966
|
+
},
|
|
967
|
+
accessory: options.picUrl ? {
|
|
968
|
+
type: "image",
|
|
969
|
+
image_url: options.picUrl,
|
|
970
|
+
alt_text: options.title || "image"
|
|
971
|
+
} : void 0
|
|
972
|
+
}
|
|
973
|
+
];
|
|
974
|
+
if (options.linkUrl) {
|
|
975
|
+
blocks.push({
|
|
976
|
+
type: "actions",
|
|
977
|
+
elements: [
|
|
978
|
+
{
|
|
979
|
+
type: "button",
|
|
980
|
+
text: { type: "plain_text", text: "Open Link" },
|
|
981
|
+
url: options.linkUrl
|
|
982
|
+
}
|
|
983
|
+
]
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
text: `${options.title || "Link"}: ${options.linkUrl || ""}`,
|
|
988
|
+
blocks
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
case "image": {
|
|
992
|
+
const imageUrl = options.imageUrl || options.content;
|
|
993
|
+
return {
|
|
994
|
+
text: options.title || "Image",
|
|
995
|
+
blocks: [
|
|
996
|
+
{
|
|
997
|
+
type: "image",
|
|
998
|
+
image_url: imageUrl,
|
|
999
|
+
alt_text: options.title || "image"
|
|
1000
|
+
}
|
|
1001
|
+
]
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
case "card": {
|
|
1005
|
+
const blocks = [];
|
|
1006
|
+
if (options.title) {
|
|
1007
|
+
blocks.push({
|
|
1008
|
+
type: "header",
|
|
1009
|
+
text: { type: "plain_text", text: options.title }
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
blocks.push({
|
|
1013
|
+
type: "section",
|
|
1014
|
+
text: { type: "mrkdwn", text: options.content }
|
|
1015
|
+
});
|
|
1016
|
+
if (options.buttons && options.buttons.length > 0) {
|
|
1017
|
+
blocks.push({
|
|
1018
|
+
type: "actions",
|
|
1019
|
+
elements: options.buttons.map((btn) => ({
|
|
1020
|
+
type: "button",
|
|
1021
|
+
text: { type: "plain_text", text: btn.title },
|
|
1022
|
+
url: btn.url
|
|
1023
|
+
}))
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
text: options.title || options.content.slice(0, 100),
|
|
1028
|
+
blocks
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
case "news": {
|
|
1032
|
+
const blocks = [];
|
|
1033
|
+
if (options.title) {
|
|
1034
|
+
blocks.push({
|
|
1035
|
+
type: "header",
|
|
1036
|
+
text: { type: "plain_text", text: options.title }
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
for (const item of options.newsItems || []) {
|
|
1040
|
+
blocks.push({
|
|
1041
|
+
type: "section",
|
|
1042
|
+
text: {
|
|
1043
|
+
type: "mrkdwn",
|
|
1044
|
+
text: `*<${item.url}|${item.title}>*
|
|
1045
|
+
${item.description || ""}`
|
|
1046
|
+
},
|
|
1047
|
+
accessory: item.picUrl ? {
|
|
1048
|
+
type: "image",
|
|
1049
|
+
image_url: item.picUrl,
|
|
1050
|
+
alt_text: item.title
|
|
1051
|
+
} : void 0
|
|
1052
|
+
});
|
|
1053
|
+
blocks.push({ type: "divider" });
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
text: options.title || "News",
|
|
1057
|
+
blocks
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
case "file":
|
|
1061
|
+
return {
|
|
1062
|
+
text: options.content || `[File: ${options.filePath || "unknown"}]`
|
|
1063
|
+
};
|
|
1064
|
+
default:
|
|
1065
|
+
return { text: options.content };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async send(options) {
|
|
1069
|
+
return this.withTiming(async () => {
|
|
1070
|
+
const payload = this.buildPayload(options);
|
|
1071
|
+
const { status, data } = await this.httpPost(this.cfg.webhook, payload);
|
|
1072
|
+
if (status === 200) {
|
|
1073
|
+
return { success: true, messageId: this.generateMessageId(), raw: data };
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: `Slack error [${status}]: ${typeof data === "string" ? data : JSON.stringify(data)}`,
|
|
1078
|
+
raw: data
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
async sendFile(filePath, _caption) {
|
|
1083
|
+
return {
|
|
1084
|
+
success: false,
|
|
1085
|
+
platform: "slack",
|
|
1086
|
+
bot: this.botName,
|
|
1087
|
+
latencyMs: 0,
|
|
1088
|
+
error: "Slack incoming webhooks do not support file uploads."
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
// src/platforms/discord.ts
|
|
1094
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1095
|
+
import { basename } from "path";
|
|
1096
|
+
var DiscordAdapter = class extends PlatformAdapter {
|
|
1097
|
+
get platformName() {
|
|
1098
|
+
return "discord";
|
|
1099
|
+
}
|
|
1100
|
+
get cfg() {
|
|
1101
|
+
return this.config;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Build common fields for all payloads
|
|
1105
|
+
*/
|
|
1106
|
+
getBasePayload() {
|
|
1107
|
+
const base = {};
|
|
1108
|
+
if (this.cfg.username) base.username = this.cfg.username;
|
|
1109
|
+
if (this.cfg.avatar_url) base.avatar_url = this.cfg.avatar_url;
|
|
1110
|
+
return base;
|
|
1111
|
+
}
|
|
1112
|
+
buildPayload(options) {
|
|
1113
|
+
const base = this.getBasePayload();
|
|
1114
|
+
switch (options.type) {
|
|
1115
|
+
case "text":
|
|
1116
|
+
return { ...base, content: options.content };
|
|
1117
|
+
case "markdown":
|
|
1118
|
+
case "link": {
|
|
1119
|
+
const embed = {
|
|
1120
|
+
description: options.content,
|
|
1121
|
+
color: 3447003
|
|
1122
|
+
// Blue
|
|
1123
|
+
};
|
|
1124
|
+
if (options.title) embed.title = options.title;
|
|
1125
|
+
if (options.linkUrl) embed.url = options.linkUrl;
|
|
1126
|
+
if (options.picUrl) embed.thumbnail = { url: options.picUrl };
|
|
1127
|
+
embed.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1128
|
+
return { ...base, embeds: [embed] };
|
|
1129
|
+
}
|
|
1130
|
+
case "image": {
|
|
1131
|
+
const imageUrl = options.imageUrl || options.content;
|
|
1132
|
+
return {
|
|
1133
|
+
...base,
|
|
1134
|
+
embeds: [
|
|
1135
|
+
{
|
|
1136
|
+
title: options.title || "Image",
|
|
1137
|
+
image: { url: imageUrl },
|
|
1138
|
+
color: 3447003
|
|
1139
|
+
}
|
|
1140
|
+
]
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
case "card": {
|
|
1144
|
+
const embed = {
|
|
1145
|
+
title: options.title || "Card",
|
|
1146
|
+
description: options.content,
|
|
1147
|
+
color: 3447003,
|
|
1148
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1149
|
+
};
|
|
1150
|
+
if (options.buttons && options.buttons.length > 0) {
|
|
1151
|
+
return {
|
|
1152
|
+
...base,
|
|
1153
|
+
embeds: [embed],
|
|
1154
|
+
components: [
|
|
1155
|
+
{
|
|
1156
|
+
type: 1,
|
|
1157
|
+
components: options.buttons.map((btn) => ({
|
|
1158
|
+
type: 2,
|
|
1159
|
+
style: 5,
|
|
1160
|
+
label: btn.title,
|
|
1161
|
+
url: btn.url
|
|
1162
|
+
}))
|
|
1163
|
+
}
|
|
1164
|
+
]
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return { ...base, embeds: [embed] };
|
|
1168
|
+
}
|
|
1169
|
+
case "news": {
|
|
1170
|
+
const embeds = (options.newsItems || []).map((item) => ({
|
|
1171
|
+
title: item.title,
|
|
1172
|
+
description: item.description || "",
|
|
1173
|
+
url: item.url,
|
|
1174
|
+
thumbnail: item.picUrl ? { url: item.picUrl } : void 0,
|
|
1175
|
+
color: 3447003
|
|
1176
|
+
}));
|
|
1177
|
+
return { ...base, embeds: embeds.slice(0, 10) };
|
|
1178
|
+
}
|
|
1179
|
+
case "file":
|
|
1180
|
+
return {
|
|
1181
|
+
...base,
|
|
1182
|
+
content: options.content || `[File: ${options.filePath || "unknown"}]`
|
|
1183
|
+
};
|
|
1184
|
+
default:
|
|
1185
|
+
return { ...base, content: options.content };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
async send(options) {
|
|
1189
|
+
return this.withTiming(async () => {
|
|
1190
|
+
const payload = this.buildPayload(options);
|
|
1191
|
+
const { status, data } = await this.httpPost(this.cfg.webhook, payload);
|
|
1192
|
+
if (status === 204 || status === 200) {
|
|
1193
|
+
return { success: true, messageId: this.generateMessageId(), raw: data };
|
|
1194
|
+
}
|
|
1195
|
+
const result = data;
|
|
1196
|
+
return {
|
|
1197
|
+
success: false,
|
|
1198
|
+
error: `Discord error [${result.code || status}]: ${result.message || JSON.stringify(data)}`,
|
|
1199
|
+
raw: result
|
|
1200
|
+
};
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
async sendFile(filePath, caption) {
|
|
1204
|
+
return this.withTiming(async () => {
|
|
1205
|
+
try {
|
|
1206
|
+
const fileBuffer = readFileSync3(filePath);
|
|
1207
|
+
const fileName = basename(filePath);
|
|
1208
|
+
const formData = new FormData();
|
|
1209
|
+
const payloadJson = {
|
|
1210
|
+
...this.getBasePayload(),
|
|
1211
|
+
content: caption || ""
|
|
1212
|
+
};
|
|
1213
|
+
formData.append("payload_json", JSON.stringify(payloadJson));
|
|
1214
|
+
formData.append("files[0]", new Blob([fileBuffer]), fileName);
|
|
1215
|
+
const response = await fetch(this.cfg.webhook, {
|
|
1216
|
+
method: "POST",
|
|
1217
|
+
body: formData
|
|
1218
|
+
});
|
|
1219
|
+
if (response.status === 204 || response.status === 200) {
|
|
1220
|
+
return {
|
|
1221
|
+
success: true,
|
|
1222
|
+
messageId: this.generateMessageId(),
|
|
1223
|
+
raw: await response.text()
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const errorData = await response.json().catch(() => response.statusText);
|
|
1227
|
+
return {
|
|
1228
|
+
success: false,
|
|
1229
|
+
error: `Discord file upload failed [${response.status}]: ${JSON.stringify(errorData)}`
|
|
1230
|
+
};
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
return {
|
|
1233
|
+
success: false,
|
|
1234
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
|
|
1241
|
+
// src/platforms/telegram.ts
|
|
1242
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1243
|
+
import { basename as basename2 } from "path";
|
|
1244
|
+
var TelegramAdapter = class extends PlatformAdapter {
|
|
1245
|
+
get platformName() {
|
|
1246
|
+
return "telegram";
|
|
1247
|
+
}
|
|
1248
|
+
get cfg() {
|
|
1249
|
+
return this.config;
|
|
1250
|
+
}
|
|
1251
|
+
get apiBase() {
|
|
1252
|
+
return `https://api.telegram.org/bot${this.cfg.token}`;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Escape special characters for MarkdownV2
|
|
1256
|
+
*/
|
|
1257
|
+
escapeMarkdownV2(text) {
|
|
1258
|
+
if (this.cfg.parse_mode !== "MarkdownV2") return text;
|
|
1259
|
+
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
1260
|
+
}
|
|
1261
|
+
buildPayload(options) {
|
|
1262
|
+
const basePayload = {
|
|
1263
|
+
chat_id: this.cfg.chat_id
|
|
1264
|
+
};
|
|
1265
|
+
if (this.cfg.parse_mode) {
|
|
1266
|
+
basePayload.parse_mode = this.cfg.parse_mode;
|
|
1267
|
+
}
|
|
1268
|
+
switch (options.type) {
|
|
1269
|
+
case "text":
|
|
1270
|
+
return {
|
|
1271
|
+
...basePayload,
|
|
1272
|
+
text: options.content
|
|
1273
|
+
};
|
|
1274
|
+
case "markdown":
|
|
1275
|
+
return {
|
|
1276
|
+
...basePayload,
|
|
1277
|
+
parse_mode: this.cfg.parse_mode || "MarkdownV2",
|
|
1278
|
+
text: options.content
|
|
1279
|
+
};
|
|
1280
|
+
case "link": {
|
|
1281
|
+
const text = options.title ? `[${options.title}](${options.linkUrl})
|
|
1282
|
+
${options.content}` : `${options.content}
|
|
1283
|
+
${options.linkUrl}`;
|
|
1284
|
+
return {
|
|
1285
|
+
...basePayload,
|
|
1286
|
+
parse_mode: "MarkdownV2",
|
|
1287
|
+
text: this.escapeMarkdownV2(options.content) + (options.linkUrl ? `
|
|
1288
|
+
[Open Link](${options.linkUrl})` : "")
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
case "image":
|
|
1292
|
+
return {
|
|
1293
|
+
chat_id: this.cfg.chat_id,
|
|
1294
|
+
photo: options.imageUrl || options.content,
|
|
1295
|
+
caption: options.title || "",
|
|
1296
|
+
...this.cfg.parse_mode ? { parse_mode: this.cfg.parse_mode } : {}
|
|
1297
|
+
};
|
|
1298
|
+
case "card": {
|
|
1299
|
+
const inlineKeyboard = (options.buttons || []).map((btn) => [
|
|
1300
|
+
{ text: btn.title, url: btn.url }
|
|
1301
|
+
]);
|
|
1302
|
+
const payload = {
|
|
1303
|
+
...basePayload,
|
|
1304
|
+
text: options.title ? `*${options.title}*
|
|
1305
|
+
${options.content}` : options.content
|
|
1306
|
+
};
|
|
1307
|
+
if (inlineKeyboard.length > 0) {
|
|
1308
|
+
payload.reply_markup = { inline_keyboard: inlineKeyboard };
|
|
1309
|
+
}
|
|
1310
|
+
return payload;
|
|
1311
|
+
}
|
|
1312
|
+
case "news": {
|
|
1313
|
+
const lines = (options.newsItems || []).map(
|
|
1314
|
+
(item, i) => `${i + 1}. [${item.title}](${item.url})${item.description ? `
|
|
1315
|
+
${item.description}` : ""}`
|
|
1316
|
+
);
|
|
1317
|
+
return {
|
|
1318
|
+
...basePayload,
|
|
1319
|
+
parse_mode: "MarkdownV2",
|
|
1320
|
+
text: (options.title ? `*${this.escapeMarkdownV2(options.title)}*
|
|
1321
|
+
` : "") + this.escapeMarkdownV2(lines.join("\n"))
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
case "file":
|
|
1325
|
+
return {
|
|
1326
|
+
chat_id: this.cfg.chat_id,
|
|
1327
|
+
document: options.content || options.linkUrl || "",
|
|
1328
|
+
caption: options.title || ""
|
|
1329
|
+
};
|
|
1330
|
+
default:
|
|
1331
|
+
return { ...basePayload, text: options.content };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Determine the correct Telegram API method based on message type
|
|
1336
|
+
*/
|
|
1337
|
+
getApiMethod(type) {
|
|
1338
|
+
switch (type) {
|
|
1339
|
+
case "image":
|
|
1340
|
+
return "sendPhoto";
|
|
1341
|
+
case "file":
|
|
1342
|
+
return "sendDocument";
|
|
1343
|
+
default:
|
|
1344
|
+
return "sendMessage";
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async send(options) {
|
|
1348
|
+
return this.withTiming(async () => {
|
|
1349
|
+
const method = this.getApiMethod(options.type);
|
|
1350
|
+
const url = `${this.apiBase}/${method}`;
|
|
1351
|
+
const payload = this.buildPayload(options);
|
|
1352
|
+
const { status, data } = await this.httpPost(url, payload);
|
|
1353
|
+
const result = data;
|
|
1354
|
+
if (status === 200 && result.ok === true) {
|
|
1355
|
+
const messageData = result.result;
|
|
1356
|
+
return {
|
|
1357
|
+
success: true,
|
|
1358
|
+
messageId: messageData?.message_id ? String(messageData.message_id) : this.generateMessageId(),
|
|
1359
|
+
raw: result
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
success: false,
|
|
1364
|
+
error: `Telegram error [${result.error_code}]: ${result.description}`,
|
|
1365
|
+
raw: result
|
|
1366
|
+
};
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
async sendFile(filePath, caption) {
|
|
1370
|
+
return this.withTiming(async () => {
|
|
1371
|
+
try {
|
|
1372
|
+
const url = `${this.apiBase}/sendDocument`;
|
|
1373
|
+
const fileBuffer = readFileSync4(filePath);
|
|
1374
|
+
const fileName = basename2(filePath);
|
|
1375
|
+
const formData = new FormData();
|
|
1376
|
+
formData.append("chat_id", this.cfg.chat_id);
|
|
1377
|
+
formData.append("document", new Blob([fileBuffer]), fileName);
|
|
1378
|
+
if (caption) formData.append("caption", caption);
|
|
1379
|
+
if (this.cfg.parse_mode) formData.append("parse_mode", this.cfg.parse_mode);
|
|
1380
|
+
const response = await fetch(url, {
|
|
1381
|
+
method: "POST",
|
|
1382
|
+
body: formData
|
|
1383
|
+
});
|
|
1384
|
+
const data = await response.json();
|
|
1385
|
+
if (response.status === 200 && data.ok === true) {
|
|
1386
|
+
return {
|
|
1387
|
+
success: true,
|
|
1388
|
+
messageId: String(data.result?.message_id || ""),
|
|
1389
|
+
raw: data
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
success: false,
|
|
1394
|
+
error: `Telegram file upload failed [${data.error_code}]: ${data.description}`,
|
|
1395
|
+
raw: data
|
|
1396
|
+
};
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
return {
|
|
1399
|
+
success: false,
|
|
1400
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
// src/platforms/registry.ts
|
|
1408
|
+
var PlatformRegistry = class {
|
|
1409
|
+
bots = /* @__PURE__ */ new Map();
|
|
1410
|
+
/**
|
|
1411
|
+
* Initialize registry from full config
|
|
1412
|
+
*/
|
|
1413
|
+
loadConfig(config) {
|
|
1414
|
+
for (const [name, botConfig] of Object.entries(config.bots)) {
|
|
1415
|
+
this.register(name, botConfig);
|
|
1416
|
+
}
|
|
1417
|
+
logger.info(`Registry loaded: ${this.bots.size} bot(s) registered`);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Register a single bot
|
|
1421
|
+
*/
|
|
1422
|
+
register(name, config) {
|
|
1423
|
+
const adapter = this.createAdapter(name, config);
|
|
1424
|
+
this.bots.set(name, adapter);
|
|
1425
|
+
logger.debug(`Registered bot: ${name} (${config.platform})`);
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Get a bot adapter by name
|
|
1429
|
+
*/
|
|
1430
|
+
get(name) {
|
|
1431
|
+
const adapter = this.bots.get(name);
|
|
1432
|
+
if (!adapter) throw new BotNotFoundError(name);
|
|
1433
|
+
return adapter;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Check if a bot exists
|
|
1437
|
+
*/
|
|
1438
|
+
has(name) {
|
|
1439
|
+
return this.bots.has(name);
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* List all registered bots
|
|
1443
|
+
*/
|
|
1444
|
+
list() {
|
|
1445
|
+
return Array.from(this.bots.entries()).map(([name, adapter]) => ({
|
|
1446
|
+
name,
|
|
1447
|
+
platform: adapter.platformName,
|
|
1448
|
+
enabled: true
|
|
1449
|
+
}));
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Get all adapter instances
|
|
1453
|
+
*/
|
|
1454
|
+
getAll() {
|
|
1455
|
+
return Array.from(this.bots.values());
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Get adapters by bot names
|
|
1459
|
+
*/
|
|
1460
|
+
getMany(names) {
|
|
1461
|
+
return names.map((name) => this.get(name));
|
|
1462
|
+
}
|
|
1463
|
+
createAdapter(name, config) {
|
|
1464
|
+
switch (config.platform) {
|
|
1465
|
+
case "dingtalk":
|
|
1466
|
+
return new DingTalkAdapter(name, config);
|
|
1467
|
+
case "wecom":
|
|
1468
|
+
return new WeComAdapter(name, config);
|
|
1469
|
+
case "feishu":
|
|
1470
|
+
return new FeishuAdapter(name, config);
|
|
1471
|
+
case "lark":
|
|
1472
|
+
return new LarkAdapter(name, config);
|
|
1473
|
+
case "slack":
|
|
1474
|
+
return new SlackAdapter(name, config);
|
|
1475
|
+
case "discord":
|
|
1476
|
+
return new DiscordAdapter(name, config);
|
|
1477
|
+
case "telegram":
|
|
1478
|
+
return new TelegramAdapter(name, config);
|
|
1479
|
+
default:
|
|
1480
|
+
throw new Error(`Unsupported platform: ${config.platform}`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
// src/queue/rate-limiter.ts
|
|
1486
|
+
import Bottleneck from "bottleneck";
|
|
1487
|
+
var PLATFORM_LIMITS = {
|
|
1488
|
+
dingtalk: {
|
|
1489
|
+
reservoir: 18,
|
|
1490
|
+
// Leave margin below 20/min
|
|
1491
|
+
reservoirRefreshAmount: 18,
|
|
1492
|
+
reservoirRefreshInterval: 6e4,
|
|
1493
|
+
// per minute
|
|
1494
|
+
maxConcurrent: 3,
|
|
1495
|
+
minTime: 3e3,
|
|
1496
|
+
// ~20/min spacing
|
|
1497
|
+
highWater: 50,
|
|
1498
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1499
|
+
},
|
|
1500
|
+
wecom: {
|
|
1501
|
+
reservoir: 18,
|
|
1502
|
+
reservoirRefreshAmount: 18,
|
|
1503
|
+
reservoirRefreshInterval: 6e4,
|
|
1504
|
+
maxConcurrent: 3,
|
|
1505
|
+
minTime: 3e3,
|
|
1506
|
+
highWater: 50,
|
|
1507
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1508
|
+
},
|
|
1509
|
+
feishu: {
|
|
1510
|
+
reservoir: 90,
|
|
1511
|
+
// Leave margin below 100/min
|
|
1512
|
+
reservoirRefreshAmount: 90,
|
|
1513
|
+
reservoirRefreshInterval: 6e4,
|
|
1514
|
+
maxConcurrent: 4,
|
|
1515
|
+
minTime: 250,
|
|
1516
|
+
// ~5/sec = 200ms spacing + margin
|
|
1517
|
+
highWater: 200,
|
|
1518
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1519
|
+
},
|
|
1520
|
+
lark: {
|
|
1521
|
+
reservoir: 90,
|
|
1522
|
+
reservoirRefreshAmount: 90,
|
|
1523
|
+
reservoirRefreshInterval: 6e4,
|
|
1524
|
+
maxConcurrent: 4,
|
|
1525
|
+
minTime: 250,
|
|
1526
|
+
highWater: 200,
|
|
1527
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1528
|
+
},
|
|
1529
|
+
slack: {
|
|
1530
|
+
reservoir: 55,
|
|
1531
|
+
// ~1/sec with margin
|
|
1532
|
+
reservoirRefreshAmount: 55,
|
|
1533
|
+
reservoirRefreshInterval: 6e4,
|
|
1534
|
+
maxConcurrent: 1,
|
|
1535
|
+
minTime: 1100,
|
|
1536
|
+
// 1.1s between messages
|
|
1537
|
+
highWater: 100,
|
|
1538
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1539
|
+
},
|
|
1540
|
+
discord: {
|
|
1541
|
+
reservoir: 4,
|
|
1542
|
+
// Leave margin below 5/2sec
|
|
1543
|
+
reservoirRefreshAmount: 4,
|
|
1544
|
+
reservoirRefreshInterval: 2e3,
|
|
1545
|
+
maxConcurrent: 2,
|
|
1546
|
+
minTime: 500,
|
|
1547
|
+
highWater: 50,
|
|
1548
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1549
|
+
},
|
|
1550
|
+
telegram: {
|
|
1551
|
+
reservoir: 25,
|
|
1552
|
+
// Leave margin below 30/sec
|
|
1553
|
+
reservoirRefreshAmount: 25,
|
|
1554
|
+
reservoirRefreshInterval: 1e3,
|
|
1555
|
+
maxConcurrent: 5,
|
|
1556
|
+
minTime: 40,
|
|
1557
|
+
// ~25/sec
|
|
1558
|
+
highWater: 100,
|
|
1559
|
+
strategy: Bottleneck.strategy.LEAK
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
var RETRYABLE_PATTERNS = [
|
|
1563
|
+
"429",
|
|
1564
|
+
"500",
|
|
1565
|
+
"502",
|
|
1566
|
+
"503",
|
|
1567
|
+
"504",
|
|
1568
|
+
"ECONNRESET",
|
|
1569
|
+
"ETIMEDOUT",
|
|
1570
|
+
"ENOTFOUND",
|
|
1571
|
+
"RATE_LIMIT",
|
|
1572
|
+
"Too Many Requests"
|
|
1573
|
+
];
|
|
1574
|
+
var RateLimiterManager = class {
|
|
1575
|
+
limiters = /* @__PURE__ */ new Map();
|
|
1576
|
+
maxRetries;
|
|
1577
|
+
retryMultiplier;
|
|
1578
|
+
maxRetryDelayMs;
|
|
1579
|
+
constructor(options = {}) {
|
|
1580
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
1581
|
+
this.retryMultiplier = options.retryMultiplier ?? 2;
|
|
1582
|
+
this.maxRetryDelayMs = options.maxRetryDelayMs ?? 3e4;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Get or create a Bottleneck limiter for a specific bot
|
|
1586
|
+
*/
|
|
1587
|
+
getLimiter(botName, platform) {
|
|
1588
|
+
if (this.limiters.has(botName)) {
|
|
1589
|
+
return this.limiters.get(botName);
|
|
1590
|
+
}
|
|
1591
|
+
const platformConfig = PLATFORM_LIMITS[platform] || PLATFORM_LIMITS.telegram;
|
|
1592
|
+
const limiter = new Bottleneck({
|
|
1593
|
+
...platformConfig,
|
|
1594
|
+
maxExecutionTime: 3e4
|
|
1595
|
+
// 30s timeout per request
|
|
1596
|
+
});
|
|
1597
|
+
limiter.on("failed", async (error, jobInfo) => {
|
|
1598
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1599
|
+
const shouldRetry = RETRYABLE_PATTERNS.some(
|
|
1600
|
+
(pattern) => errorMessage.includes(pattern)
|
|
1601
|
+
);
|
|
1602
|
+
if (shouldRetry && jobInfo.retryCount < this.maxRetries) {
|
|
1603
|
+
const delay = Math.min(
|
|
1604
|
+
Math.pow(this.retryMultiplier, jobInfo.retryCount) * 1e3,
|
|
1605
|
+
this.maxRetryDelayMs
|
|
1606
|
+
);
|
|
1607
|
+
logger.warn(`Retry #${jobInfo.retryCount + 1} for ${botName} after ${delay}ms`, {
|
|
1608
|
+
error: errorMessage
|
|
1609
|
+
});
|
|
1610
|
+
return delay;
|
|
1611
|
+
}
|
|
1612
|
+
logger.error(`Max retries exceeded for ${botName}`, {
|
|
1613
|
+
attempts: jobInfo.retryCount + 1,
|
|
1614
|
+
error: errorMessage
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
this.limiters.set(botName, limiter);
|
|
1618
|
+
logger.debug(`Created rate limiter for bot: ${botName} (${platform})`);
|
|
1619
|
+
return limiter;
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Schedule a task through a bot's rate limiter
|
|
1623
|
+
*/
|
|
1624
|
+
async schedule(botName, platform, fn) {
|
|
1625
|
+
const limiter = this.getLimiter(botName, platform);
|
|
1626
|
+
return limiter.schedule(fn);
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Get queue stats for a specific bot
|
|
1630
|
+
*/
|
|
1631
|
+
getStats(botName) {
|
|
1632
|
+
const limiter = this.limiters.get(botName);
|
|
1633
|
+
if (!limiter) return null;
|
|
1634
|
+
const counts = limiter.counts();
|
|
1635
|
+
return {
|
|
1636
|
+
queued: counts.EXECUTING + counts.QUEUED,
|
|
1637
|
+
running: counts.RUNNING
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Disconnect all limiters (graceful shutdown)
|
|
1642
|
+
*/
|
|
1643
|
+
async disconnect() {
|
|
1644
|
+
const promises = Array.from(this.limiters.values()).map(
|
|
1645
|
+
(limiter) => limiter.disconnect()
|
|
1646
|
+
);
|
|
1647
|
+
await Promise.all(promises);
|
|
1648
|
+
this.limiters.clear();
|
|
1649
|
+
logger.info("All rate limiters disconnected");
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
// src/audit/logger.ts
|
|
1654
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1655
|
+
import { dirname } from "path";
|
|
1656
|
+
var AuditLogger = class {
|
|
1657
|
+
entries = [];
|
|
1658
|
+
maxSize;
|
|
1659
|
+
filePath;
|
|
1660
|
+
constructor(options = {}) {
|
|
1661
|
+
this.maxSize = options.maxSize ?? 1e3;
|
|
1662
|
+
this.filePath = options.filePath;
|
|
1663
|
+
if (this.filePath) {
|
|
1664
|
+
const dir = dirname(this.filePath);
|
|
1665
|
+
if (!existsSync2(dir)) {
|
|
1666
|
+
mkdirSync(dir, { recursive: true });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Record a message send event
|
|
1672
|
+
*/
|
|
1673
|
+
log(entry) {
|
|
1674
|
+
this.entries.push(entry);
|
|
1675
|
+
if (this.entries.length > this.maxSize) {
|
|
1676
|
+
this.entries.shift();
|
|
1677
|
+
}
|
|
1678
|
+
if (this.filePath) {
|
|
1679
|
+
try {
|
|
1680
|
+
appendFileSync(this.filePath, JSON.stringify(entry) + "\n", "utf-8");
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
logger.warn("Failed to write audit log", {
|
|
1683
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
logger.debug("Audit log entry", {
|
|
1688
|
+
id: entry.id,
|
|
1689
|
+
bot: entry.bot,
|
|
1690
|
+
platform: entry.platform,
|
|
1691
|
+
status: entry.status
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Query audit entries with filters
|
|
1696
|
+
*/
|
|
1697
|
+
query(filters = {}) {
|
|
1698
|
+
let results = [...this.entries];
|
|
1699
|
+
if (filters.messageId) {
|
|
1700
|
+
results = results.filter((e) => e.messageId === filters.messageId);
|
|
1701
|
+
}
|
|
1702
|
+
if (filters.bot) {
|
|
1703
|
+
results = results.filter((e) => e.bot === filters.bot);
|
|
1704
|
+
}
|
|
1705
|
+
if (filters.platform) {
|
|
1706
|
+
results = results.filter((e) => e.platform === filters.platform);
|
|
1707
|
+
}
|
|
1708
|
+
if (filters.status) {
|
|
1709
|
+
results = results.filter((e) => e.status === filters.status);
|
|
1710
|
+
}
|
|
1711
|
+
if (filters.since) {
|
|
1712
|
+
const sinceTime = new Date(filters.since).getTime();
|
|
1713
|
+
results = results.filter(
|
|
1714
|
+
(e) => new Date(e.timestamp).getTime() >= sinceTime
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
if (filters.until) {
|
|
1718
|
+
const untilTime = new Date(filters.until).getTime();
|
|
1719
|
+
results = results.filter(
|
|
1720
|
+
(e) => new Date(e.timestamp).getTime() <= untilTime
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
const limit = filters.limit ?? 50;
|
|
1724
|
+
return results.slice(-limit);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Get summary statistics
|
|
1728
|
+
*/
|
|
1729
|
+
stats() {
|
|
1730
|
+
const byPlatform = {};
|
|
1731
|
+
const byBot = {};
|
|
1732
|
+
let success = 0;
|
|
1733
|
+
let failed = 0;
|
|
1734
|
+
for (const entry of this.entries) {
|
|
1735
|
+
if (entry.status === "success") success++;
|
|
1736
|
+
else if (entry.status === "failed") failed++;
|
|
1737
|
+
byPlatform[entry.platform] = (byPlatform[entry.platform] || 0) + 1;
|
|
1738
|
+
byBot[entry.bot] = (byBot[entry.bot] || 0) + 1;
|
|
1739
|
+
}
|
|
1740
|
+
return {
|
|
1741
|
+
total: this.entries.length,
|
|
1742
|
+
success,
|
|
1743
|
+
failed,
|
|
1744
|
+
byPlatform,
|
|
1745
|
+
byBot
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Find entry by audit ID
|
|
1750
|
+
*/
|
|
1751
|
+
findById(id) {
|
|
1752
|
+
return this.entries.find((e) => e.id === id);
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Find entry by message ID
|
|
1756
|
+
*/
|
|
1757
|
+
findByMessageId(messageId) {
|
|
1758
|
+
return this.entries.find((e) => e.messageId === messageId);
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Clear all in-memory entries
|
|
1762
|
+
*/
|
|
1763
|
+
clear() {
|
|
1764
|
+
this.entries = [];
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
function hashContent(content, maxLen = 32) {
|
|
1768
|
+
if (content.length <= maxLen) return content;
|
|
1769
|
+
return content.slice(0, maxLen) + "...";
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// src/templates/engine.ts
|
|
1773
|
+
import Handlebars from "handlebars";
|
|
1774
|
+
var TemplateEngine = class {
|
|
1775
|
+
compiled = /* @__PURE__ */ new Map();
|
|
1776
|
+
hb;
|
|
1777
|
+
constructor() {
|
|
1778
|
+
this.hb = Handlebars.create();
|
|
1779
|
+
this.registerHelpers();
|
|
1780
|
+
}
|
|
1781
|
+
registerHelpers() {
|
|
1782
|
+
this.hb.registerHelper("date", (dateStr, format) => {
|
|
1783
|
+
const d = new Date(dateStr);
|
|
1784
|
+
if (isNaN(d.getTime())) return dateStr;
|
|
1785
|
+
if (format === "iso") return d.toISOString();
|
|
1786
|
+
if (format === "locale") return d.toLocaleString("zh-CN");
|
|
1787
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
1788
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1789
|
+
});
|
|
1790
|
+
this.hb.registerHelper(
|
|
1791
|
+
"uppercase",
|
|
1792
|
+
(str) => typeof str === "string" ? str.toUpperCase() : str
|
|
1793
|
+
);
|
|
1794
|
+
this.hb.registerHelper(
|
|
1795
|
+
"lowercase",
|
|
1796
|
+
(str) => typeof str === "string" ? str.toLowerCase() : str
|
|
1797
|
+
);
|
|
1798
|
+
this.hb.registerHelper(
|
|
1799
|
+
"capitalize",
|
|
1800
|
+
(str) => typeof str === "string" ? str.charAt(0).toUpperCase() + str.slice(1) : str
|
|
1801
|
+
);
|
|
1802
|
+
this.hb.registerHelper(
|
|
1803
|
+
"truncate",
|
|
1804
|
+
(str, maxLen, suffix) => {
|
|
1805
|
+
if (typeof str !== "string") return str;
|
|
1806
|
+
const limit = typeof maxLen === "number" ? maxLen : 100;
|
|
1807
|
+
const end = typeof suffix === "string" ? suffix : "...";
|
|
1808
|
+
return str.length > limit ? str.slice(0, limit) + end : str;
|
|
1809
|
+
}
|
|
1810
|
+
);
|
|
1811
|
+
this.hb.registerHelper(
|
|
1812
|
+
"if_eq",
|
|
1813
|
+
function(a, b, options) {
|
|
1814
|
+
return a === b ? options.fn(this) : options.inverse(this);
|
|
1815
|
+
}
|
|
1816
|
+
);
|
|
1817
|
+
this.hb.registerHelper("json", (obj, indent) => {
|
|
1818
|
+
const spaces = typeof indent === "number" ? indent : 2;
|
|
1819
|
+
return JSON.stringify(obj, null, spaces);
|
|
1820
|
+
});
|
|
1821
|
+
this.hb.registerHelper("now", (format) => {
|
|
1822
|
+
const now = /* @__PURE__ */ new Date();
|
|
1823
|
+
if (format === "iso") return now.toISOString();
|
|
1824
|
+
if (format === "timestamp") return Math.floor(now.getTime() / 1e3).toString();
|
|
1825
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
1826
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
1827
|
+
});
|
|
1828
|
+
this.hb.registerHelper(
|
|
1829
|
+
"if_gt",
|
|
1830
|
+
function(a, b, options) {
|
|
1831
|
+
return a > b ? options.fn(this) : options.inverse(this);
|
|
1832
|
+
}
|
|
1833
|
+
);
|
|
1834
|
+
this.hb.registerHelper("join", (arr, separator) => {
|
|
1835
|
+
if (!Array.isArray(arr)) return String(arr || "");
|
|
1836
|
+
const sep = typeof separator === "string" ? separator : ", ";
|
|
1837
|
+
return arr.join(sep);
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Register a partial template for reuse
|
|
1842
|
+
*/
|
|
1843
|
+
registerPartial(name, template) {
|
|
1844
|
+
this.hb.registerPartial(name, template);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Compile and cache a template string
|
|
1848
|
+
*/
|
|
1849
|
+
compile(templateStr, name) {
|
|
1850
|
+
if (name && this.compiled.has(name)) {
|
|
1851
|
+
return this.compiled.get(name);
|
|
1852
|
+
}
|
|
1853
|
+
const compiled = this.hb.compile(templateStr, { noEscape: true });
|
|
1854
|
+
if (name) this.compiled.set(name, compiled);
|
|
1855
|
+
return compiled;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Render a template string with variables
|
|
1859
|
+
*/
|
|
1860
|
+
render(templateStr, variables = {}) {
|
|
1861
|
+
const template = this.compile(templateStr);
|
|
1862
|
+
return template(variables);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Check if a string contains template expressions
|
|
1866
|
+
*/
|
|
1867
|
+
hasExpressions(templateStr) {
|
|
1868
|
+
return /\{\{[^}]+\}\}/.test(templateStr);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Extract variable names from a template string
|
|
1872
|
+
*/
|
|
1873
|
+
extractVariables(templateStr) {
|
|
1874
|
+
const matches = templateStr.match(/\{\{([^}]+)\}\}/g);
|
|
1875
|
+
if (!matches) return [];
|
|
1876
|
+
return [...new Set(matches.map((m) => m.replace(/\{\{|\}\}/g, "").trim()))];
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Clear the compiled template cache
|
|
1880
|
+
*/
|
|
1881
|
+
clearCache() {
|
|
1882
|
+
this.compiled.clear();
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
var templateEngine = new TemplateEngine();
|
|
1886
|
+
|
|
1887
|
+
// src/tools/send-message.ts
|
|
1888
|
+
function createSendMessageHandler(registry, rateLimiter, auditLogger, dryRun) {
|
|
1889
|
+
return async (args) => {
|
|
1890
|
+
const adapter = registry.get(args.bot);
|
|
1891
|
+
let content = args.content;
|
|
1892
|
+
if (args.variables && templateEngine.hasExpressions(content)) {
|
|
1893
|
+
content = templateEngine.render(content, args.variables);
|
|
1894
|
+
}
|
|
1895
|
+
let title = args.title;
|
|
1896
|
+
if (title && args.variables && templateEngine.hasExpressions(title)) {
|
|
1897
|
+
title = templateEngine.render(title, args.variables);
|
|
1898
|
+
}
|
|
1899
|
+
const sendOptions = {
|
|
1900
|
+
type: args.type,
|
|
1901
|
+
content,
|
|
1902
|
+
title,
|
|
1903
|
+
at: args.at,
|
|
1904
|
+
linkUrl: args.link_url,
|
|
1905
|
+
picUrl: args.pic_url,
|
|
1906
|
+
buttons: args.buttons,
|
|
1907
|
+
singleButtonText: args.single_button_text,
|
|
1908
|
+
singleButtonUrl: args.single_button_url,
|
|
1909
|
+
newsItems: args.news_items,
|
|
1910
|
+
imageUrl: args.image_url,
|
|
1911
|
+
imageBase64: args.image_base64
|
|
1912
|
+
};
|
|
1913
|
+
if (dryRun) {
|
|
1914
|
+
const payload = adapter.buildPayload(sendOptions);
|
|
1915
|
+
const auditId2 = `dry_${Date.now().toString(36)}`;
|
|
1916
|
+
auditLogger.log({
|
|
1917
|
+
id: auditId2,
|
|
1918
|
+
bot: args.bot,
|
|
1919
|
+
platform: adapter.platformName,
|
|
1920
|
+
type: args.type,
|
|
1921
|
+
contentHash: hashContent(content),
|
|
1922
|
+
status: "success",
|
|
1923
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1924
|
+
latencyMs: 0
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
dry_run: true,
|
|
1928
|
+
bot: args.bot,
|
|
1929
|
+
platform: adapter.platformName,
|
|
1930
|
+
payload,
|
|
1931
|
+
rendered_content: content,
|
|
1932
|
+
audit_id: auditId2
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
const platform = adapter.platformName;
|
|
1936
|
+
const result = await rateLimiter.schedule(
|
|
1937
|
+
args.bot,
|
|
1938
|
+
platform,
|
|
1939
|
+
() => adapter.send(sendOptions)
|
|
1940
|
+
);
|
|
1941
|
+
const auditId = result.messageId || `msg_${Date.now().toString(36)}`;
|
|
1942
|
+
auditLogger.log({
|
|
1943
|
+
id: auditId,
|
|
1944
|
+
messageId: result.messageId,
|
|
1945
|
+
bot: args.bot,
|
|
1946
|
+
platform: result.platform,
|
|
1947
|
+
type: args.type,
|
|
1948
|
+
contentHash: hashContent(content),
|
|
1949
|
+
status: result.success ? "success" : "failed",
|
|
1950
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1951
|
+
latencyMs: result.latencyMs,
|
|
1952
|
+
error: result.error
|
|
1953
|
+
});
|
|
1954
|
+
return {
|
|
1955
|
+
success: result.success,
|
|
1956
|
+
bot: args.bot,
|
|
1957
|
+
platform: result.platform,
|
|
1958
|
+
message_id: result.messageId,
|
|
1959
|
+
latency_ms: result.latencyMs,
|
|
1960
|
+
error: result.error,
|
|
1961
|
+
audit_id: auditId
|
|
1962
|
+
};
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/tools/send-file.ts
|
|
1967
|
+
function createSendFileHandler(registry, rateLimiter, auditLogger, dryRun) {
|
|
1968
|
+
return async (args) => {
|
|
1969
|
+
const adapter = registry.get(args.bot);
|
|
1970
|
+
if (dryRun) {
|
|
1971
|
+
const auditId2 = `dry_${Date.now().toString(36)}`;
|
|
1972
|
+
auditLogger.log({
|
|
1973
|
+
id: auditId2,
|
|
1974
|
+
bot: args.bot,
|
|
1975
|
+
platform: adapter.platformName,
|
|
1976
|
+
type: "file",
|
|
1977
|
+
contentHash: hashContent(args.file_path),
|
|
1978
|
+
status: "success",
|
|
1979
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1980
|
+
latencyMs: 0
|
|
1981
|
+
});
|
|
1982
|
+
return {
|
|
1983
|
+
dry_run: true,
|
|
1984
|
+
bot: args.bot,
|
|
1985
|
+
platform: adapter.platformName,
|
|
1986
|
+
file_path: args.file_path,
|
|
1987
|
+
audit_id: auditId2
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
const platform = adapter.platformName;
|
|
1991
|
+
const result = await rateLimiter.schedule(
|
|
1992
|
+
args.bot,
|
|
1993
|
+
platform,
|
|
1994
|
+
() => adapter.sendFile(args.file_path, args.caption)
|
|
1995
|
+
);
|
|
1996
|
+
const auditId = result.messageId || `msg_${Date.now().toString(36)}`;
|
|
1997
|
+
auditLogger.log({
|
|
1998
|
+
id: auditId,
|
|
1999
|
+
messageId: result.messageId,
|
|
2000
|
+
bot: args.bot,
|
|
2001
|
+
platform: result.platform,
|
|
2002
|
+
type: "file",
|
|
2003
|
+
contentHash: hashContent(args.file_path),
|
|
2004
|
+
status: result.success ? "success" : "failed",
|
|
2005
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2006
|
+
latencyMs: result.latencyMs,
|
|
2007
|
+
error: result.error
|
|
2008
|
+
});
|
|
2009
|
+
return {
|
|
2010
|
+
success: result.success,
|
|
2011
|
+
bot: args.bot,
|
|
2012
|
+
platform: result.platform,
|
|
2013
|
+
message_id: result.messageId,
|
|
2014
|
+
latency_ms: result.latencyMs,
|
|
2015
|
+
error: result.error,
|
|
2016
|
+
audit_id: auditId
|
|
2017
|
+
};
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/tools/send-broadcast.ts
|
|
2022
|
+
function createSendBroadcastHandler(registry, rateLimiter, auditLogger, dryRun) {
|
|
2023
|
+
return async (args) => {
|
|
2024
|
+
let content = args.content;
|
|
2025
|
+
if (args.variables && templateEngine.hasExpressions(content)) {
|
|
2026
|
+
content = templateEngine.render(content, args.variables);
|
|
2027
|
+
}
|
|
2028
|
+
let title = args.title;
|
|
2029
|
+
if (title && args.variables && templateEngine.hasExpressions(title)) {
|
|
2030
|
+
title = templateEngine.render(title, args.variables);
|
|
2031
|
+
}
|
|
2032
|
+
const sendOptions = {
|
|
2033
|
+
type: args.type,
|
|
2034
|
+
content,
|
|
2035
|
+
title,
|
|
2036
|
+
at: args.at,
|
|
2037
|
+
linkUrl: args.link_url,
|
|
2038
|
+
picUrl: args.pic_url,
|
|
2039
|
+
buttons: args.buttons,
|
|
2040
|
+
newsItems: args.news_items,
|
|
2041
|
+
imageUrl: args.image_url
|
|
2042
|
+
};
|
|
2043
|
+
const results = await Promise.allSettled(
|
|
2044
|
+
args.bots.map(async (botName) => {
|
|
2045
|
+
const adapter = registry.get(botName);
|
|
2046
|
+
const platform = adapter.platformName;
|
|
2047
|
+
if (dryRun) {
|
|
2048
|
+
return {
|
|
2049
|
+
bot: botName,
|
|
2050
|
+
platform: adapter.platformName,
|
|
2051
|
+
dry_run: true,
|
|
2052
|
+
payload: adapter.buildPayload(sendOptions)
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const result = await rateLimiter.schedule(
|
|
2056
|
+
botName,
|
|
2057
|
+
platform,
|
|
2058
|
+
() => adapter.send(sendOptions)
|
|
2059
|
+
);
|
|
2060
|
+
const auditId = result.messageId || `msg_${Date.now().toString(36)}`;
|
|
2061
|
+
auditLogger.log({
|
|
2062
|
+
id: auditId,
|
|
2063
|
+
messageId: result.messageId,
|
|
2064
|
+
bot: botName,
|
|
2065
|
+
platform: result.platform,
|
|
2066
|
+
type: args.type,
|
|
2067
|
+
contentHash: hashContent(content),
|
|
2068
|
+
status: result.success ? "success" : "failed",
|
|
2069
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2070
|
+
latencyMs: result.latencyMs,
|
|
2071
|
+
error: result.error
|
|
2072
|
+
});
|
|
2073
|
+
return {
|
|
2074
|
+
bot: botName,
|
|
2075
|
+
success: result.success,
|
|
2076
|
+
platform: result.platform,
|
|
2077
|
+
message_id: result.messageId,
|
|
2078
|
+
latency_ms: result.latencyMs,
|
|
2079
|
+
error: result.error,
|
|
2080
|
+
audit_id: auditId
|
|
2081
|
+
};
|
|
2082
|
+
})
|
|
2083
|
+
);
|
|
2084
|
+
const outcomes = results.map((r, i) => {
|
|
2085
|
+
if (r.status === "fulfilled") return r.value;
|
|
2086
|
+
return {
|
|
2087
|
+
bot: args.bots[i],
|
|
2088
|
+
success: false,
|
|
2089
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
2090
|
+
};
|
|
2091
|
+
});
|
|
2092
|
+
const successCount = outcomes.filter((o) => o.success || o.dry_run).length;
|
|
2093
|
+
const failCount = outcomes.length - successCount;
|
|
2094
|
+
return {
|
|
2095
|
+
total: outcomes.length,
|
|
2096
|
+
success: successCount,
|
|
2097
|
+
failed: failCount,
|
|
2098
|
+
results: outcomes
|
|
2099
|
+
};
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/tools/send-batch.ts
|
|
2104
|
+
function createSendBatchHandler(registry, rateLimiter, auditLogger, dryRun) {
|
|
2105
|
+
return async (args) => {
|
|
2106
|
+
const adapter = registry.get(args.bot);
|
|
2107
|
+
let content = args.content;
|
|
2108
|
+
if (args.variables && templateEngine.hasExpressions(content)) {
|
|
2109
|
+
content = templateEngine.render(content, args.variables);
|
|
2110
|
+
}
|
|
2111
|
+
const sendOptions = {
|
|
2112
|
+
type: args.type,
|
|
2113
|
+
content,
|
|
2114
|
+
title: args.title,
|
|
2115
|
+
at: args.at,
|
|
2116
|
+
linkUrl: args.link_url,
|
|
2117
|
+
picUrl: args.pic_url,
|
|
2118
|
+
buttons: args.buttons,
|
|
2119
|
+
newsItems: args.news_items,
|
|
2120
|
+
imageUrl: args.image_url
|
|
2121
|
+
};
|
|
2122
|
+
const results = await Promise.allSettled(
|
|
2123
|
+
args.targets.map(async (target) => {
|
|
2124
|
+
if (dryRun) {
|
|
2125
|
+
return {
|
|
2126
|
+
target,
|
|
2127
|
+
dry_run: true,
|
|
2128
|
+
platform: adapter.platformName
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
const platform = adapter.platformName;
|
|
2132
|
+
const result = await rateLimiter.schedule(
|
|
2133
|
+
args.bot,
|
|
2134
|
+
platform,
|
|
2135
|
+
() => adapter.send(sendOptions)
|
|
2136
|
+
);
|
|
2137
|
+
const auditId = result.messageId || `msg_${Date.now().toString(36)}`;
|
|
2138
|
+
auditLogger.log({
|
|
2139
|
+
id: auditId,
|
|
2140
|
+
messageId: result.messageId,
|
|
2141
|
+
bot: args.bot,
|
|
2142
|
+
platform: result.platform,
|
|
2143
|
+
type: args.type,
|
|
2144
|
+
contentHash: hashContent(content),
|
|
2145
|
+
status: result.success ? "success" : "failed",
|
|
2146
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2147
|
+
latencyMs: result.latencyMs,
|
|
2148
|
+
target,
|
|
2149
|
+
error: result.error
|
|
2150
|
+
});
|
|
2151
|
+
return {
|
|
2152
|
+
target,
|
|
2153
|
+
success: result.success,
|
|
2154
|
+
message_id: result.messageId,
|
|
2155
|
+
latency_ms: result.latencyMs,
|
|
2156
|
+
error: result.error,
|
|
2157
|
+
audit_id: auditId
|
|
2158
|
+
};
|
|
2159
|
+
})
|
|
2160
|
+
);
|
|
2161
|
+
const outcomes = results.map((r, i) => {
|
|
2162
|
+
if (r.status === "fulfilled") return r.value;
|
|
2163
|
+
return {
|
|
2164
|
+
target: args.targets[i],
|
|
2165
|
+
success: false,
|
|
2166
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
2167
|
+
};
|
|
2168
|
+
});
|
|
2169
|
+
const successCount = outcomes.filter((o) => o.success || o.dry_run).length;
|
|
2170
|
+
return {
|
|
2171
|
+
bot: args.bot,
|
|
2172
|
+
total: outcomes.length,
|
|
2173
|
+
success: successCount,
|
|
2174
|
+
failed: outcomes.length - successCount,
|
|
2175
|
+
results: outcomes
|
|
2176
|
+
};
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/tools/list-bots.ts
|
|
2181
|
+
function createListBotsHandler(registry, rateLimiter) {
|
|
2182
|
+
return async () => {
|
|
2183
|
+
const bots = registry.list();
|
|
2184
|
+
const detailed = bots.map((bot) => {
|
|
2185
|
+
const queueStats = rateLimiter.getStats(bot.name);
|
|
2186
|
+
return {
|
|
2187
|
+
name: bot.name,
|
|
2188
|
+
platform: bot.platform,
|
|
2189
|
+
enabled: bot.enabled,
|
|
2190
|
+
queue: queueStats || { queued: 0, running: 0 }
|
|
2191
|
+
};
|
|
2192
|
+
});
|
|
2193
|
+
return {
|
|
2194
|
+
total: detailed.length,
|
|
2195
|
+
bots: detailed,
|
|
2196
|
+
platforms: [...new Set(detailed.map((b) => b.platform))]
|
|
2197
|
+
};
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/tools/message-status.ts
|
|
2202
|
+
function createMessageStatusHandler(auditLogger) {
|
|
2203
|
+
return async (args) => {
|
|
2204
|
+
if (args.audit_id) {
|
|
2205
|
+
const entry = auditLogger.findById(args.audit_id);
|
|
2206
|
+
if (!entry) {
|
|
2207
|
+
return { found: false, message: `Audit entry "${args.audit_id}" not found` };
|
|
2208
|
+
}
|
|
2209
|
+
return { found: true, entry };
|
|
2210
|
+
}
|
|
2211
|
+
if (args.message_id) {
|
|
2212
|
+
const entry = auditLogger.findByMessageId(args.message_id);
|
|
2213
|
+
if (!entry) {
|
|
2214
|
+
return { found: false, message: `Message "${args.message_id}" not found in audit log` };
|
|
2215
|
+
}
|
|
2216
|
+
return { found: true, entry };
|
|
2217
|
+
}
|
|
2218
|
+
const entries = auditLogger.query({
|
|
2219
|
+
bot: args.bot,
|
|
2220
|
+
platform: args.platform,
|
|
2221
|
+
status: args.status,
|
|
2222
|
+
since: args.since,
|
|
2223
|
+
limit: args.limit ?? 20
|
|
2224
|
+
});
|
|
2225
|
+
const stats = auditLogger.stats();
|
|
2226
|
+
return {
|
|
2227
|
+
total_found: entries.length,
|
|
2228
|
+
entries,
|
|
2229
|
+
stats: {
|
|
2230
|
+
total_tracked: stats.total,
|
|
2231
|
+
success: stats.success,
|
|
2232
|
+
failed: stats.failed,
|
|
2233
|
+
by_platform: stats.byPlatform
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// src/tools/render-template.ts
|
|
2240
|
+
function createRenderTemplateHandler() {
|
|
2241
|
+
return async (args) => {
|
|
2242
|
+
const hasVars = templateEngine.hasExpressions(args.template);
|
|
2243
|
+
const variables = templateEngine.extractVariables(args.template);
|
|
2244
|
+
const rendered = templateEngine.render(args.template, args.variables || {});
|
|
2245
|
+
return {
|
|
2246
|
+
template: args.template,
|
|
2247
|
+
variables_detected: variables,
|
|
2248
|
+
variables_provided: Object.keys(args.variables || {}),
|
|
2249
|
+
rendered,
|
|
2250
|
+
has_expressions: hasVars
|
|
2251
|
+
};
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// src/tools/index.ts
|
|
2256
|
+
var toolDefinitions = [
|
|
2257
|
+
{
|
|
2258
|
+
name: "send_message",
|
|
2259
|
+
description: "Send a message to a configured bot. Supports text, markdown, link, image, card, news, and file types. Content can include Handlebars template variables like {{name}}.",
|
|
2260
|
+
inputSchema: {
|
|
2261
|
+
type: "object",
|
|
2262
|
+
properties: {
|
|
2263
|
+
bot: {
|
|
2264
|
+
type: "string",
|
|
2265
|
+
description: "Bot instance name (as configured, e.g. 'dingtalk-ops', 'telegram-bot')"
|
|
2266
|
+
},
|
|
2267
|
+
type: {
|
|
2268
|
+
type: "string",
|
|
2269
|
+
enum: ["text", "markdown", "link", "image", "card", "news", "file"],
|
|
2270
|
+
description: "Message type"
|
|
2271
|
+
},
|
|
2272
|
+
content: {
|
|
2273
|
+
type: "string",
|
|
2274
|
+
description: "Message content. Supports Handlebars templates: {{variable_name}}"
|
|
2275
|
+
},
|
|
2276
|
+
title: {
|
|
2277
|
+
type: "string",
|
|
2278
|
+
description: "Message title (for markdown, link, card types)"
|
|
2279
|
+
},
|
|
2280
|
+
variables: {
|
|
2281
|
+
type: "object",
|
|
2282
|
+
additionalProperties: { type: "string" },
|
|
2283
|
+
description: "Template variables as key-value pairs"
|
|
2284
|
+
},
|
|
2285
|
+
at: {
|
|
2286
|
+
type: "object",
|
|
2287
|
+
properties: {
|
|
2288
|
+
users: { type: "array", items: { type: "string" }, description: "User IDs to mention" },
|
|
2289
|
+
mobiles: { type: "array", items: { type: "string" }, description: "Phone numbers to mention" },
|
|
2290
|
+
all: { type: "boolean", description: "Mention everyone" }
|
|
2291
|
+
},
|
|
2292
|
+
description: "@mention configuration"
|
|
2293
|
+
},
|
|
2294
|
+
link_url: { type: "string", description: "URL for link type messages" },
|
|
2295
|
+
pic_url: { type: "string", description: "Picture/thumbnail URL" },
|
|
2296
|
+
buttons: {
|
|
2297
|
+
type: "array",
|
|
2298
|
+
items: {
|
|
2299
|
+
type: "object",
|
|
2300
|
+
properties: {
|
|
2301
|
+
title: { type: "string" },
|
|
2302
|
+
url: { type: "string" }
|
|
2303
|
+
},
|
|
2304
|
+
required: ["title", "url"]
|
|
2305
|
+
},
|
|
2306
|
+
description: "Buttons for card type messages"
|
|
2307
|
+
},
|
|
2308
|
+
single_button_text: { type: "string", description: "Single button text (for actionCard)" },
|
|
2309
|
+
single_button_url: { type: "string", description: "Single button URL" },
|
|
2310
|
+
news_items: {
|
|
2311
|
+
type: "array",
|
|
2312
|
+
items: {
|
|
2313
|
+
type: "object",
|
|
2314
|
+
properties: {
|
|
2315
|
+
title: { type: "string" },
|
|
2316
|
+
description: { type: "string" },
|
|
2317
|
+
url: { type: "string" },
|
|
2318
|
+
pic_url: { type: "string" }
|
|
2319
|
+
},
|
|
2320
|
+
required: ["title", "url"]
|
|
2321
|
+
},
|
|
2322
|
+
description: "News/feed items for news type messages"
|
|
2323
|
+
},
|
|
2324
|
+
image_url: { type: "string", description: "Image URL for image type messages" },
|
|
2325
|
+
image_base64: { type: "string", description: "Base64-encoded image data" }
|
|
2326
|
+
},
|
|
2327
|
+
required: ["bot", "type", "content"]
|
|
2328
|
+
}
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
name: "send_file",
|
|
2332
|
+
description: "Send a file or image through a configured bot. Supported platforms: WeCom (files via media_id), Discord (multipart upload), Telegram (sendDocument).",
|
|
2333
|
+
inputSchema: {
|
|
2334
|
+
type: "object",
|
|
2335
|
+
properties: {
|
|
2336
|
+
bot: { type: "string", description: "Bot instance name" },
|
|
2337
|
+
file_path: { type: "string", description: "Absolute path to the file to send" },
|
|
2338
|
+
caption: { type: "string", description: "Optional caption for the file" }
|
|
2339
|
+
},
|
|
2340
|
+
required: ["bot", "file_path"]
|
|
2341
|
+
}
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "send_broadcast",
|
|
2345
|
+
description: "Send the same message to multiple bots simultaneously (cross-platform broadcast). Useful for sending alerts to DingTalk + WeCom + Slack at the same time.",
|
|
2346
|
+
inputSchema: {
|
|
2347
|
+
type: "object",
|
|
2348
|
+
properties: {
|
|
2349
|
+
bots: {
|
|
2350
|
+
type: "array",
|
|
2351
|
+
items: { type: "string" },
|
|
2352
|
+
description: "List of bot instance names to broadcast to"
|
|
2353
|
+
},
|
|
2354
|
+
type: {
|
|
2355
|
+
type: "string",
|
|
2356
|
+
enum: ["text", "markdown", "link", "image", "card", "news", "file"],
|
|
2357
|
+
description: "Message type"
|
|
2358
|
+
},
|
|
2359
|
+
content: { type: "string", description: "Message content (supports Handlebars templates)" },
|
|
2360
|
+
title: { type: "string", description: "Message title" },
|
|
2361
|
+
variables: {
|
|
2362
|
+
type: "object",
|
|
2363
|
+
additionalProperties: { type: "string" },
|
|
2364
|
+
description: "Template variables"
|
|
2365
|
+
},
|
|
2366
|
+
at: {
|
|
2367
|
+
type: "object",
|
|
2368
|
+
properties: {
|
|
2369
|
+
users: { type: "array", items: { type: "string" } },
|
|
2370
|
+
mobiles: { type: "array", items: { type: "string" } },
|
|
2371
|
+
all: { type: "boolean" }
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
link_url: { type: "string" },
|
|
2375
|
+
pic_url: { type: "string" },
|
|
2376
|
+
buttons: {
|
|
2377
|
+
type: "array",
|
|
2378
|
+
items: {
|
|
2379
|
+
type: "object",
|
|
2380
|
+
properties: { title: { type: "string" }, url: { type: "string" } },
|
|
2381
|
+
required: ["title", "url"]
|
|
2382
|
+
}
|
|
2383
|
+
},
|
|
2384
|
+
news_items: {
|
|
2385
|
+
type: "array",
|
|
2386
|
+
items: {
|
|
2387
|
+
type: "object",
|
|
2388
|
+
properties: {
|
|
2389
|
+
title: { type: "string" },
|
|
2390
|
+
description: { type: "string" },
|
|
2391
|
+
url: { type: "string" },
|
|
2392
|
+
pic_url: { type: "string" }
|
|
2393
|
+
},
|
|
2394
|
+
required: ["title", "url"]
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2397
|
+
image_url: { type: "string" }
|
|
2398
|
+
},
|
|
2399
|
+
required: ["bots", "type", "content"]
|
|
2400
|
+
}
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
name: "send_batch",
|
|
2404
|
+
description: "Send the same message to multiple targets through the same bot. Useful for Telegram (multiple chat_ids) or broadcasting within one platform.",
|
|
2405
|
+
inputSchema: {
|
|
2406
|
+
type: "object",
|
|
2407
|
+
properties: {
|
|
2408
|
+
bot: { type: "string", description: "Bot instance name" },
|
|
2409
|
+
targets: {
|
|
2410
|
+
type: "array",
|
|
2411
|
+
items: { type: "string" },
|
|
2412
|
+
description: "List of target IDs (e.g. Telegram chat_ids)"
|
|
2413
|
+
},
|
|
2414
|
+
type: {
|
|
2415
|
+
type: "string",
|
|
2416
|
+
enum: ["text", "markdown", "link", "image", "card", "news", "file"],
|
|
2417
|
+
description: "Message type"
|
|
2418
|
+
},
|
|
2419
|
+
content: { type: "string", description: "Message content" },
|
|
2420
|
+
title: { type: "string" },
|
|
2421
|
+
variables: {
|
|
2422
|
+
type: "object",
|
|
2423
|
+
additionalProperties: { type: "string" }
|
|
2424
|
+
},
|
|
2425
|
+
at: {
|
|
2426
|
+
type: "object",
|
|
2427
|
+
properties: {
|
|
2428
|
+
users: { type: "array", items: { type: "string" } },
|
|
2429
|
+
mobiles: { type: "array", items: { type: "string" } },
|
|
2430
|
+
all: { type: "boolean" }
|
|
2431
|
+
}
|
|
2432
|
+
},
|
|
2433
|
+
link_url: { type: "string" },
|
|
2434
|
+
pic_url: { type: "string" },
|
|
2435
|
+
buttons: {
|
|
2436
|
+
type: "array",
|
|
2437
|
+
items: {
|
|
2438
|
+
type: "object",
|
|
2439
|
+
properties: { title: { type: "string" }, url: { type: "string" } },
|
|
2440
|
+
required: ["title", "url"]
|
|
2441
|
+
}
|
|
2442
|
+
},
|
|
2443
|
+
news_items: {
|
|
2444
|
+
type: "array",
|
|
2445
|
+
items: {
|
|
2446
|
+
type: "object",
|
|
2447
|
+
properties: {
|
|
2448
|
+
title: { type: "string" },
|
|
2449
|
+
description: { type: "string" },
|
|
2450
|
+
url: { type: "string" },
|
|
2451
|
+
pic_url: { type: "string" }
|
|
2452
|
+
},
|
|
2453
|
+
required: ["title", "url"]
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
image_url: { type: "string" }
|
|
2457
|
+
},
|
|
2458
|
+
required: ["bot", "targets", "type", "content"]
|
|
2459
|
+
}
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
name: "list_bots",
|
|
2463
|
+
description: "List all configured bot instances with their platform, status, and queue information.",
|
|
2464
|
+
inputSchema: {
|
|
2465
|
+
type: "object",
|
|
2466
|
+
properties: {}
|
|
2467
|
+
}
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
name: "message_status",
|
|
2471
|
+
description: "Query the audit log for message delivery status. Look up by message_id, audit_id, or filter by bot/platform/status/time range.",
|
|
2472
|
+
inputSchema: {
|
|
2473
|
+
type: "object",
|
|
2474
|
+
properties: {
|
|
2475
|
+
message_id: { type: "string", description: "Message ID to look up" },
|
|
2476
|
+
audit_id: { type: "string", description: "Audit log entry ID" },
|
|
2477
|
+
bot: { type: "string", description: "Filter by bot name" },
|
|
2478
|
+
platform: { type: "string", description: "Filter by platform" },
|
|
2479
|
+
status: { type: "string", enum: ["success", "failed"], description: "Filter by status" },
|
|
2480
|
+
since: { type: "string", description: "ISO timestamp to filter entries after" },
|
|
2481
|
+
limit: { type: "number", description: "Maximum entries to return (default: 20)" }
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
},
|
|
2485
|
+
{
|
|
2486
|
+
name: "render_template",
|
|
2487
|
+
description: "Preview a Handlebars template rendering without sending. Shows detected variables and rendered output.",
|
|
2488
|
+
inputSchema: {
|
|
2489
|
+
type: "object",
|
|
2490
|
+
properties: {
|
|
2491
|
+
template: {
|
|
2492
|
+
type: "string",
|
|
2493
|
+
description: "Handlebars template string (e.g. 'Hello {{name}}, status: {{status}}')"
|
|
2494
|
+
},
|
|
2495
|
+
variables: {
|
|
2496
|
+
type: "object",
|
|
2497
|
+
additionalProperties: { type: "string" },
|
|
2498
|
+
description: "Variables to substitute"
|
|
2499
|
+
}
|
|
2500
|
+
},
|
|
2501
|
+
required: ["template"]
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
];
|
|
2505
|
+
function createToolHandlers(registry, rateLimiter, auditLogger, dryRun) {
|
|
2506
|
+
return {
|
|
2507
|
+
send_message: createSendMessageHandler(registry, rateLimiter, auditLogger, dryRun),
|
|
2508
|
+
send_file: createSendFileHandler(registry, rateLimiter, auditLogger, dryRun),
|
|
2509
|
+
send_broadcast: createSendBroadcastHandler(registry, rateLimiter, auditLogger, dryRun),
|
|
2510
|
+
send_batch: createSendBatchHandler(registry, rateLimiter, auditLogger, dryRun),
|
|
2511
|
+
list_bots: createListBotsHandler(registry, rateLimiter),
|
|
2512
|
+
message_status: createMessageStatusHandler(auditLogger),
|
|
2513
|
+
render_template: createRenderTemplateHandler()
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/index.ts
|
|
2518
|
+
var VERSION = "0.1.0";
|
|
2519
|
+
async function main() {
|
|
2520
|
+
logger.info("botmsg starting...", { version: VERSION });
|
|
2521
|
+
const config = loadConfig();
|
|
2522
|
+
logger.setLevel(config.settings.log_level);
|
|
2523
|
+
if (Object.keys(config.bots).length === 0) {
|
|
2524
|
+
logger.warn("No bots configured. Use env vars or a config file. See README for details.");
|
|
2525
|
+
}
|
|
2526
|
+
const registry = new PlatformRegistry();
|
|
2527
|
+
registry.loadConfig(config);
|
|
2528
|
+
const rateLimiter = new RateLimiterManager({
|
|
2529
|
+
maxRetries: config.settings.max_retries
|
|
2530
|
+
});
|
|
2531
|
+
const auditLogger = new AuditLogger({
|
|
2532
|
+
filePath: config.settings.audit_log
|
|
2533
|
+
});
|
|
2534
|
+
const dryRun = config.settings.dry_run;
|
|
2535
|
+
if (dryRun) {
|
|
2536
|
+
logger.info("DRY RUN mode enabled - no messages will be sent");
|
|
2537
|
+
}
|
|
2538
|
+
const handlers = createToolHandlers(registry, rateLimiter, auditLogger, dryRun);
|
|
2539
|
+
const server = new Server(
|
|
2540
|
+
{ name: "botmsg", version: VERSION },
|
|
2541
|
+
{ capabilities: { tools: {} } }
|
|
2542
|
+
);
|
|
2543
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
2544
|
+
tools: toolDefinitions.map((td) => ({
|
|
2545
|
+
name: td.name,
|
|
2546
|
+
description: td.description,
|
|
2547
|
+
inputSchema: td.inputSchema
|
|
2548
|
+
}))
|
|
2549
|
+
}));
|
|
2550
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2551
|
+
const { name, arguments: args } = request.params;
|
|
2552
|
+
const startTime = Date.now();
|
|
2553
|
+
try {
|
|
2554
|
+
logger.debug(`Tool call: ${name}`, { args: JSON.stringify(args).slice(0, 200) });
|
|
2555
|
+
const handler = handlers[name];
|
|
2556
|
+
if (!handler) {
|
|
2557
|
+
return {
|
|
2558
|
+
content: [{ type: "text", text: `Error: Unknown tool "${name}"` }],
|
|
2559
|
+
isError: true
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
const result = await handler(args || {});
|
|
2563
|
+
const latencyMs = Date.now() - startTime;
|
|
2564
|
+
logger.info(`Tool ${name} completed`, { latencyMs });
|
|
2565
|
+
return {
|
|
2566
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2567
|
+
};
|
|
2568
|
+
} catch (error) {
|
|
2569
|
+
const latencyMs = Date.now() - startTime;
|
|
2570
|
+
let message;
|
|
2571
|
+
if (error instanceof BotMsgError) {
|
|
2572
|
+
message = `[${error.code}] ${error.message}`;
|
|
2573
|
+
if (error.platform) message = `[${error.platform}] ${message}`;
|
|
2574
|
+
} else if (error instanceof Error) {
|
|
2575
|
+
message = error.message;
|
|
2576
|
+
} else {
|
|
2577
|
+
message = "An unexpected error occurred";
|
|
2578
|
+
}
|
|
2579
|
+
logger.error(`Tool ${name} failed`, { latencyMs, error: message });
|
|
2580
|
+
return {
|
|
2581
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2582
|
+
isError: true
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
const transport = new StdioServerTransport();
|
|
2587
|
+
await server.connect(transport);
|
|
2588
|
+
logger.info("botmsg MCP server running on stdio", {
|
|
2589
|
+
bots: Object.keys(config.bots).length,
|
|
2590
|
+
dryRun
|
|
2591
|
+
});
|
|
2592
|
+
const shutdown = async (signal) => {
|
|
2593
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
2594
|
+
await rateLimiter.disconnect();
|
|
2595
|
+
await server.close();
|
|
2596
|
+
process.exit(0);
|
|
2597
|
+
};
|
|
2598
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
2599
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
2600
|
+
process.on("uncaughtException", (error) => {
|
|
2601
|
+
logger.error("Uncaught exception", { error: error.message, stack: error.stack });
|
|
2602
|
+
process.exit(1);
|
|
2603
|
+
});
|
|
2604
|
+
process.on("unhandledRejection", (reason) => {
|
|
2605
|
+
logger.error("Unhandled rejection", {
|
|
2606
|
+
reason: reason instanceof Error ? reason.message : String(reason)
|
|
2607
|
+
});
|
|
2608
|
+
process.exit(1);
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
main().catch((error) => {
|
|
2612
|
+
logger.error("Fatal error during startup", {
|
|
2613
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2614
|
+
});
|
|
2615
|
+
process.exit(1);
|
|
2616
|
+
});
|
|
2617
|
+
//# sourceMappingURL=index.js.map
|