@nextclaw/channel-runtime 0.1.6 → 0.1.8
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 +1 -0
- package/dist/index.d.ts +33 -1
- package/dist/index.js +450 -8
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,9 @@ declare class DiscordChannel extends BaseChannel<Config["channels"]["discord"]>
|
|
|
64
64
|
send(msg: OutboundMessage): Promise<void>;
|
|
65
65
|
private handleIncoming;
|
|
66
66
|
private resolveProxyAgent;
|
|
67
|
+
private resolveAccountId;
|
|
68
|
+
private isAllowedByPolicy;
|
|
69
|
+
private resolveMentionState;
|
|
67
70
|
private resolveInboundAttachment;
|
|
68
71
|
private startTyping;
|
|
69
72
|
private stopTyping;
|
|
@@ -192,6 +195,8 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
|
|
|
192
195
|
private sessionManager?;
|
|
193
196
|
name: string;
|
|
194
197
|
private bot;
|
|
198
|
+
private botUserId;
|
|
199
|
+
private botUsername;
|
|
195
200
|
private readonly typingController;
|
|
196
201
|
private transcriber;
|
|
197
202
|
constructor(config: Config["channels"]["telegram"], bus: MessageBus, groqApiKey?: string, sessionManager?: SessionManager | undefined);
|
|
@@ -202,6 +207,28 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
|
|
|
202
207
|
private dispatchToBus;
|
|
203
208
|
private startTyping;
|
|
204
209
|
private stopTyping;
|
|
210
|
+
private resolveAccountId;
|
|
211
|
+
private isAllowedByPolicy;
|
|
212
|
+
private resolveMentionState;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
declare class WeComChannel extends BaseChannel<Config["channels"]["wecom"]> {
|
|
216
|
+
name: string;
|
|
217
|
+
private server;
|
|
218
|
+
private accessToken;
|
|
219
|
+
private tokenExpiry;
|
|
220
|
+
private processedIds;
|
|
221
|
+
private processedSet;
|
|
222
|
+
constructor(config: Config["channels"]["wecom"], bus: MessageBus);
|
|
223
|
+
start(): Promise<void>;
|
|
224
|
+
stop(): Promise<void>;
|
|
225
|
+
send(msg: OutboundMessage): Promise<void>;
|
|
226
|
+
private handleCallbackRequest;
|
|
227
|
+
private handleVerificationRequest;
|
|
228
|
+
private handleInboundMessageRequest;
|
|
229
|
+
private verifySignature;
|
|
230
|
+
private getAccessToken;
|
|
231
|
+
private isDuplicate;
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
declare class WhatsAppChannel extends BaseChannel<Config["channels"]["whatsapp"]> {
|
|
@@ -256,6 +283,11 @@ declare const BUILTIN_CHANNEL_RUNTIMES: {
|
|
|
256
283
|
readonly isEnabled: (config: Config) => boolean;
|
|
257
284
|
readonly createChannel: (context: BuiltinChannelCreateContext) => DingTalkChannel;
|
|
258
285
|
};
|
|
286
|
+
readonly wecom: {
|
|
287
|
+
readonly id: "wecom";
|
|
288
|
+
readonly isEnabled: (config: Config) => boolean;
|
|
289
|
+
readonly createChannel: (context: BuiltinChannelCreateContext) => WeComChannel;
|
|
290
|
+
};
|
|
259
291
|
readonly email: {
|
|
260
292
|
readonly id: "email";
|
|
261
293
|
readonly isEnabled: (config: Config) => boolean;
|
|
@@ -277,4 +309,4 @@ declare const BUILTIN_CHANNEL_PLUGIN_IDS: BuiltinChannelId[];
|
|
|
277
309
|
declare function listBuiltinChannelRuntimes(): BuiltinChannelRuntime[];
|
|
278
310
|
declare function resolveBuiltinChannelRuntime(channelId: string): BuiltinChannelRuntime;
|
|
279
311
|
|
|
280
|
-
export { BUILTIN_CHANNEL_PLUGIN_IDS, type BuiltinChannelId, type BuiltinChannelRuntime, DingTalkChannel, DiscordChannel, EmailChannel, FeishuChannel, MochatChannel, QQChannel, SlackChannel, TelegramChannel, WhatsAppChannel, listBuiltinChannelRuntimes, resolveBuiltinChannelRuntime };
|
|
312
|
+
export { BUILTIN_CHANNEL_PLUGIN_IDS, type BuiltinChannelId, type BuiltinChannelRuntime, DingTalkChannel, DiscordChannel, EmailChannel, FeishuChannel, MochatChannel, QQChannel, SlackChannel, TelegramChannel, WeComChannel, WhatsAppChannel, listBuiltinChannelRuntimes, resolveBuiltinChannelRuntime };
|
package/dist/index.js
CHANGED
|
@@ -316,7 +316,12 @@ var DiscordChannel = class extends BaseChannel {
|
|
|
316
316
|
}
|
|
317
317
|
const senderId = message.author.id;
|
|
318
318
|
const channelId = message.channelId;
|
|
319
|
-
|
|
319
|
+
const isGroup = Boolean(message.guildId);
|
|
320
|
+
if (!this.isAllowedByPolicy({ senderId, channelId, isGroup })) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const mentionState = this.resolveMentionState({ message, selfUserId, channelId, isGroup });
|
|
324
|
+
if (mentionState.requireMention && !mentionState.wasMentioned) {
|
|
320
325
|
return;
|
|
321
326
|
}
|
|
322
327
|
const contentParts = [];
|
|
@@ -358,8 +363,16 @@ var DiscordChannel = class extends BaseChannel {
|
|
|
358
363
|
attachments,
|
|
359
364
|
metadata: {
|
|
360
365
|
message_id: message.id,
|
|
366
|
+
channel_id: channelId,
|
|
361
367
|
guild_id: message.guildId,
|
|
362
368
|
reply_to: replyTo,
|
|
369
|
+
account_id: this.resolveAccountId(),
|
|
370
|
+
accountId: this.resolveAccountId(),
|
|
371
|
+
is_group: isGroup,
|
|
372
|
+
peer_kind: isGroup ? "channel" : "direct",
|
|
373
|
+
peer_id: isGroup ? channelId : senderId,
|
|
374
|
+
was_mentioned: mentionState.wasMentioned,
|
|
375
|
+
require_mention: mentionState.requireMention,
|
|
363
376
|
...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
|
|
364
377
|
}
|
|
365
378
|
});
|
|
@@ -378,6 +391,62 @@ var DiscordChannel = class extends BaseChannel {
|
|
|
378
391
|
return null;
|
|
379
392
|
}
|
|
380
393
|
}
|
|
394
|
+
resolveAccountId() {
|
|
395
|
+
const accountId = this.config.accountId?.trim();
|
|
396
|
+
return accountId || "default";
|
|
397
|
+
}
|
|
398
|
+
isAllowedByPolicy(params) {
|
|
399
|
+
if (!params.isGroup) {
|
|
400
|
+
if (this.config.dmPolicy === "disabled") {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const allowFrom = this.config.allowFrom ?? [];
|
|
404
|
+
if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") {
|
|
405
|
+
return this.isAllowed(params.senderId);
|
|
406
|
+
}
|
|
407
|
+
if (allowFrom.includes("*")) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return allowFrom.length === 0 ? true : this.isAllowed(params.senderId);
|
|
411
|
+
}
|
|
412
|
+
if (this.config.groupPolicy === "disabled") {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
if (this.config.groupPolicy === "allowlist") {
|
|
416
|
+
const allowFrom = this.config.groupAllowFrom ?? [];
|
|
417
|
+
return allowFrom.includes("*") || allowFrom.includes(params.channelId);
|
|
418
|
+
}
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
resolveMentionState(params) {
|
|
422
|
+
if (!params.isGroup) {
|
|
423
|
+
return { wasMentioned: false, requireMention: false };
|
|
424
|
+
}
|
|
425
|
+
const groups = this.config.groups ?? {};
|
|
426
|
+
const groupRule = groups[params.channelId] ?? groups["*"];
|
|
427
|
+
const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
|
|
428
|
+
if (!requireMention) {
|
|
429
|
+
return { wasMentioned: false, requireMention: false };
|
|
430
|
+
}
|
|
431
|
+
const patterns = [
|
|
432
|
+
...this.config.mentionPatterns ?? [],
|
|
433
|
+
...groupRule?.mentionPatterns ?? []
|
|
434
|
+
].map((pattern) => pattern.trim()).filter(Boolean);
|
|
435
|
+
const content = params.message.content ?? "";
|
|
436
|
+
const wasMentionedByUserRef = Boolean(params.selfUserId) && params.message.mentions.users.has(params.selfUserId ?? "");
|
|
437
|
+
const wasMentionedByText = Boolean(params.selfUserId) && (content.includes(`<@${params.selfUserId}>`) || content.includes(`<@!${params.selfUserId}>`));
|
|
438
|
+
const wasMentionedByPattern = patterns.some((pattern) => {
|
|
439
|
+
try {
|
|
440
|
+
return new RegExp(pattern, "i").test(content);
|
|
441
|
+
} catch {
|
|
442
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
wasMentioned: wasMentionedByUserRef || wasMentionedByText || wasMentionedByPattern,
|
|
447
|
+
requireMention
|
|
448
|
+
};
|
|
449
|
+
}
|
|
381
450
|
async resolveInboundAttachment(params) {
|
|
382
451
|
const { attachment, mediaDir, maxBytes, proxy } = params;
|
|
383
452
|
const id = attachment.id;
|
|
@@ -2395,6 +2464,8 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2395
2464
|
}
|
|
2396
2465
|
name = "telegram";
|
|
2397
2466
|
bot = null;
|
|
2467
|
+
botUserId = null;
|
|
2468
|
+
botUsername = null;
|
|
2398
2469
|
typingController;
|
|
2399
2470
|
transcriber;
|
|
2400
2471
|
async start() {
|
|
@@ -2407,6 +2478,14 @@ var TelegramChannel = class extends BaseChannel {
|
|
|
2407
2478
|
options.request = { proxy: this.config.proxy };
|
|
2408
2479
|
}
|
|
2409
2480
|
this.bot = new TelegramBot(this.config.token, options);
|
|
2481
|
+
try {
|
|
2482
|
+
const me = await this.bot.getMe();
|
|
2483
|
+
this.botUserId = me.id;
|
|
2484
|
+
this.botUsername = me.username ?? null;
|
|
2485
|
+
} catch {
|
|
2486
|
+
this.botUserId = null;
|
|
2487
|
+
this.botUsername = null;
|
|
2488
|
+
}
|
|
2410
2489
|
this.bot.onText(/^\/start$/, async (msg) => {
|
|
2411
2490
|
await this.bot?.sendMessage(
|
|
2412
2491
|
msg.chat.id,
|
|
@@ -2432,12 +2511,31 @@ Just send me a text message to chat!`;
|
|
|
2432
2511
|
await this.bot?.sendMessage(msg.chat.id, "\u26A0\uFE0F Session management is not available.");
|
|
2433
2512
|
return;
|
|
2434
2513
|
}
|
|
2435
|
-
const
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2514
|
+
const accountId = this.resolveAccountId();
|
|
2515
|
+
const candidates = this.sessionManager.listSessions().filter((entry) => {
|
|
2516
|
+
const metadata = entry.metadata ?? {};
|
|
2517
|
+
const lastChannel = typeof metadata.last_channel === "string" ? metadata.last_channel : "";
|
|
2518
|
+
const lastTo = typeof metadata.last_to === "string" ? metadata.last_to : "";
|
|
2519
|
+
const lastAccountId = typeof metadata.last_account_id === "string" ? metadata.last_account_id : typeof metadata.last_accountId === "string" ? metadata.last_accountId : "default";
|
|
2520
|
+
return lastChannel === this.name && lastTo === chatId && lastAccountId === accountId;
|
|
2521
|
+
}).map((entry) => String(entry.key ?? "")).filter(Boolean);
|
|
2522
|
+
let totalCleared = 0;
|
|
2523
|
+
for (const key of candidates) {
|
|
2524
|
+
const session = this.sessionManager.getIfExists(key);
|
|
2525
|
+
if (!session) {
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
totalCleared += session.messages.length;
|
|
2529
|
+
this.sessionManager.clear(session);
|
|
2530
|
+
this.sessionManager.save(session);
|
|
2531
|
+
}
|
|
2532
|
+
if (candidates.length === 0) {
|
|
2533
|
+
const legacySession = this.sessionManager.getOrCreate(`${this.name}:${chatId}`);
|
|
2534
|
+
totalCleared = legacySession.messages.length;
|
|
2535
|
+
this.sessionManager.clear(legacySession);
|
|
2536
|
+
this.sessionManager.save(legacySession);
|
|
2537
|
+
}
|
|
2538
|
+
await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${totalCleared} messages).`);
|
|
2441
2539
|
});
|
|
2442
2540
|
this.bot.on("message", async (msg) => {
|
|
2443
2541
|
if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
|
|
@@ -2498,6 +2596,14 @@ Just send me a text message to chat!`;
|
|
|
2498
2596
|
return;
|
|
2499
2597
|
}
|
|
2500
2598
|
const chatId = String(message.chat.id);
|
|
2599
|
+
const isGroup = message.chat.type !== "private";
|
|
2600
|
+
if (!this.isAllowedByPolicy({ senderId: String(sender.id), chatId, isGroup })) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const mentionState = this.resolveMentionState({ message, chatId, isGroup });
|
|
2604
|
+
if (mentionState.requireMention && !mentionState.wasMentioned) {
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2501
2607
|
let senderId = String(sender.id);
|
|
2502
2608
|
if (sender.username) {
|
|
2503
2609
|
senderId = `${senderId}|${sender.username}`;
|
|
@@ -2546,7 +2652,13 @@ Just send me a text message to chat!`;
|
|
|
2546
2652
|
first_name: sender.firstName,
|
|
2547
2653
|
sender_type: sender.type,
|
|
2548
2654
|
is_bot: sender.isBot,
|
|
2549
|
-
is_group:
|
|
2655
|
+
is_group: isGroup,
|
|
2656
|
+
account_id: this.resolveAccountId(),
|
|
2657
|
+
accountId: this.resolveAccountId(),
|
|
2658
|
+
peer_kind: isGroup ? "group" : "direct",
|
|
2659
|
+
peer_id: isGroup ? chatId : String(sender.id),
|
|
2660
|
+
was_mentioned: mentionState.wasMentioned,
|
|
2661
|
+
require_mention: mentionState.requireMention
|
|
2550
2662
|
});
|
|
2551
2663
|
} finally {
|
|
2552
2664
|
this.stopTyping(chatId);
|
|
@@ -2561,6 +2673,63 @@ Just send me a text message to chat!`;
|
|
|
2561
2673
|
stopTyping(chatId) {
|
|
2562
2674
|
this.typingController.stop(chatId);
|
|
2563
2675
|
}
|
|
2676
|
+
resolveAccountId() {
|
|
2677
|
+
const accountId = this.config.accountId?.trim();
|
|
2678
|
+
return accountId || "default";
|
|
2679
|
+
}
|
|
2680
|
+
isAllowedByPolicy(params) {
|
|
2681
|
+
if (!params.isGroup) {
|
|
2682
|
+
if (this.config.dmPolicy === "disabled") {
|
|
2683
|
+
return false;
|
|
2684
|
+
}
|
|
2685
|
+
const allowFrom = this.config.allowFrom ?? [];
|
|
2686
|
+
if (this.config.dmPolicy === "allowlist" || this.config.dmPolicy === "pairing") {
|
|
2687
|
+
return this.isAllowed(params.senderId);
|
|
2688
|
+
}
|
|
2689
|
+
if (allowFrom.includes("*")) {
|
|
2690
|
+
return true;
|
|
2691
|
+
}
|
|
2692
|
+
return allowFrom.length === 0 ? true : this.isAllowed(params.senderId);
|
|
2693
|
+
}
|
|
2694
|
+
if (this.config.groupPolicy === "disabled") {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
if (this.config.groupPolicy === "allowlist") {
|
|
2698
|
+
const allowFrom = this.config.groupAllowFrom ?? [];
|
|
2699
|
+
return allowFrom.includes("*") || allowFrom.includes(params.chatId);
|
|
2700
|
+
}
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
resolveMentionState(params) {
|
|
2704
|
+
if (!params.isGroup) {
|
|
2705
|
+
return { wasMentioned: false, requireMention: false };
|
|
2706
|
+
}
|
|
2707
|
+
const groups = this.config.groups ?? {};
|
|
2708
|
+
const groupRule = groups[params.chatId] ?? groups["*"];
|
|
2709
|
+
const requireMention = groupRule?.requireMention ?? this.config.requireMention ?? false;
|
|
2710
|
+
if (!requireMention) {
|
|
2711
|
+
return { wasMentioned: false, requireMention: false };
|
|
2712
|
+
}
|
|
2713
|
+
const content = `${params.message.text ?? ""}
|
|
2714
|
+
${params.message.caption ?? ""}`.trim();
|
|
2715
|
+
const patterns = [
|
|
2716
|
+
...this.config.mentionPatterns ?? [],
|
|
2717
|
+
...groupRule?.mentionPatterns ?? []
|
|
2718
|
+
].map((pattern) => pattern.trim()).filter(Boolean);
|
|
2719
|
+
const usernameMentioned = this.botUsername ? content.includes(`@${this.botUsername}`) : false;
|
|
2720
|
+
const replyToBot = Boolean(this.botUserId) && Boolean(params.message.reply_to_message?.from) && params.message.reply_to_message?.from?.id === this.botUserId;
|
|
2721
|
+
const patternMentioned = patterns.some((pattern) => {
|
|
2722
|
+
try {
|
|
2723
|
+
return new RegExp(pattern, "i").test(content);
|
|
2724
|
+
} catch {
|
|
2725
|
+
return content.toLowerCase().includes(pattern.toLowerCase());
|
|
2726
|
+
}
|
|
2727
|
+
});
|
|
2728
|
+
return {
|
|
2729
|
+
wasMentioned: usernameMentioned || replyToBot || patternMentioned,
|
|
2730
|
+
requireMention
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2564
2733
|
};
|
|
2565
2734
|
function resolveSender(message) {
|
|
2566
2735
|
if (message.from) {
|
|
@@ -2668,6 +2837,273 @@ function markdownToTelegramHtml(text) {
|
|
|
2668
2837
|
return text;
|
|
2669
2838
|
}
|
|
2670
2839
|
|
|
2840
|
+
// src/channels/wecom.ts
|
|
2841
|
+
import { createHash } from "crypto";
|
|
2842
|
+
import { createServer } from "http";
|
|
2843
|
+
import { fetch as fetch5 } from "undici";
|
|
2844
|
+
var MSG_TYPE_FALLBACK = {
|
|
2845
|
+
image: "[image]",
|
|
2846
|
+
voice: "[voice]",
|
|
2847
|
+
video: "[video]",
|
|
2848
|
+
file: "[file]",
|
|
2849
|
+
location: "[location]",
|
|
2850
|
+
event: "[event]"
|
|
2851
|
+
};
|
|
2852
|
+
var TOKEN_EXPIRY_BUFFER_MS = 6e4;
|
|
2853
|
+
var WeComChannel = class extends BaseChannel {
|
|
2854
|
+
name = "wecom";
|
|
2855
|
+
server = null;
|
|
2856
|
+
accessToken = null;
|
|
2857
|
+
tokenExpiry = 0;
|
|
2858
|
+
processedIds = [];
|
|
2859
|
+
processedSet = /* @__PURE__ */ new Set();
|
|
2860
|
+
constructor(config, bus) {
|
|
2861
|
+
super(config, bus);
|
|
2862
|
+
}
|
|
2863
|
+
async start() {
|
|
2864
|
+
if (!this.config.corpId || !this.config.agentId || !this.config.secret || !this.config.token) {
|
|
2865
|
+
throw new Error("WeCom corpId/agentId/secret/token not configured");
|
|
2866
|
+
}
|
|
2867
|
+
this.running = true;
|
|
2868
|
+
await new Promise((resolve, reject) => {
|
|
2869
|
+
this.server = createServer((req, res) => {
|
|
2870
|
+
void this.handleCallbackRequest(req, res);
|
|
2871
|
+
});
|
|
2872
|
+
this.server.once("error", reject);
|
|
2873
|
+
this.server.listen(this.config.callbackPort, () => {
|
|
2874
|
+
this.server?.off("error", reject);
|
|
2875
|
+
resolve();
|
|
2876
|
+
});
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
async stop() {
|
|
2880
|
+
this.running = false;
|
|
2881
|
+
if (!this.server) {
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
await new Promise((resolve) => {
|
|
2885
|
+
this.server?.close(() => resolve());
|
|
2886
|
+
});
|
|
2887
|
+
this.server = null;
|
|
2888
|
+
}
|
|
2889
|
+
async send(msg) {
|
|
2890
|
+
const receiver = msg.chatId?.trim();
|
|
2891
|
+
if (!receiver) {
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
const content = normalizeOutboundContent(msg);
|
|
2895
|
+
if (!content) {
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const accessToken = await this.getAccessToken();
|
|
2899
|
+
const sendUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(accessToken)}`;
|
|
2900
|
+
const agentIdNumber = Number(this.config.agentId);
|
|
2901
|
+
const payload = {
|
|
2902
|
+
touser: receiver,
|
|
2903
|
+
msgtype: "text",
|
|
2904
|
+
agentid: Number.isFinite(agentIdNumber) ? agentIdNumber : this.config.agentId,
|
|
2905
|
+
text: {
|
|
2906
|
+
content
|
|
2907
|
+
},
|
|
2908
|
+
safe: 0
|
|
2909
|
+
};
|
|
2910
|
+
const response = await fetch5(sendUrl, {
|
|
2911
|
+
method: "POST",
|
|
2912
|
+
headers: {
|
|
2913
|
+
"content-type": "application/json"
|
|
2914
|
+
},
|
|
2915
|
+
body: JSON.stringify(payload)
|
|
2916
|
+
});
|
|
2917
|
+
if (!response.ok) {
|
|
2918
|
+
throw new Error(`WeCom send failed: HTTP ${response.status}`);
|
|
2919
|
+
}
|
|
2920
|
+
const body = await response.json();
|
|
2921
|
+
const errcode = Number(body.errcode ?? -1);
|
|
2922
|
+
if (!Number.isFinite(errcode) || errcode !== 0) {
|
|
2923
|
+
const errmsg = typeof body.errmsg === "string" ? body.errmsg : "unknown error";
|
|
2924
|
+
throw new Error(`WeCom send failed: ${errcode} ${errmsg}`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
async handleCallbackRequest(req, res) {
|
|
2928
|
+
const callbackPath = normalizeCallbackPath(this.config.callbackPath);
|
|
2929
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
|
|
2930
|
+
if (requestUrl.pathname !== callbackPath) {
|
|
2931
|
+
res.statusCode = 404;
|
|
2932
|
+
res.end("not found");
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2936
|
+
if (method === "GET") {
|
|
2937
|
+
this.handleVerificationRequest(requestUrl, res);
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
if (method === "POST") {
|
|
2941
|
+
await this.handleInboundMessageRequest(req, requestUrl, res);
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
res.statusCode = 405;
|
|
2945
|
+
res.end("method not allowed");
|
|
2946
|
+
}
|
|
2947
|
+
handleVerificationRequest(requestUrl, res) {
|
|
2948
|
+
const timestamp = requestUrl.searchParams.get("timestamp") ?? "";
|
|
2949
|
+
const nonce = requestUrl.searchParams.get("nonce") ?? "";
|
|
2950
|
+
const echostr = requestUrl.searchParams.get("echostr") ?? "";
|
|
2951
|
+
const signature = requestUrl.searchParams.get("msg_signature") ?? requestUrl.searchParams.get("signature") ?? "";
|
|
2952
|
+
if (!timestamp || !nonce || !echostr || !signature) {
|
|
2953
|
+
res.statusCode = 400;
|
|
2954
|
+
res.end("invalid verification payload");
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
if (!this.verifySignature(timestamp, nonce, signature)) {
|
|
2958
|
+
res.statusCode = 401;
|
|
2959
|
+
res.end("signature mismatch");
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
res.statusCode = 200;
|
|
2963
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
2964
|
+
res.end(echostr);
|
|
2965
|
+
}
|
|
2966
|
+
async handleInboundMessageRequest(req, requestUrl, res) {
|
|
2967
|
+
const timestamp = requestUrl.searchParams.get("timestamp") ?? "";
|
|
2968
|
+
const nonce = requestUrl.searchParams.get("nonce") ?? "";
|
|
2969
|
+
const signature = requestUrl.searchParams.get("msg_signature") ?? requestUrl.searchParams.get("signature") ?? "";
|
|
2970
|
+
if (!timestamp || !nonce || !signature || !this.verifySignature(timestamp, nonce, signature)) {
|
|
2971
|
+
res.statusCode = 401;
|
|
2972
|
+
res.end("signature mismatch");
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
const rawBody = await readBody(req);
|
|
2976
|
+
if (!rawBody.trim()) {
|
|
2977
|
+
respondSuccess(res);
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
if (extractXmlField(rawBody, "Encrypt")) {
|
|
2981
|
+
respondSuccess(res);
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
const senderId = extractXmlField(rawBody, "FromUserName");
|
|
2985
|
+
if (!senderId || !this.isAllowed(senderId)) {
|
|
2986
|
+
respondSuccess(res);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
const msgType = extractXmlField(rawBody, "MsgType") || "text";
|
|
2990
|
+
const msgId = extractXmlField(rawBody, "MsgId") || buildSyntheticMessageId(rawBody, senderId, msgType);
|
|
2991
|
+
if (this.isDuplicate(msgId)) {
|
|
2992
|
+
respondSuccess(res);
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
const content = extractXmlField(rawBody, "Content")?.trim() || MSG_TYPE_FALLBACK[msgType.toLowerCase()] || "[unsupported message]";
|
|
2996
|
+
await this.handleMessage({
|
|
2997
|
+
senderId,
|
|
2998
|
+
chatId: senderId,
|
|
2999
|
+
content,
|
|
3000
|
+
attachments: [],
|
|
3001
|
+
metadata: {
|
|
3002
|
+
message_id: msgId,
|
|
3003
|
+
wecom: {
|
|
3004
|
+
msgType,
|
|
3005
|
+
toUserName: extractXmlField(rawBody, "ToUserName"),
|
|
3006
|
+
agentId: extractXmlField(rawBody, "AgentID"),
|
|
3007
|
+
createTime: extractXmlField(rawBody, "CreateTime")
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
});
|
|
3011
|
+
respondSuccess(res);
|
|
3012
|
+
}
|
|
3013
|
+
verifySignature(timestamp, nonce, signature) {
|
|
3014
|
+
const expected = createHash("sha1").update([this.config.token, timestamp, nonce].sort().join("")).digest("hex");
|
|
3015
|
+
return expected === signature;
|
|
3016
|
+
}
|
|
3017
|
+
async getAccessToken() {
|
|
3018
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
3019
|
+
return this.accessToken;
|
|
3020
|
+
}
|
|
3021
|
+
const tokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(this.config.corpId)}&corpsecret=${encodeURIComponent(this.config.secret)}`;
|
|
3022
|
+
const response = await fetch5(tokenUrl, { method: "GET" });
|
|
3023
|
+
if (!response.ok) {
|
|
3024
|
+
throw new Error(`WeCom gettoken failed: HTTP ${response.status}`);
|
|
3025
|
+
}
|
|
3026
|
+
const body = await response.json();
|
|
3027
|
+
const errcode = Number(body.errcode ?? -1);
|
|
3028
|
+
if (!Number.isFinite(errcode) || errcode !== 0) {
|
|
3029
|
+
const errmsg = typeof body.errmsg === "string" ? body.errmsg : "unknown error";
|
|
3030
|
+
throw new Error(`WeCom gettoken failed: ${errcode} ${errmsg}`);
|
|
3031
|
+
}
|
|
3032
|
+
const accessToken = typeof body.access_token === "string" ? body.access_token : "";
|
|
3033
|
+
const expiresIn = Number(body.expires_in ?? 7200);
|
|
3034
|
+
if (!accessToken) {
|
|
3035
|
+
throw new Error("WeCom gettoken failed: missing access_token");
|
|
3036
|
+
}
|
|
3037
|
+
this.accessToken = accessToken;
|
|
3038
|
+
this.tokenExpiry = Date.now() + Math.max(0, expiresIn * 1e3 - TOKEN_EXPIRY_BUFFER_MS);
|
|
3039
|
+
return accessToken;
|
|
3040
|
+
}
|
|
3041
|
+
isDuplicate(messageId) {
|
|
3042
|
+
if (this.processedSet.has(messageId)) {
|
|
3043
|
+
return true;
|
|
3044
|
+
}
|
|
3045
|
+
this.processedSet.add(messageId);
|
|
3046
|
+
this.processedIds.push(messageId);
|
|
3047
|
+
if (this.processedIds.length > 1e3) {
|
|
3048
|
+
const removed = this.processedIds.splice(0, 500);
|
|
3049
|
+
for (const id of removed) {
|
|
3050
|
+
this.processedSet.delete(id);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
return false;
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
function normalizeCallbackPath(path) {
|
|
3057
|
+
if (!path) {
|
|
3058
|
+
return "/wecom/callback";
|
|
3059
|
+
}
|
|
3060
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
3061
|
+
}
|
|
3062
|
+
function respondSuccess(res) {
|
|
3063
|
+
res.statusCode = 200;
|
|
3064
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
3065
|
+
res.end("success");
|
|
3066
|
+
}
|
|
3067
|
+
function buildSyntheticMessageId(rawBody, senderId, msgType) {
|
|
3068
|
+
return createHash("sha1").update(`${senderId}:${msgType}:${rawBody}`).digest("hex");
|
|
3069
|
+
}
|
|
3070
|
+
function extractXmlField(xml, field) {
|
|
3071
|
+
const cdataPattern = new RegExp(`<${field}><!\\[CDATA\\[(.*?)\\]\\]><\\/${field}>`, "s");
|
|
3072
|
+
const cdataMatch = xml.match(cdataPattern);
|
|
3073
|
+
if (cdataMatch?.[1]) {
|
|
3074
|
+
return cdataMatch[1].trim();
|
|
3075
|
+
}
|
|
3076
|
+
const textPattern = new RegExp(`<${field}>(.*?)<\\/${field}>`, "s");
|
|
3077
|
+
const textMatch = xml.match(textPattern);
|
|
3078
|
+
return textMatch?.[1]?.trim() ?? "";
|
|
3079
|
+
}
|
|
3080
|
+
async function readBody(req) {
|
|
3081
|
+
const chunks = [];
|
|
3082
|
+
return await new Promise((resolve, reject) => {
|
|
3083
|
+
req.on("data", (chunk) => {
|
|
3084
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3085
|
+
});
|
|
3086
|
+
req.on("error", reject);
|
|
3087
|
+
req.on("end", () => {
|
|
3088
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
3089
|
+
});
|
|
3090
|
+
});
|
|
3091
|
+
}
|
|
3092
|
+
function normalizeOutboundContent(msg) {
|
|
3093
|
+
const segments = [];
|
|
3094
|
+
if (typeof msg.content === "string" && msg.content.trim()) {
|
|
3095
|
+
segments.push(msg.content.trim());
|
|
3096
|
+
}
|
|
3097
|
+
if (Array.isArray(msg.media)) {
|
|
3098
|
+
for (const item of msg.media) {
|
|
3099
|
+
if (typeof item === "string" && item.trim()) {
|
|
3100
|
+
segments.push(item.trim());
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return segments.join("\n").trim();
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2671
3107
|
// src/channels/whatsapp.ts
|
|
2672
3108
|
import WebSocket from "ws";
|
|
2673
3109
|
var WhatsAppChannel = class extends BaseChannel {
|
|
@@ -2818,6 +3254,11 @@ var BUILTIN_CHANNEL_RUNTIMES = {
|
|
|
2818
3254
|
isEnabled: (config) => config.channels.dingtalk.enabled,
|
|
2819
3255
|
createChannel: (context) => new DingTalkChannel(context.config.channels.dingtalk, context.bus)
|
|
2820
3256
|
},
|
|
3257
|
+
wecom: {
|
|
3258
|
+
id: "wecom",
|
|
3259
|
+
isEnabled: (config) => config.channels.wecom.enabled,
|
|
3260
|
+
createChannel: (context) => new WeComChannel(context.config.channels.wecom, context.bus)
|
|
3261
|
+
},
|
|
2821
3262
|
email: {
|
|
2822
3263
|
id: "email",
|
|
2823
3264
|
isEnabled: (config) => config.channels.email.enabled,
|
|
@@ -2857,6 +3298,7 @@ export {
|
|
|
2857
3298
|
QQChannel,
|
|
2858
3299
|
SlackChannel,
|
|
2859
3300
|
TelegramChannel,
|
|
3301
|
+
WeComChannel,
|
|
2860
3302
|
WhatsAppChannel,
|
|
2861
3303
|
listBuiltinChannelRuntimes,
|
|
2862
3304
|
resolveBuiltinChannelRuntime
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/channel-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Runtime implementations for NextClaw builtin channel plugins.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@larksuiteoapi/node-sdk": "^1.58.0",
|
|
18
|
-
"@nextclaw/core": "^0.6.
|
|
18
|
+
"@nextclaw/core": "^0.6.22",
|
|
19
19
|
"@slack/socket-mode": "^1.3.3",
|
|
20
20
|
"@slack/web-api": "^7.6.0",
|
|
21
21
|
"dingtalk-stream": "^2.1.4",
|