@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 CHANGED
@@ -7,3 +7,4 @@ This package is consumed by channel plugin packages such as:
7
7
  - `@nextclaw/channel-plugin-telegram`
8
8
  - `@nextclaw/channel-plugin-discord`
9
9
  - `@nextclaw/channel-plugin-email`
10
+ - `@nextclaw/channel-plugin-wecom`
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
- if (!this.isAllowed(senderId)) {
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 sessionKey = `${this.name}:${chatId}`;
2436
- const session = this.sessionManager.getOrCreate(sessionKey);
2437
- const count = session.messages.length;
2438
- this.sessionManager.clear(session);
2439
- this.sessionManager.save(session);
2440
- await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${count} messages).`);
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: message.chat.type !== "private"
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.6",
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.20",
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",