dsh-connect 0.7.2 → 0.8.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.
Files changed (114) hide show
  1. package/README.md +68 -32
  2. package/README.zh.md +67 -31
  3. package/client/client.js +387 -0
  4. package/client/client.js.map +7 -0
  5. package/client/settings-client.mjs +177 -0
  6. package/examples/minimal.config.json +16 -0
  7. package/lib/channels/dingtalk/adapter.d.ts +59 -0
  8. package/lib/channels/dingtalk/adapter.d.ts.map +1 -0
  9. package/lib/channels/dingtalk/adapter.js +123 -0
  10. package/lib/channels/dingtalk/adapter.js.map +1 -0
  11. package/lib/channels/dingtalk/i18n.d.ts +14 -0
  12. package/lib/channels/dingtalk/i18n.d.ts.map +1 -0
  13. package/lib/channels/dingtalk/i18n.js +16 -0
  14. package/lib/channels/dingtalk/i18n.js.map +1 -0
  15. package/lib/channels/dingtalk/index.d.ts +127 -0
  16. package/lib/channels/dingtalk/index.d.ts.map +1 -0
  17. package/lib/channels/dingtalk/index.js +144 -0
  18. package/lib/channels/dingtalk/index.js.map +1 -0
  19. package/lib/channels/dingtalk/message.d.ts +49 -0
  20. package/lib/channels/dingtalk/message.d.ts.map +1 -0
  21. package/lib/channels/dingtalk/message.js +43 -0
  22. package/lib/channels/dingtalk/message.js.map +1 -0
  23. package/lib/channels/dingtalk/stomp.d.ts +36 -0
  24. package/lib/channels/dingtalk/stomp.d.ts.map +1 -0
  25. package/lib/channels/dingtalk/stomp.js +107 -0
  26. package/lib/channels/dingtalk/stomp.js.map +1 -0
  27. package/lib/channels/dingtalk/stream.d.ts +53 -0
  28. package/lib/channels/dingtalk/stream.d.ts.map +1 -0
  29. package/lib/channels/dingtalk/stream.js +199 -0
  30. package/lib/channels/dingtalk/stream.js.map +1 -0
  31. package/lib/channels/dingtalk/webhook.d.ts +84 -0
  32. package/lib/channels/dingtalk/webhook.d.ts.map +1 -0
  33. package/lib/channels/dingtalk/webhook.js +143 -0
  34. package/lib/channels/dingtalk/webhook.js.map +1 -0
  35. package/lib/channels/feishu/adapter.d.ts +139 -0
  36. package/lib/channels/feishu/adapter.d.ts.map +1 -0
  37. package/lib/channels/feishu/adapter.js +677 -0
  38. package/lib/channels/feishu/adapter.js.map +1 -0
  39. package/lib/channels/feishu/i18n.d.ts +28 -0
  40. package/lib/channels/feishu/i18n.d.ts.map +1 -0
  41. package/lib/channels/feishu/i18n.js +40 -0
  42. package/lib/channels/feishu/i18n.js.map +1 -0
  43. package/lib/channels/feishu/index.d.ts +61 -0
  44. package/lib/channels/feishu/index.d.ts.map +1 -0
  45. package/lib/channels/feishu/index.js +77 -0
  46. package/lib/channels/feishu/index.js.map +1 -0
  47. package/lib/channels/feishu/onboard.d.ts +16 -0
  48. package/lib/channels/feishu/onboard.d.ts.map +1 -0
  49. package/lib/channels/feishu/onboard.js +86 -0
  50. package/lib/channels/feishu/onboard.js.map +1 -0
  51. package/lib/channels/telegram/adapter.d.ts +70 -0
  52. package/lib/channels/telegram/adapter.d.ts.map +1 -0
  53. package/lib/channels/telegram/adapter.js +454 -0
  54. package/lib/channels/telegram/adapter.js.map +1 -0
  55. package/lib/channels/telegram/client.d.ts +115 -0
  56. package/lib/channels/telegram/client.d.ts.map +1 -0
  57. package/lib/channels/telegram/client.js +198 -0
  58. package/lib/channels/telegram/client.js.map +1 -0
  59. package/lib/channels/telegram/i18n.d.ts +14 -0
  60. package/lib/channels/telegram/i18n.d.ts.map +1 -0
  61. package/lib/channels/telegram/i18n.js +18 -0
  62. package/lib/channels/telegram/i18n.js.map +1 -0
  63. package/lib/channels/telegram/index.d.ts +46 -0
  64. package/lib/channels/telegram/index.d.ts.map +1 -0
  65. package/lib/channels/telegram/index.js +54 -0
  66. package/lib/channels/telegram/index.js.map +1 -0
  67. package/lib/channels/web/adapter.d.ts +103 -0
  68. package/lib/channels/web/adapter.d.ts.map +1 -0
  69. package/lib/channels/web/adapter.js +161 -0
  70. package/lib/channels/web/adapter.js.map +1 -0
  71. package/lib/channels/web/index.d.ts +47 -0
  72. package/lib/channels/web/index.d.ts.map +1 -0
  73. package/lib/channels/web/index.js +59 -0
  74. package/lib/channels/web/index.js.map +1 -0
  75. package/lib/index.d.ts +68 -11
  76. package/lib/index.d.ts.map +1 -1
  77. package/lib/index.js +141 -8
  78. package/lib/index.js.map +1 -1
  79. package/lib/runner.d.ts +1 -2
  80. package/lib/runner.d.ts.map +1 -1
  81. package/lib/runner.js.map +1 -1
  82. package/lib/service.d.ts +1 -1
  83. package/lib/service.d.ts.map +1 -1
  84. package/lib/service.js +1 -1
  85. package/lib/service.js.map +1 -1
  86. package/lib/settings/channels.d.ts +51 -0
  87. package/lib/settings/channels.d.ts.map +1 -0
  88. package/lib/settings/channels.js +101 -0
  89. package/lib/settings/channels.js.map +1 -0
  90. package/lib/settings/credential-store.d.ts +39 -0
  91. package/lib/settings/credential-store.d.ts.map +1 -0
  92. package/lib/settings/credential-store.js +68 -0
  93. package/lib/settings/credential-store.js.map +1 -0
  94. package/lib/settings/index.d.ts +14 -0
  95. package/lib/settings/index.d.ts.map +1 -0
  96. package/lib/settings/index.js +14 -0
  97. package/lib/settings/index.js.map +1 -0
  98. package/lib/settings/rpc-client.d.ts +36 -0
  99. package/lib/settings/rpc-client.d.ts.map +1 -0
  100. package/lib/settings/rpc-client.js +45 -0
  101. package/lib/settings/rpc-client.js.map +1 -0
  102. package/lib/settings/settings-model.d.ts +107 -0
  103. package/lib/settings/settings-model.d.ts.map +1 -0
  104. package/lib/settings/settings-model.js +110 -0
  105. package/lib/settings/settings-model.js.map +1 -0
  106. package/lib/settings/settings-rpc.d.ts +62 -0
  107. package/lib/settings/settings-rpc.d.ts.map +1 -0
  108. package/lib/settings/settings-rpc.js +90 -0
  109. package/lib/settings/settings-rpc.js.map +1 -0
  110. package/lib/settings/settings-service.d.ts +27 -0
  111. package/lib/settings/settings-service.d.ts.map +1 -0
  112. package/lib/settings/settings-service.js +103 -0
  113. package/lib/settings/settings-service.js.map +1 -0
  114. package/package.json +48 -6
@@ -0,0 +1,199 @@
1
+ /**
2
+ * DingTalk stream-mode gateway client: STOMP over WebSocket, zero third-party
3
+ * dependencies (Node ≥ 22 global `WebSocket`; older Node needs a polyfill —
4
+ * the constructor is looked up lazily on `globalThis` so the module loads
5
+ * fine on Node 20, it only fails at connect time).
6
+ *
7
+ * This file is the live network boundary: it is intentionally thin and every
8
+ * protocol detail it depends on lives in the pure modules (stomp.ts,
9
+ * message.ts) that carry the unit tests. Actual connectivity requires real
10
+ * app credentials and an outbound connection to the DingTalk gateway, so it
11
+ * cannot be exercised in CI here.
12
+ * @module dsh-connect/channels/dingtalk/stream
13
+ */
14
+ import { decodeFrames, encodeFrame } from "./stomp.js";
15
+ import { buildConnectBody, INBOUND_DESTINATION, INBOUND_SUBSCRIPTION, REPLY_DESTINATION, } from "./message.js";
16
+ export const DEFAULT_STREAM_URL = "wss://api.dingtalk.com/connect";
17
+ /** STOMP heartbeat interval (client → gateway), ms. */
18
+ const HEARTBEAT_MS = 10_000;
19
+ const RECONNECT_BASE_MS = 1_000;
20
+ const RECONNECT_MAX_MS = 30_000;
21
+ function lazyWebSocketCtor() {
22
+ return globalThis.WebSocket;
23
+ }
24
+ /**
25
+ * STOMP-over-WebSocket gateway client. Usage:
26
+ * ```ts
27
+ * const client = new DingtalkStreamClient({ clientId, clientSecret });
28
+ * client.onInbound((raw) => console.log(raw.conversationId, raw.text?.content));
29
+ * await client.connect();
30
+ * await client.sendReply(msgId, buildTextReplyBody(msgId, "hi"));
31
+ * ```
32
+ */
33
+ export class DingtalkStreamClient {
34
+ clientId;
35
+ clientSecret;
36
+ url;
37
+ logger;
38
+ ws;
39
+ rest = "";
40
+ heartbeat;
41
+ reconnectTimer;
42
+ backoffMs = RECONNECT_BASE_MS;
43
+ closedByUs = false;
44
+ messageHandler;
45
+ constructor(options) {
46
+ this.clientId = options.clientId;
47
+ this.clientSecret = options.clientSecret;
48
+ this.url = options.url ?? DEFAULT_STREAM_URL;
49
+ this.logger = options.logger;
50
+ }
51
+ /** Register the inbound-message handler (called for every gateway push). */
52
+ onInbound(handler) {
53
+ this.messageHandler = handler;
54
+ }
55
+ /** Open the socket, perform the STOMP handshake and subscribe. Reconnect-safe. */
56
+ async connect() {
57
+ const Ctor = lazyWebSocketCtor();
58
+ if (Ctor === undefined) {
59
+ throw new Error("connect-dingtalk: WebSocket is not available on this runtime (Node ≥ 22 or a polyfill required)");
60
+ }
61
+ if (this.closedByUs)
62
+ return;
63
+ this.logger?.info?.(`connect-dingtalk: connecting to ${this.url}`);
64
+ const ws = new Ctor(this.url);
65
+ this.ws = ws;
66
+ await new Promise((resolve, reject) => {
67
+ const timer = setTimeout(() => {
68
+ cleanup();
69
+ reject(new Error("connect-dingtalk: WebSocket handshake timed out"));
70
+ }, 15_000);
71
+ const cleanup = () => {
72
+ clearTimeout(timer);
73
+ ws.onopen = null;
74
+ ws.onerror = null;
75
+ };
76
+ ws.onopen = () => {
77
+ cleanup();
78
+ this.onSocketOpen();
79
+ resolve();
80
+ };
81
+ ws.onerror = () => {
82
+ // onclose fires next; the close handler owns reconnect scheduling.
83
+ };
84
+ });
85
+ ws.onmessage = (ev) => this.onSocketMessage(String(ev.data ?? ""));
86
+ ws.onclose = (ev) => this.onSocketClose(ev.code, ev.reason);
87
+ }
88
+ /** Send a reply to an inbound message (SEND frame to the reply topic). */
89
+ async sendReply(msgId, body) {
90
+ const ws = this.ws;
91
+ if (ws === undefined || ws.readyState !== 1 /* OPEN */) {
92
+ throw new Error("connect-dingtalk: stream not connected");
93
+ }
94
+ ws.send(encodeFrame("SEND", { destination: REPLY_DESTINATION }, body));
95
+ }
96
+ /** Stop the client and cancel any scheduled reconnect. */
97
+ close() {
98
+ this.closedByUs = true;
99
+ if (this.reconnectTimer !== undefined) {
100
+ clearTimeout(this.reconnectTimer);
101
+ this.reconnectTimer = undefined;
102
+ }
103
+ this.stopHeartbeat();
104
+ this.ws?.close(1000, "bye");
105
+ this.ws = undefined;
106
+ }
107
+ onSocketOpen() {
108
+ this.logger?.info?.("connect-dingtalk: socket open, sending CONNECT");
109
+ this.sendFrame("CONNECT", {
110
+ "accept-version": "1.2",
111
+ host: "api.dingtalk.com",
112
+ "heart-beat": `${HEARTBEAT_MS},${HEARTBEAT_MS}`,
113
+ }, buildConnectBody(this.clientId, this.clientSecret));
114
+ }
115
+ onSocketMessage(data) {
116
+ const { frames, rest } = decodeFrames(this.rest + data);
117
+ this.rest = rest;
118
+ for (const frame of frames)
119
+ this.handleFrame(frame);
120
+ }
121
+ onSocketClose(code, reason) {
122
+ this.stopHeartbeat();
123
+ this.ws = undefined;
124
+ if (this.closedByUs)
125
+ return;
126
+ this.logger?.warn?.(`connect-dingtalk: socket closed (code=${String(code)} reason=${String(reason)}), reconnecting in ${this.backoffMs}ms`);
127
+ this.scheduleReconnect();
128
+ }
129
+ scheduleReconnect() {
130
+ if (this.closedByUs || this.reconnectTimer !== undefined)
131
+ return;
132
+ const delay = this.backoffMs;
133
+ this.backoffMs = Math.min(this.backoffMs * 2, RECONNECT_MAX_MS);
134
+ this.reconnectTimer = setTimeout(() => {
135
+ this.reconnectTimer = undefined;
136
+ void this.connect().catch((error) => {
137
+ this.logger?.error?.(`connect-dingtalk: reconnect attempt failed: ${String(error)}`);
138
+ this.scheduleReconnect();
139
+ });
140
+ }, delay);
141
+ }
142
+ handleFrame(frame) {
143
+ switch (frame.command) {
144
+ case "CONNECTED": {
145
+ this.backoffMs = RECONNECT_BASE_MS;
146
+ this.logger?.info?.("connect-dingtalk: CONNECTED, subscribing");
147
+ this.sendFrame("SUBSCRIBE", { id: INBOUND_SUBSCRIPTION, destination: INBOUND_DESTINATION });
148
+ this.startHeartbeat();
149
+ break;
150
+ }
151
+ case "MESSAGE": {
152
+ if (this.messageHandler === undefined)
153
+ break;
154
+ try {
155
+ const raw = JSON.parse(frame.body);
156
+ void Promise.resolve(this.messageHandler(raw)).catch((error) => {
157
+ this.logger?.warn?.(`connect-dingtalk: inbound handler failed: ${String(error)}`);
158
+ });
159
+ }
160
+ catch (error) {
161
+ this.logger?.warn?.(`connect-dingtalk: malformed gateway payload: ${String(error)}`);
162
+ }
163
+ break;
164
+ }
165
+ case "ERROR": {
166
+ this.logger?.error?.(`connect-dingtalk: gateway ERROR: ${frame.body || frame.headers.message || "no detail"}`);
167
+ break;
168
+ }
169
+ case "RECEIPT": {
170
+ this.logger?.info?.("connect-dingtalk: gateway receipt received");
171
+ break;
172
+ }
173
+ default:
174
+ this.logger?.warn?.(`connect-dingtalk: unhandled STOMP frame ${frame.command}`);
175
+ }
176
+ }
177
+ sendFrame(command, headers, body = "") {
178
+ const ws = this.ws;
179
+ if (ws === undefined || ws.readyState !== 1)
180
+ return;
181
+ ws.send(encodeFrame(command, headers, body));
182
+ }
183
+ startHeartbeat() {
184
+ this.stopHeartbeat();
185
+ this.heartbeat = setInterval(() => {
186
+ const ws = this.ws;
187
+ if (ws !== undefined && ws.readyState === 1)
188
+ ws.send("\n");
189
+ }, HEARTBEAT_MS);
190
+ this.heartbeat.unref?.();
191
+ }
192
+ stopHeartbeat() {
193
+ if (this.heartbeat !== undefined) {
194
+ clearInterval(this.heartbeat);
195
+ this.heartbeat = undefined;
196
+ }
197
+ }
198
+ }
199
+ //# sourceMappingURL=stream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream.js","sourceRoot":"","sources":["../../../src/channels/dingtalk/stream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /** DingTalk webhook configuration. */
2
+ export interface DingtalkWebhookConfig {
3
+ /** Full webhook URL, e.g. `https://oapi.dingtalk.com/robot/send?access_token=xxx`. */
4
+ webhookUrl: string;
5
+ /** Optional signing secret (`SEC...`) — when set, requests are signed. */
6
+ secret?: string;
7
+ /** HTTP timeout in ms (default 10s). */
8
+ timeoutMs?: number;
9
+ /** Backoff delays between retries for transient network errors (default 1s/2s/4s). */
10
+ retryDelaysMs?: readonly number[];
11
+ /** Extra delay for the 130101 frequency-limit retry (default 10s). */
12
+ rateLimitDelayMs?: number;
13
+ }
14
+ /** Body shape accepted by the DingTalk robot API. */
15
+ export type DingtalkBody = {
16
+ msgtype: "text";
17
+ text: {
18
+ content: string;
19
+ };
20
+ at?: {
21
+ atMobiles?: string[];
22
+ atUserIds?: string[];
23
+ isAtAll?: boolean;
24
+ };
25
+ } | {
26
+ msgtype: "markdown";
27
+ markdown: {
28
+ title: string;
29
+ text: string;
30
+ };
31
+ at?: {
32
+ atMobiles?: string[];
33
+ atUserIds?: string[];
34
+ isAtAll?: boolean;
35
+ };
36
+ };
37
+ /** Response of the DingTalk robot API. */
38
+ export interface DingtalkResponse {
39
+ errcode: number;
40
+ errmsg: string;
41
+ }
42
+ /** How to address people in a DingTalk group. */
43
+ export interface DingtalkAt {
44
+ /** Phone numbers — required by DingTalk for `atMobiles` (robots can't resolve names). */
45
+ mobiles?: readonly string[];
46
+ /** User ids (unionid/`userId` when known). */
47
+ userIds?: readonly string[];
48
+ /** @everyone. */
49
+ all?: boolean;
50
+ }
51
+ /** DingTalk caps markdown body text at 20000 chars. */
52
+ export declare const MARKDOWN_LIMIT = 20000;
53
+ /**
54
+ * Compute the DingTalk webhook signature. The SDK expects
55
+ * `timestamp + "\n" + secret` signed with HMAC-SHA256, base64-encoded, then
56
+ * URL-encoded as the `sign` query param.
57
+ */
58
+ export declare function signDingtalk(secret: string, timestampMs: number): string;
59
+ /**
60
+ * Verify an incoming signature against a secret (used by webhook receivers, if
61
+ * ever needed). Both sides are compared after URL-decoding, so a sender that
62
+ * encoded `+` as `%2B` (or vice versa) still verifies, and the timestamp must
63
+ * be fresh (default: ±5 minutes) to be replay-resistant.
64
+ */
65
+ export declare function verifyDingtalkSignature(secret: string, timestampMs: string, sign: string, nowMs?: number, maxAgeMs?: number): boolean;
66
+ /**
67
+ * Minimal DingTalk group-robot webhook client built on the global `fetch`
68
+ * (Node ≥ 18 / ≥ 20 — no third-party HTTP dependency). Transient failures
69
+ * (network errors, the 20/min frequency limit) are retried with backoff.
70
+ */
71
+ export declare class DingtalkWebhook {
72
+ readonly config: DingtalkWebhookConfig;
73
+ constructor(config: DingtalkWebhookConfig);
74
+ /**
75
+ * Send a markdown message. `title` is shown as the message's card title in
76
+ * DingTalk; `text` is the markdown body (truncated to 20000 chars, the API
77
+ * limit). Optionally @-mention people.
78
+ */
79
+ sendMarkdown(title: string, text: string, at?: DingtalkAt): Promise<DingtalkResponse>;
80
+ /** Send a plain-text message (content shown verbatim, `\n` newlines allowed). */
81
+ sendText(content: string, at?: DingtalkAt): Promise<DingtalkResponse>;
82
+ private send;
83
+ }
84
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/dingtalk/webhook.ts"],"names":[],"mappings":"AAYA,sCAAsC;AACtC,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC;IACnB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qDAAqD;AACrD,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1B,EAAE,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxE,GAAG;IACF,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,EAAE,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CACxE,CAAC;AAEF,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iDAAiD;AACjD,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,8CAA8C;IAC9C,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,iBAAiB;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,uDAAuD;AACvD,eAAO,MAAM,cAAc,QAAS,CAAC;AAIrC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAIxE;AAUD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAmB,EAC1B,QAAQ,GAAE,MAAmB,GAC5B,OAAO,CAOT;AA2BD;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;gBAE3B,MAAM,EAAE,qBAAqB;IAOzC;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAK3F,iFAAiF;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAI7D,IAAI;CAiCnB"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * DingTalk group-robot webhook transport. A webhook URL is created from a
3
+ * group's "custom robot" (群自定义机器人): the group owner adds a robot, gets
4
+ * an `access_token`, and optionally enables a signing secret. This transport
5
+ * only *pushes* — DingTalk custom webhooks cannot receive messages, so this
6
+ * package is a one-way notice channel (progress / results / alerts), not a
7
+ * bidirectional conversation adapter like the Feishu one.
8
+ *
9
+ * @module dsh-connect/channels/dingtalk/webhook
10
+ */
11
+ import { createHmac, timingSafeEqual } from "node:crypto";
12
+ /** DingTalk caps markdown body text at 20000 chars. */
13
+ export const MARKDOWN_LIMIT = 20_000;
14
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
+ /**
16
+ * Compute the DingTalk webhook signature. The SDK expects
17
+ * `timestamp + "\n" + secret` signed with HMAC-SHA256, base64-encoded, then
18
+ * URL-encoded as the `sign` query param.
19
+ */
20
+ export function signDingtalk(secret, timestampMs) {
21
+ const stringToSign = `${timestampMs}\n${secret}`;
22
+ const mac = createHmac("sha256", secret).update(stringToSign, "utf8").digest("base64");
23
+ return encodeURIComponent(mac);
24
+ }
25
+ function safeDecode(value) {
26
+ try {
27
+ return decodeURIComponent(value);
28
+ }
29
+ catch {
30
+ return value; // not URL-encoded — compare as-is
31
+ }
32
+ }
33
+ /**
34
+ * Verify an incoming signature against a secret (used by webhook receivers, if
35
+ * ever needed). Both sides are compared after URL-decoding, so a sender that
36
+ * encoded `+` as `%2B` (or vice versa) still verifies, and the timestamp must
37
+ * be fresh (default: ±5 minutes) to be replay-resistant.
38
+ */
39
+ export function verifyDingtalkSignature(secret, timestampMs, sign, nowMs = Date.now(), maxAgeMs = 5 * 60_000) {
40
+ const ts = Number(timestampMs);
41
+ if (!Number.isFinite(ts) || Math.abs(nowMs - ts) > maxAgeMs)
42
+ return false;
43
+ const expected = signDingtalk(secret, ts);
44
+ const a = Buffer.from(safeDecode(sign));
45
+ const b = Buffer.from(safeDecode(expected));
46
+ return a.length === b.length && timingSafeEqual(a, b);
47
+ }
48
+ /** Replace non-url-safe chars in a DingTalk webhook URL's query (idempotent-safe). */
49
+ function withSignature(url, secret, timestampMs) {
50
+ const sign = signDingtalk(secret, timestampMs);
51
+ const sep = url.includes("?") ? "&" : "?";
52
+ return `${url}${sep}timestamp=${timestampMs}&sign=${sign}`;
53
+ }
54
+ /** Errors that are safe to retry: transient network issues and the frequency limit. */
55
+ function isRetryable(error) {
56
+ const errcode = error.errcode;
57
+ if (errcode === 130101)
58
+ return true; // 发送频率限制 (20/min per robot)
59
+ if (error instanceof TypeError)
60
+ return true; // fetch network failure
61
+ return false;
62
+ }
63
+ function delayFor(config, error, attempt) {
64
+ const errcode = error.errcode;
65
+ if (errcode === 130101)
66
+ return config.rateLimitDelayMs ?? 10_000; // rate limit: wait for the window to pass
67
+ const backoff = config.retryDelaysMs;
68
+ if (backoff !== undefined && backoff.length > 0)
69
+ return backoff[Math.min(attempt, backoff.length - 1)] ?? 1_000;
70
+ return 1_000 * 2 ** attempt; // 1s, 2s, 4s…
71
+ }
72
+ const MAX_ATTEMPTS = 3;
73
+ /**
74
+ * Minimal DingTalk group-robot webhook client built on the global `fetch`
75
+ * (Node ≥ 18 / ≥ 20 — no third-party HTTP dependency). Transient failures
76
+ * (network errors, the 20/min frequency limit) are retried with backoff.
77
+ */
78
+ export class DingtalkWebhook {
79
+ config;
80
+ constructor(config) {
81
+ if (!config.webhookUrl || !config.webhookUrl.startsWith("https://")) {
82
+ throw new Error("connect-dingtalk: webhookUrl must be a https:// DingTalk robot URL");
83
+ }
84
+ this.config = config;
85
+ }
86
+ /**
87
+ * Send a markdown message. `title` is shown as the message's card title in
88
+ * DingTalk; `text` is the markdown body (truncated to 20000 chars, the API
89
+ * limit). Optionally @-mention people.
90
+ */
91
+ async sendMarkdown(title, text, at) {
92
+ const body = text.length > MARKDOWN_LIMIT ? `${text.slice(0, MARKDOWN_LIMIT)}\n\n…(已截断)` : text;
93
+ return this.send({ msgtype: "markdown", markdown: { title, text: body }, at: atBody(at) });
94
+ }
95
+ /** Send a plain-text message (content shown verbatim, `\n` newlines allowed). */
96
+ async sendText(content, at) {
97
+ return this.send({ msgtype: "text", text: { content }, at: atBody(at) });
98
+ }
99
+ async send(body, attempt = 0) {
100
+ const { webhookUrl, secret, timeoutMs = 10_000 } = this.config;
101
+ const url = secret ? withSignature(webhookUrl, secret, Date.now()) : webhookUrl;
102
+ const controller = new AbortController();
103
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
104
+ try {
105
+ const response = await fetch(url, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify(body),
109
+ signal: controller.signal,
110
+ });
111
+ if (!response.ok) {
112
+ throw new Error(`connect-dingtalk: HTTP ${response.status} ${response.statusText}`);
113
+ }
114
+ const parsed = (await response.json());
115
+ if (parsed.errcode !== 0) {
116
+ const error = new Error(`connect-dingtalk: DingTalk error ${parsed.errcode}: ${parsed.errmsg}`);
117
+ error.errcode = parsed.errcode;
118
+ throw error;
119
+ }
120
+ return parsed;
121
+ }
122
+ catch (error) {
123
+ if (isRetryable(error) && attempt < MAX_ATTEMPTS) {
124
+ await sleep(delayFor(this.config, error, attempt));
125
+ return this.send(body, attempt + 1);
126
+ }
127
+ throw error;
128
+ }
129
+ finally {
130
+ clearTimeout(timer);
131
+ }
132
+ }
133
+ }
134
+ function atBody(at) {
135
+ if (at === undefined)
136
+ return undefined;
137
+ return {
138
+ ...(at.mobiles !== undefined && at.mobiles.length > 0 ? { atMobiles: [...at.mobiles] } : {}),
139
+ ...(at.userIds !== undefined && at.userIds.length > 0 ? { atUserIds: [...at.userIds] } : {}),
140
+ ...(at.all === true ? { isAtAll: true } : {}),
141
+ };
142
+ }
143
+ //# sourceMappingURL=webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../../src/channels/dingtalk/webhook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ import type { ChannelAdapter, ChoiceOption, ChoicePrompt, ChoiceResult, InboundMessage, OutboundTarget, SummaryCard } from "../../types.js";
2
+ import type { Language } from "../../i18n.js";
3
+ /** Pad every label with trailing spaces up to the same display width. */
4
+ export declare function padLabels(options: readonly ChoiceOption[]): ChoiceOption[];
5
+ /**
6
+ * Render options as an equal-width button grid: 2 buttons per row via
7
+ * `column_set` (two weighted columns), padding the last row with empty
8
+ * columns so every row is a uniform 2-cell layout. Labels are padded to the
9
+ * same display width so the buttons themselves render uniformly. Destructive
10
+ * actions (labels starting with ❌) render as red danger buttons.
11
+ */
12
+ export declare function buildButtonGrid(options: readonly ChoiceOption[], columnsPerRow?: number): unknown[];
13
+ /**
14
+ * Render options as a single-select dropdown (`select_static`) inside an
15
+ * `action` container. Selecting an entry fires the card action with
16
+ * `action.tag === "select_static"` and the picked option id in `action.option`.
17
+ */
18
+ export declare function buildSelectMenu(options: readonly ChoiceOption[], placeholder?: string, initial?: string): unknown[];
19
+ /**
20
+ * Assemble the card elements for a choice prompt. When `sections` is given,
21
+ * the options are split into titled groups — each with a bold section caption
22
+ * and a divider before it — instead of one flat grid. Options not listed in
23
+ * any section (typically the trailing exit/back buttons) are rendered after
24
+ * the sections, separated by a divider.
25
+ *
26
+ * @param prompt - The choice prompt to render
27
+ * @param defaultColumns - Default number of columns per row (default: 2)
28
+ */
29
+ export declare function buildChoiceElements(prompt: ChoicePrompt, defaultColumns?: number): unknown[];
30
+ /**
31
+ * Encode a chatKey with an optional thread id. With `threadIsolation` on, a
32
+ * group message inside a thread gets `chatId:thread=<rootId>` as its chatKey,
33
+ * so each thread binds its own DSH session while outbound sends still target
34
+ * the base chat id (replies land in the thread via replyRef).
35
+ */
36
+ export declare function encodeChatKey(chatId: string, threadId?: string): string;
37
+ /** Decode a possibly thread-scoped chatKey back to its base chat id. */
38
+ export declare function decodeChatKey(chatKey: string): {
39
+ chatId: string;
40
+ threadId?: string;
41
+ };
42
+ /** Classify a local file name for the Feishu upload API (image vs stream file). */
43
+ export declare function classifyFeishuFile(filename: string): "image" | "file";
44
+ /** Strip path separators / control chars from a downloaded file's name. */
45
+ export declare function sanitizeFileName(name: string): string;
46
+ /**
47
+ * Extract a human-readable error detail. Feishu business errors (missing
48
+ * permission, invalid resource, …) arrive as HTTP 200/400 with the real
49
+ * `{code, msg}` in the response body; prefer that over the axios fallback
50
+ * message like "Request failed with status code 400" that hides the cause.
51
+ */
52
+ export declare function extractErrorDetail(error: unknown): string;
53
+ export interface FeishuConfig {
54
+ appId?: string;
55
+ appSecret?: string;
56
+ transport?: "websocket" | "webhook";
57
+ verificationToken?: string;
58
+ encryptKey?: string;
59
+ /** HTTP port for `transport: "webhook"` (default 9000). */
60
+ webhookPort?: number;
61
+ /** HTTP path the Feishu event callback posts to (default "/"). */
62
+ webhookPath?: string;
63
+ /** Group messages must @-mention the bot (default true). */
64
+ requireMention?: boolean;
65
+ /** Single-chat policy (SDK values): open / allowlist / pair / disabled. */
66
+ dmMode?: "open" | "allowlist" | "pair" | "disabled";
67
+ /** Bind one DSH session per group thread instead of one per chat (default false). */
68
+ threadIsolation?: boolean;
69
+ /** User-facing message language: `zh` (default) or `en`. */
70
+ language?: Language;
71
+ }
72
+ export declare class FeishuAdapter implements ChannelAdapter {
73
+ private readonly logger?;
74
+ private readonly isChatAllowed?;
75
+ readonly id = "feishu";
76
+ private readonly channel;
77
+ private readonly pendingChoices;
78
+ /** Cards whose stale-button tap was already noticed (dedupe, self-clearing). */
79
+ private readonly staleNoticed;
80
+ private readonly staleTimers;
81
+ private readonly t;
82
+ private readonly transport;
83
+ private readonly webhookPort;
84
+ private readonly webhookPath;
85
+ private readonly threadIsolation;
86
+ private server?;
87
+ private handler?;
88
+ constructor(config: FeishuConfig, logger?: {
89
+ info?: (...args: unknown[]) => void;
90
+ warn?: (...args: unknown[]) => void;
91
+ error?: (...args: unknown[]) => void;
92
+ } | undefined, isChatAllowed?: ((channel: string, chatKey: string, senderKey: string) => boolean) | undefined);
93
+ onInbound(handler: (msg: InboundMessage) => void | Promise<void>): void;
94
+ /** Resource types downloadable via `im.v1.messageResource.get`. Stickers are not supported by the Feishu API. */
95
+ private static readonly DOWNLOADABLE_TYPES;
96
+ /** Download attached images / files into local temp dirs; return their paths (and any failures). */
97
+ private downloadResources;
98
+ /**
99
+ * Download a resource inside a user message.
100
+ *
101
+ * Note: do NOT use the SDK's `downloadResource(fileKey, type)` — it calls
102
+ * `im.v1.image.get` / `im.v1.file.get` (download image / download file), and
103
+ * per the Feishu docs those endpoints **can only download resources uploaded
104
+ * by the bot itself**. Resources inside user-sent messages must be fetched
105
+ * with "get resource file from message" `im.v1.messageResource.get` (with
106
+ * message_id + type=image/file/audio/video), otherwise it returns HTTP 400.
107
+ */
108
+ private downloadMessageResource;
109
+ /**
110
+ * Housekeeping for the per-channel temp dirs: remove downloads older than
111
+ * 24h so a busy bot doesn't fill the OS temp partition. Best-effort — a
112
+ * failure to clean is logged, never fatal.
113
+ */
114
+ private cleanupTempDirs;
115
+ start(): Promise<void>;
116
+ stop(): Promise<void>;
117
+ /** Base chat id for outbound sends (strips any `:thread=` suffix). */
118
+ private chatIdOf;
119
+ sendText(target: OutboundTarget, text: string): Promise<void>;
120
+ sendCard(target: OutboundTarget, card: SummaryCard): Promise<void>;
121
+ streamText(target: OutboundTarget, chunks: AsyncIterable<string>): Promise<void>;
122
+ /**
123
+ * Deliver a local file to the chat. The SDK uploads the source itself:
124
+ * images are sent inline, everything else as an attachment.
125
+ */
126
+ sendFile(target: OutboundTarget, filePath: string, options?: {
127
+ filename?: string;
128
+ }): Promise<void>;
129
+ /**
130
+ * Build the send options shared by text/markdown deliveries: an optional
131
+ * reply target and @-mentions (SDK renders `<at user_id=…>` prefixes for
132
+ * both text and post messages, so group completion cards can nudge the
133
+ * requester).
134
+ */
135
+ private sendOpts;
136
+ promptChoice(target: OutboundTarget, prompt: ChoicePrompt, updateMessageId?: string): Promise<ChoiceResult>;
137
+ closeMenu(messageId: string, summary: string): Promise<void>;
138
+ }
139
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/channels/feishu/adapter.ts"],"names":[],"mappings":"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"}