@nextclaw/channel-runtime 0.1.5 → 0.1.7

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
@@ -204,6 +204,25 @@ declare class TelegramChannel extends BaseChannel<Config["channels"]["telegram"]
204
204
  private stopTyping;
205
205
  }
206
206
 
207
+ declare class WeComChannel extends BaseChannel<Config["channels"]["wecom"]> {
208
+ name: string;
209
+ private server;
210
+ private accessToken;
211
+ private tokenExpiry;
212
+ private processedIds;
213
+ private processedSet;
214
+ constructor(config: Config["channels"]["wecom"], bus: MessageBus);
215
+ start(): Promise<void>;
216
+ stop(): Promise<void>;
217
+ send(msg: OutboundMessage): Promise<void>;
218
+ private handleCallbackRequest;
219
+ private handleVerificationRequest;
220
+ private handleInboundMessageRequest;
221
+ private verifySignature;
222
+ private getAccessToken;
223
+ private isDuplicate;
224
+ }
225
+
207
226
  declare class WhatsAppChannel extends BaseChannel<Config["channels"]["whatsapp"]> {
208
227
  name: string;
209
228
  private ws;
@@ -256,6 +275,11 @@ declare const BUILTIN_CHANNEL_RUNTIMES: {
256
275
  readonly isEnabled: (config: Config) => boolean;
257
276
  readonly createChannel: (context: BuiltinChannelCreateContext) => DingTalkChannel;
258
277
  };
278
+ readonly wecom: {
279
+ readonly id: "wecom";
280
+ readonly isEnabled: (config: Config) => boolean;
281
+ readonly createChannel: (context: BuiltinChannelCreateContext) => WeComChannel;
282
+ };
259
283
  readonly email: {
260
284
  readonly id: "email";
261
285
  readonly isEnabled: (config: Config) => boolean;
@@ -277,4 +301,4 @@ declare const BUILTIN_CHANNEL_PLUGIN_IDS: BuiltinChannelId[];
277
301
  declare function listBuiltinChannelRuntimes(): BuiltinChannelRuntime[];
278
302
  declare function resolveBuiltinChannelRuntime(channelId: string): BuiltinChannelRuntime;
279
303
 
280
- export { BUILTIN_CHANNEL_PLUGIN_IDS, type BuiltinChannelId, type BuiltinChannelRuntime, DingTalkChannel, DiscordChannel, EmailChannel, FeishuChannel, MochatChannel, QQChannel, SlackChannel, TelegramChannel, WhatsAppChannel, listBuiltinChannelRuntimes, resolveBuiltinChannelRuntime };
304
+ 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
@@ -363,9 +363,8 @@ var DiscordChannel = class extends BaseChannel {
363
363
  ...attachmentIssues.length ? { attachment_issues: attachmentIssues } : {}
364
364
  }
365
365
  });
366
- } catch (err) {
366
+ } finally {
367
367
  this.stopTyping(channelId);
368
- throw err;
369
368
  }
370
369
  }
371
370
  resolveProxyAgent() {
@@ -2549,9 +2548,8 @@ Just send me a text message to chat!`;
2549
2548
  is_bot: sender.isBot,
2550
2549
  is_group: message.chat.type !== "private"
2551
2550
  });
2552
- } catch (err) {
2551
+ } finally {
2553
2552
  this.stopTyping(chatId);
2554
- throw err;
2555
2553
  }
2556
2554
  }
2557
2555
  async dispatchToBus(senderId, chatId, content, attachments, metadata) {
@@ -2670,6 +2668,273 @@ function markdownToTelegramHtml(text) {
2670
2668
  return text;
2671
2669
  }
2672
2670
 
2671
+ // src/channels/wecom.ts
2672
+ import { createHash } from "crypto";
2673
+ import { createServer } from "http";
2674
+ import { fetch as fetch5 } from "undici";
2675
+ var MSG_TYPE_FALLBACK = {
2676
+ image: "[image]",
2677
+ voice: "[voice]",
2678
+ video: "[video]",
2679
+ file: "[file]",
2680
+ location: "[location]",
2681
+ event: "[event]"
2682
+ };
2683
+ var TOKEN_EXPIRY_BUFFER_MS = 6e4;
2684
+ var WeComChannel = class extends BaseChannel {
2685
+ name = "wecom";
2686
+ server = null;
2687
+ accessToken = null;
2688
+ tokenExpiry = 0;
2689
+ processedIds = [];
2690
+ processedSet = /* @__PURE__ */ new Set();
2691
+ constructor(config, bus) {
2692
+ super(config, bus);
2693
+ }
2694
+ async start() {
2695
+ if (!this.config.corpId || !this.config.agentId || !this.config.secret || !this.config.token) {
2696
+ throw new Error("WeCom corpId/agentId/secret/token not configured");
2697
+ }
2698
+ this.running = true;
2699
+ await new Promise((resolve, reject) => {
2700
+ this.server = createServer((req, res) => {
2701
+ void this.handleCallbackRequest(req, res);
2702
+ });
2703
+ this.server.once("error", reject);
2704
+ this.server.listen(this.config.callbackPort, () => {
2705
+ this.server?.off("error", reject);
2706
+ resolve();
2707
+ });
2708
+ });
2709
+ }
2710
+ async stop() {
2711
+ this.running = false;
2712
+ if (!this.server) {
2713
+ return;
2714
+ }
2715
+ await new Promise((resolve) => {
2716
+ this.server?.close(() => resolve());
2717
+ });
2718
+ this.server = null;
2719
+ }
2720
+ async send(msg) {
2721
+ const receiver = msg.chatId?.trim();
2722
+ if (!receiver) {
2723
+ return;
2724
+ }
2725
+ const content = normalizeOutboundContent(msg);
2726
+ if (!content) {
2727
+ return;
2728
+ }
2729
+ const accessToken = await this.getAccessToken();
2730
+ const sendUrl = `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${encodeURIComponent(accessToken)}`;
2731
+ const agentIdNumber = Number(this.config.agentId);
2732
+ const payload = {
2733
+ touser: receiver,
2734
+ msgtype: "text",
2735
+ agentid: Number.isFinite(agentIdNumber) ? agentIdNumber : this.config.agentId,
2736
+ text: {
2737
+ content
2738
+ },
2739
+ safe: 0
2740
+ };
2741
+ const response = await fetch5(sendUrl, {
2742
+ method: "POST",
2743
+ headers: {
2744
+ "content-type": "application/json"
2745
+ },
2746
+ body: JSON.stringify(payload)
2747
+ });
2748
+ if (!response.ok) {
2749
+ throw new Error(`WeCom send failed: HTTP ${response.status}`);
2750
+ }
2751
+ const body = await response.json();
2752
+ const errcode = Number(body.errcode ?? -1);
2753
+ if (!Number.isFinite(errcode) || errcode !== 0) {
2754
+ const errmsg = typeof body.errmsg === "string" ? body.errmsg : "unknown error";
2755
+ throw new Error(`WeCom send failed: ${errcode} ${errmsg}`);
2756
+ }
2757
+ }
2758
+ async handleCallbackRequest(req, res) {
2759
+ const callbackPath = normalizeCallbackPath(this.config.callbackPath);
2760
+ const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`);
2761
+ if (requestUrl.pathname !== callbackPath) {
2762
+ res.statusCode = 404;
2763
+ res.end("not found");
2764
+ return;
2765
+ }
2766
+ const method = (req.method ?? "GET").toUpperCase();
2767
+ if (method === "GET") {
2768
+ this.handleVerificationRequest(requestUrl, res);
2769
+ return;
2770
+ }
2771
+ if (method === "POST") {
2772
+ await this.handleInboundMessageRequest(req, requestUrl, res);
2773
+ return;
2774
+ }
2775
+ res.statusCode = 405;
2776
+ res.end("method not allowed");
2777
+ }
2778
+ handleVerificationRequest(requestUrl, res) {
2779
+ const timestamp = requestUrl.searchParams.get("timestamp") ?? "";
2780
+ const nonce = requestUrl.searchParams.get("nonce") ?? "";
2781
+ const echostr = requestUrl.searchParams.get("echostr") ?? "";
2782
+ const signature = requestUrl.searchParams.get("msg_signature") ?? requestUrl.searchParams.get("signature") ?? "";
2783
+ if (!timestamp || !nonce || !echostr || !signature) {
2784
+ res.statusCode = 400;
2785
+ res.end("invalid verification payload");
2786
+ return;
2787
+ }
2788
+ if (!this.verifySignature(timestamp, nonce, signature)) {
2789
+ res.statusCode = 401;
2790
+ res.end("signature mismatch");
2791
+ return;
2792
+ }
2793
+ res.statusCode = 200;
2794
+ res.setHeader("content-type", "text/plain; charset=utf-8");
2795
+ res.end(echostr);
2796
+ }
2797
+ async handleInboundMessageRequest(req, requestUrl, res) {
2798
+ const timestamp = requestUrl.searchParams.get("timestamp") ?? "";
2799
+ const nonce = requestUrl.searchParams.get("nonce") ?? "";
2800
+ const signature = requestUrl.searchParams.get("msg_signature") ?? requestUrl.searchParams.get("signature") ?? "";
2801
+ if (!timestamp || !nonce || !signature || !this.verifySignature(timestamp, nonce, signature)) {
2802
+ res.statusCode = 401;
2803
+ res.end("signature mismatch");
2804
+ return;
2805
+ }
2806
+ const rawBody = await readBody(req);
2807
+ if (!rawBody.trim()) {
2808
+ respondSuccess(res);
2809
+ return;
2810
+ }
2811
+ if (extractXmlField(rawBody, "Encrypt")) {
2812
+ respondSuccess(res);
2813
+ return;
2814
+ }
2815
+ const senderId = extractXmlField(rawBody, "FromUserName");
2816
+ if (!senderId || !this.isAllowed(senderId)) {
2817
+ respondSuccess(res);
2818
+ return;
2819
+ }
2820
+ const msgType = extractXmlField(rawBody, "MsgType") || "text";
2821
+ const msgId = extractXmlField(rawBody, "MsgId") || buildSyntheticMessageId(rawBody, senderId, msgType);
2822
+ if (this.isDuplicate(msgId)) {
2823
+ respondSuccess(res);
2824
+ return;
2825
+ }
2826
+ const content = extractXmlField(rawBody, "Content")?.trim() || MSG_TYPE_FALLBACK[msgType.toLowerCase()] || "[unsupported message]";
2827
+ await this.handleMessage({
2828
+ senderId,
2829
+ chatId: senderId,
2830
+ content,
2831
+ attachments: [],
2832
+ metadata: {
2833
+ message_id: msgId,
2834
+ wecom: {
2835
+ msgType,
2836
+ toUserName: extractXmlField(rawBody, "ToUserName"),
2837
+ agentId: extractXmlField(rawBody, "AgentID"),
2838
+ createTime: extractXmlField(rawBody, "CreateTime")
2839
+ }
2840
+ }
2841
+ });
2842
+ respondSuccess(res);
2843
+ }
2844
+ verifySignature(timestamp, nonce, signature) {
2845
+ const expected = createHash("sha1").update([this.config.token, timestamp, nonce].sort().join("")).digest("hex");
2846
+ return expected === signature;
2847
+ }
2848
+ async getAccessToken() {
2849
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
2850
+ return this.accessToken;
2851
+ }
2852
+ const tokenUrl = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(this.config.corpId)}&corpsecret=${encodeURIComponent(this.config.secret)}`;
2853
+ const response = await fetch5(tokenUrl, { method: "GET" });
2854
+ if (!response.ok) {
2855
+ throw new Error(`WeCom gettoken failed: HTTP ${response.status}`);
2856
+ }
2857
+ const body = await response.json();
2858
+ const errcode = Number(body.errcode ?? -1);
2859
+ if (!Number.isFinite(errcode) || errcode !== 0) {
2860
+ const errmsg = typeof body.errmsg === "string" ? body.errmsg : "unknown error";
2861
+ throw new Error(`WeCom gettoken failed: ${errcode} ${errmsg}`);
2862
+ }
2863
+ const accessToken = typeof body.access_token === "string" ? body.access_token : "";
2864
+ const expiresIn = Number(body.expires_in ?? 7200);
2865
+ if (!accessToken) {
2866
+ throw new Error("WeCom gettoken failed: missing access_token");
2867
+ }
2868
+ this.accessToken = accessToken;
2869
+ this.tokenExpiry = Date.now() + Math.max(0, expiresIn * 1e3 - TOKEN_EXPIRY_BUFFER_MS);
2870
+ return accessToken;
2871
+ }
2872
+ isDuplicate(messageId) {
2873
+ if (this.processedSet.has(messageId)) {
2874
+ return true;
2875
+ }
2876
+ this.processedSet.add(messageId);
2877
+ this.processedIds.push(messageId);
2878
+ if (this.processedIds.length > 1e3) {
2879
+ const removed = this.processedIds.splice(0, 500);
2880
+ for (const id of removed) {
2881
+ this.processedSet.delete(id);
2882
+ }
2883
+ }
2884
+ return false;
2885
+ }
2886
+ };
2887
+ function normalizeCallbackPath(path) {
2888
+ if (!path) {
2889
+ return "/wecom/callback";
2890
+ }
2891
+ return path.startsWith("/") ? path : `/${path}`;
2892
+ }
2893
+ function respondSuccess(res) {
2894
+ res.statusCode = 200;
2895
+ res.setHeader("content-type", "text/plain; charset=utf-8");
2896
+ res.end("success");
2897
+ }
2898
+ function buildSyntheticMessageId(rawBody, senderId, msgType) {
2899
+ return createHash("sha1").update(`${senderId}:${msgType}:${rawBody}`).digest("hex");
2900
+ }
2901
+ function extractXmlField(xml, field) {
2902
+ const cdataPattern = new RegExp(`<${field}><!\\[CDATA\\[(.*?)\\]\\]><\\/${field}>`, "s");
2903
+ const cdataMatch = xml.match(cdataPattern);
2904
+ if (cdataMatch?.[1]) {
2905
+ return cdataMatch[1].trim();
2906
+ }
2907
+ const textPattern = new RegExp(`<${field}>(.*?)<\\/${field}>`, "s");
2908
+ const textMatch = xml.match(textPattern);
2909
+ return textMatch?.[1]?.trim() ?? "";
2910
+ }
2911
+ async function readBody(req) {
2912
+ const chunks = [];
2913
+ return await new Promise((resolve, reject) => {
2914
+ req.on("data", (chunk) => {
2915
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2916
+ });
2917
+ req.on("error", reject);
2918
+ req.on("end", () => {
2919
+ resolve(Buffer.concat(chunks).toString("utf8"));
2920
+ });
2921
+ });
2922
+ }
2923
+ function normalizeOutboundContent(msg) {
2924
+ const segments = [];
2925
+ if (typeof msg.content === "string" && msg.content.trim()) {
2926
+ segments.push(msg.content.trim());
2927
+ }
2928
+ if (Array.isArray(msg.media)) {
2929
+ for (const item of msg.media) {
2930
+ if (typeof item === "string" && item.trim()) {
2931
+ segments.push(item.trim());
2932
+ }
2933
+ }
2934
+ }
2935
+ return segments.join("\n").trim();
2936
+ }
2937
+
2673
2938
  // src/channels/whatsapp.ts
2674
2939
  import WebSocket from "ws";
2675
2940
  var WhatsAppChannel = class extends BaseChannel {
@@ -2820,6 +3085,11 @@ var BUILTIN_CHANNEL_RUNTIMES = {
2820
3085
  isEnabled: (config) => config.channels.dingtalk.enabled,
2821
3086
  createChannel: (context) => new DingTalkChannel(context.config.channels.dingtalk, context.bus)
2822
3087
  },
3088
+ wecom: {
3089
+ id: "wecom",
3090
+ isEnabled: (config) => config.channels.wecom.enabled,
3091
+ createChannel: (context) => new WeComChannel(context.config.channels.wecom, context.bus)
3092
+ },
2823
3093
  email: {
2824
3094
  id: "email",
2825
3095
  isEnabled: (config) => config.channels.email.enabled,
@@ -2859,6 +3129,7 @@ export {
2859
3129
  QQChannel,
2860
3130
  SlackChannel,
2861
3131
  TelegramChannel,
3132
+ WeComChannel,
2862
3133
  WhatsAppChannel,
2863
3134
  listBuiltinChannelRuntimes,
2864
3135
  resolveBuiltinChannelRuntime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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.21",
19
19
  "@slack/socket-mode": "^1.3.3",
20
20
  "@slack/web-api": "^7.6.0",
21
21
  "dingtalk-stream": "^2.1.4",