@aigne/afs-dingtalk 1.11.0-beta.12

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/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+ # Proprietary License
2
+
3
+ Copyright (c) 2024-2025 ArcBlock, Inc. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. Unauthorized copying, modification, distribution, or use of
7
+ this Software, via any medium, is strictly prohibited.
8
+
9
+ The Software is provided for internal use only within ArcBlock, Inc. and its
10
+ authorized affiliates.
11
+
12
+ ## No License Granted
13
+
14
+ No license, express or implied, is granted to any party for any purpose.
15
+ All rights are reserved by ArcBlock, Inc.
16
+
17
+ ## Public Artifact Distribution
18
+
19
+ Portions of this Software may be released publicly under separate open-source
20
+ licenses (such as MIT License) through designated public repositories. Such
21
+ public releases are governed by their respective licenses and do not affect
22
+ the proprietary nature of this repository.
23
+
24
+ ## Contact
25
+
26
+ For licensing inquiries, contact: legal@arcblock.io
@@ -0,0 +1,11 @@
1
+
2
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
3
+ function __decorate(decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ }
9
+
10
+ //#endregion
11
+ exports.__decorate = __decorate;
@@ -0,0 +1,10 @@
1
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+
9
+ //#endregion
10
+ export { __decorate };
@@ -0,0 +1,152 @@
1
+
2
+ //#region src/client.ts
3
+ /**
4
+ * DingTalkClient — pure DingTalk API wrapper.
5
+ *
6
+ * Two modes:
7
+ * - Webhook: outbound-only, POST to Custom Robot webhook URL
8
+ * - Bot: bidirectional, Enterprise Internal Bot with token refresh
9
+ *
10
+ * Zero external dependencies (uses native fetch).
11
+ * No AFS or application-specific concepts.
12
+ */
13
+ const DEFAULT_LEGACY_API_BASE = "https://oapi.dingtalk.com";
14
+ const DEFAULT_NEW_API_BASE = "https://api.dingtalk.com/v1.0";
15
+ const DEFAULT_TIMEOUT = 3e4;
16
+ /** Token refresh margin: refresh 5 minutes before expiry. */
17
+ const TOKEN_REFRESH_MARGIN_MS = 300 * 1e3;
18
+ var DingTalkClient = class DingTalkClient {
19
+ mode;
20
+ _webhookUrl;
21
+ _appKey;
22
+ _appSecret;
23
+ _legacyApiBase;
24
+ _newApiBase;
25
+ _timeout;
26
+ _accessToken = null;
27
+ _tokenExpiresAt = 0;
28
+ constructor(opts) {
29
+ this.mode = opts.mode;
30
+ this._webhookUrl = opts.webhookUrl ?? null;
31
+ this._appKey = opts.appKey ?? null;
32
+ this._appSecret = opts.appSecret ?? null;
33
+ this._legacyApiBase = opts.legacyApiBase ?? DEFAULT_LEGACY_API_BASE;
34
+ this._newApiBase = opts.newApiBase ?? DEFAULT_NEW_API_BASE;
35
+ this._timeout = opts.timeout ?? DEFAULT_TIMEOUT;
36
+ }
37
+ static webhook(accessToken, options) {
38
+ if (!accessToken) throw new Error("DingTalkClient.webhook requires an accessToken");
39
+ return new DingTalkClient({
40
+ mode: "webhook",
41
+ webhookUrl: `${DEFAULT_LEGACY_API_BASE}/robot/send?access_token=${accessToken}`,
42
+ timeout: options?.timeout
43
+ });
44
+ }
45
+ static bot(options) {
46
+ if (!options.appKey) throw new Error("DingTalkClient.bot requires an appKey");
47
+ if (!options.appSecret) throw new Error("DingTalkClient.bot requires an appSecret");
48
+ return new DingTalkClient({
49
+ mode: "bot",
50
+ appKey: options.appKey,
51
+ appSecret: options.appSecret,
52
+ legacyApiBase: options.legacyApiBase,
53
+ newApiBase: options.newApiBase,
54
+ timeout: options.timeout
55
+ });
56
+ }
57
+ async sendWebhook(text) {
58
+ if (!this._webhookUrl) throw new Error("sendWebhook requires webhook mode");
59
+ const data = await (await fetch(this._webhookUrl, {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({
63
+ msgtype: "text",
64
+ text: { content: text }
65
+ }),
66
+ signal: AbortSignal.timeout(this._timeout)
67
+ })).json();
68
+ if (data.errcode !== 0) throw new Error(`DingTalk webhook error ${data.errcode}: ${data.errmsg}`);
69
+ }
70
+ async sendGroupMessage(conversationId, text) {
71
+ await this._ensureToken();
72
+ const response = await fetch(`${this._newApiBase}/robot/groupMessages/send`, {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ "x-acs-dingtalk-access-token": this._accessToken
77
+ },
78
+ body: JSON.stringify({
79
+ msgParam: JSON.stringify({ content: text }),
80
+ msgKey: "sampleText",
81
+ openConversationId: conversationId
82
+ }),
83
+ signal: AbortSignal.timeout(this._timeout)
84
+ });
85
+ if (!response.ok) {
86
+ const errorText = await response.text().catch(() => "");
87
+ throw new Error(`DingTalk API error ${response.status}: ${errorText}`);
88
+ }
89
+ }
90
+ async _ensureToken() {
91
+ if (!this._appKey || !this._appSecret) throw new Error("_ensureToken requires bot mode (appKey + appSecret)");
92
+ if (this._accessToken && Date.now() < this._tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS) return;
93
+ const url = `${this._legacyApiBase}/gettoken?appkey=${encodeURIComponent(this._appKey)}&appsecret=${encodeURIComponent(this._appSecret)}`;
94
+ const data = await (await fetch(url, {
95
+ method: "GET",
96
+ signal: AbortSignal.timeout(this._timeout)
97
+ })).json();
98
+ if (data.errcode && data.errcode !== 0) {
99
+ const desc = (data.errmsg || "Unknown error").replace(this._appKey, "***").replace(this._appSecret, "***");
100
+ throw new Error(`DingTalk token error ${data.errcode}: ${desc}`);
101
+ }
102
+ if (!data.access_token) throw new Error("DingTalk token response missing access_token");
103
+ this._accessToken = data.access_token;
104
+ this._tokenExpiresAt = Date.now() + (data.expires_in ?? 7200) * 1e3;
105
+ }
106
+ /**
107
+ * Parse a DingTalk outgoing webhook callback payload.
108
+ *
109
+ * DingTalk sends HTTP POST with:
110
+ * ```json
111
+ * {
112
+ * "conversationId": "cidXXX",
113
+ * "senderNick": "Alice",
114
+ * "senderStaffId": "user123",
115
+ * "text": { "content": "hello" },
116
+ * "msgtype": "text",
117
+ * "msgId": "msgXXX",
118
+ * "createAt": 1700000000000,
119
+ * "conversationType": "2"
120
+ * }
121
+ * ```
122
+ */
123
+ static parseEvent(payload) {
124
+ if (!payload) return null;
125
+ if (payload.msgtype !== "text") return null;
126
+ const content = (payload.text?.content || "").trim();
127
+ if (!content) return null;
128
+ const msgId = payload.msgId;
129
+ const conversationId = payload.conversationId;
130
+ const senderStaffId = payload.senderStaffId || "";
131
+ const senderNick = payload.senderNick || "";
132
+ const conversationType = payload.conversationType || "1";
133
+ const createAt = payload.createAt || Date.now();
134
+ if (!msgId || !conversationId) return null;
135
+ return {
136
+ msgId,
137
+ content,
138
+ conversationId,
139
+ senderStaffId,
140
+ senderNick,
141
+ conversationType,
142
+ createAt
143
+ };
144
+ }
145
+ /** Escape special characters for DingTalk markdown. */
146
+ static escapeMarkdown(text) {
147
+ return text.replace(/([*_~`|\\#])/g, "\\$1");
148
+ }
149
+ };
150
+
151
+ //#endregion
152
+ exports.DingTalkClient = DingTalkClient;
@@ -0,0 +1,72 @@
1
+ //#region src/client.d.ts
2
+ /**
3
+ * DingTalkClient — pure DingTalk API wrapper.
4
+ *
5
+ * Two modes:
6
+ * - Webhook: outbound-only, POST to Custom Robot webhook URL
7
+ * - Bot: bidirectional, Enterprise Internal Bot with token refresh
8
+ *
9
+ * Zero external dependencies (uses native fetch).
10
+ * No AFS or application-specific concepts.
11
+ */
12
+ interface DingTalkBotOptions {
13
+ appKey: string;
14
+ appSecret: string;
15
+ /** Legacy API base (default: https://oapi.dingtalk.com) */
16
+ legacyApiBase?: string;
17
+ /** New API base (default: https://api.dingtalk.com/v1.0) */
18
+ newApiBase?: string;
19
+ timeout?: number;
20
+ }
21
+ /** Parsed DingTalk outgoing webhook event. */
22
+ interface DingTalkEvent {
23
+ msgId: string;
24
+ content: string;
25
+ conversationId: string;
26
+ senderStaffId: string;
27
+ senderNick: string;
28
+ conversationType: string;
29
+ createAt: number;
30
+ }
31
+ declare class DingTalkClient {
32
+ readonly mode: "webhook" | "bot";
33
+ private readonly _webhookUrl;
34
+ private readonly _appKey;
35
+ private readonly _appSecret;
36
+ private readonly _legacyApiBase;
37
+ private readonly _newApiBase;
38
+ private readonly _timeout;
39
+ private _accessToken;
40
+ private _tokenExpiresAt;
41
+ private constructor();
42
+ static webhook(accessToken: string, options?: {
43
+ timeout?: number;
44
+ }): DingTalkClient;
45
+ static bot(options: DingTalkBotOptions): DingTalkClient;
46
+ sendWebhook(text: string): Promise<void>;
47
+ sendGroupMessage(conversationId: string, text: string): Promise<void>;
48
+ _ensureToken(): Promise<void>;
49
+ /**
50
+ * Parse a DingTalk outgoing webhook callback payload.
51
+ *
52
+ * DingTalk sends HTTP POST with:
53
+ * ```json
54
+ * {
55
+ * "conversationId": "cidXXX",
56
+ * "senderNick": "Alice",
57
+ * "senderStaffId": "user123",
58
+ * "text": { "content": "hello" },
59
+ * "msgtype": "text",
60
+ * "msgId": "msgXXX",
61
+ * "createAt": 1700000000000,
62
+ * "conversationType": "2"
63
+ * }
64
+ * ```
65
+ */
66
+ static parseEvent(payload: any): DingTalkEvent | null;
67
+ /** Escape special characters for DingTalk markdown. */
68
+ static escapeMarkdown(text: string): string;
69
+ }
70
+ //#endregion
71
+ export { DingTalkClient, DingTalkEvent };
72
+ //# sourceMappingURL=client.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";;AAkBA;;;;;;;;;UAAiB,kBAAA;EACf,MAAA;EACA,SAAA;EAS4B;EAP5B,aAAA;EAO4B;EAL5B,UAAA;EACA,OAAA;AAAA;;UAIe,aAAA;EACf,KAAA;EACA,OAAA;EACA,cAAA;EACA,aAAA;EACA,UAAA;EACA,gBAAA;EACA,QAAA;AAAA;AAAA,cAGW,cAAA;EAAA,SACF,IAAA;EAAA,iBACQ,WAAA;EAAA,iBACA,OAAA;EAAA,iBACA,UAAA;EAAA,iBACA,cAAA;EAAA,iBACA,WAAA;EAAA,iBACA,QAAA;EAAA,QAET,YAAA;EAAA,QACA,eAAA;EAAA,QAED,WAAA,CAAA;EAAA,OAoBA,OAAA,CAAQ,WAAA,UAAqB,OAAA;IAAY,OAAA;EAAA,IAAqB,cAAA;EAAA,OAW9D,GAAA,CAAI,OAAA,EAAS,kBAAA,GAAqB,cAAA;EAenC,WAAA,CAAY,IAAA,WAAe,OAAA;EAqB3B,gBAAA,CAAiB,cAAA,UAAwB,IAAA,WAAe,OAAA;EAyBxD,YAAA,CAAA,GAAgB,OAAA;EAxEf;;;;;;;;;;;;;;;;;EAAA,OAiIA,UAAA,CAAW,OAAA,QAAe,aAAA;EAA1B;EAAA,OA8BA,cAAA,CAAe,IAAA;AAAA"}
@@ -0,0 +1,72 @@
1
+ //#region src/client.d.ts
2
+ /**
3
+ * DingTalkClient — pure DingTalk API wrapper.
4
+ *
5
+ * Two modes:
6
+ * - Webhook: outbound-only, POST to Custom Robot webhook URL
7
+ * - Bot: bidirectional, Enterprise Internal Bot with token refresh
8
+ *
9
+ * Zero external dependencies (uses native fetch).
10
+ * No AFS or application-specific concepts.
11
+ */
12
+ interface DingTalkBotOptions {
13
+ appKey: string;
14
+ appSecret: string;
15
+ /** Legacy API base (default: https://oapi.dingtalk.com) */
16
+ legacyApiBase?: string;
17
+ /** New API base (default: https://api.dingtalk.com/v1.0) */
18
+ newApiBase?: string;
19
+ timeout?: number;
20
+ }
21
+ /** Parsed DingTalk outgoing webhook event. */
22
+ interface DingTalkEvent {
23
+ msgId: string;
24
+ content: string;
25
+ conversationId: string;
26
+ senderStaffId: string;
27
+ senderNick: string;
28
+ conversationType: string;
29
+ createAt: number;
30
+ }
31
+ declare class DingTalkClient {
32
+ readonly mode: "webhook" | "bot";
33
+ private readonly _webhookUrl;
34
+ private readonly _appKey;
35
+ private readonly _appSecret;
36
+ private readonly _legacyApiBase;
37
+ private readonly _newApiBase;
38
+ private readonly _timeout;
39
+ private _accessToken;
40
+ private _tokenExpiresAt;
41
+ private constructor();
42
+ static webhook(accessToken: string, options?: {
43
+ timeout?: number;
44
+ }): DingTalkClient;
45
+ static bot(options: DingTalkBotOptions): DingTalkClient;
46
+ sendWebhook(text: string): Promise<void>;
47
+ sendGroupMessage(conversationId: string, text: string): Promise<void>;
48
+ _ensureToken(): Promise<void>;
49
+ /**
50
+ * Parse a DingTalk outgoing webhook callback payload.
51
+ *
52
+ * DingTalk sends HTTP POST with:
53
+ * ```json
54
+ * {
55
+ * "conversationId": "cidXXX",
56
+ * "senderNick": "Alice",
57
+ * "senderStaffId": "user123",
58
+ * "text": { "content": "hello" },
59
+ * "msgtype": "text",
60
+ * "msgId": "msgXXX",
61
+ * "createAt": 1700000000000,
62
+ * "conversationType": "2"
63
+ * }
64
+ * ```
65
+ */
66
+ static parseEvent(payload: any): DingTalkEvent | null;
67
+ /** Escape special characters for DingTalk markdown. */
68
+ static escapeMarkdown(text: string): string;
69
+ }
70
+ //#endregion
71
+ export { DingTalkClient, DingTalkEvent };
72
+ //# sourceMappingURL=client.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;AAkBA;;;;;;;;;UAAiB,kBAAA;EACf,MAAA;EACA,SAAA;EAS4B;EAP5B,aAAA;EAO4B;EAL5B,UAAA;EACA,OAAA;AAAA;;UAIe,aAAA;EACf,KAAA;EACA,OAAA;EACA,cAAA;EACA,aAAA;EACA,UAAA;EACA,gBAAA;EACA,QAAA;AAAA;AAAA,cAGW,cAAA;EAAA,SACF,IAAA;EAAA,iBACQ,WAAA;EAAA,iBACA,OAAA;EAAA,iBACA,UAAA;EAAA,iBACA,cAAA;EAAA,iBACA,WAAA;EAAA,iBACA,QAAA;EAAA,QAET,YAAA;EAAA,QACA,eAAA;EAAA,QAED,WAAA,CAAA;EAAA,OAoBA,OAAA,CAAQ,WAAA,UAAqB,OAAA;IAAY,OAAA;EAAA,IAAqB,cAAA;EAAA,OAW9D,GAAA,CAAI,OAAA,EAAS,kBAAA,GAAqB,cAAA;EAenC,WAAA,CAAY,IAAA,WAAe,OAAA;EAqB3B,gBAAA,CAAiB,cAAA,UAAwB,IAAA,WAAe,OAAA;EAyBxD,YAAA,CAAA,GAAgB,OAAA;EAxEf;;;;;;;;;;;;;;;;;EAAA,OAiIA,UAAA,CAAW,OAAA,QAAe,aAAA;EAA1B;EAAA,OA8BA,cAAA,CAAe,IAAA;AAAA"}
@@ -0,0 +1,152 @@
1
+ //#region src/client.ts
2
+ /**
3
+ * DingTalkClient — pure DingTalk API wrapper.
4
+ *
5
+ * Two modes:
6
+ * - Webhook: outbound-only, POST to Custom Robot webhook URL
7
+ * - Bot: bidirectional, Enterprise Internal Bot with token refresh
8
+ *
9
+ * Zero external dependencies (uses native fetch).
10
+ * No AFS or application-specific concepts.
11
+ */
12
+ const DEFAULT_LEGACY_API_BASE = "https://oapi.dingtalk.com";
13
+ const DEFAULT_NEW_API_BASE = "https://api.dingtalk.com/v1.0";
14
+ const DEFAULT_TIMEOUT = 3e4;
15
+ /** Token refresh margin: refresh 5 minutes before expiry. */
16
+ const TOKEN_REFRESH_MARGIN_MS = 300 * 1e3;
17
+ var DingTalkClient = class DingTalkClient {
18
+ mode;
19
+ _webhookUrl;
20
+ _appKey;
21
+ _appSecret;
22
+ _legacyApiBase;
23
+ _newApiBase;
24
+ _timeout;
25
+ _accessToken = null;
26
+ _tokenExpiresAt = 0;
27
+ constructor(opts) {
28
+ this.mode = opts.mode;
29
+ this._webhookUrl = opts.webhookUrl ?? null;
30
+ this._appKey = opts.appKey ?? null;
31
+ this._appSecret = opts.appSecret ?? null;
32
+ this._legacyApiBase = opts.legacyApiBase ?? DEFAULT_LEGACY_API_BASE;
33
+ this._newApiBase = opts.newApiBase ?? DEFAULT_NEW_API_BASE;
34
+ this._timeout = opts.timeout ?? DEFAULT_TIMEOUT;
35
+ }
36
+ static webhook(accessToken, options) {
37
+ if (!accessToken) throw new Error("DingTalkClient.webhook requires an accessToken");
38
+ return new DingTalkClient({
39
+ mode: "webhook",
40
+ webhookUrl: `${DEFAULT_LEGACY_API_BASE}/robot/send?access_token=${accessToken}`,
41
+ timeout: options?.timeout
42
+ });
43
+ }
44
+ static bot(options) {
45
+ if (!options.appKey) throw new Error("DingTalkClient.bot requires an appKey");
46
+ if (!options.appSecret) throw new Error("DingTalkClient.bot requires an appSecret");
47
+ return new DingTalkClient({
48
+ mode: "bot",
49
+ appKey: options.appKey,
50
+ appSecret: options.appSecret,
51
+ legacyApiBase: options.legacyApiBase,
52
+ newApiBase: options.newApiBase,
53
+ timeout: options.timeout
54
+ });
55
+ }
56
+ async sendWebhook(text) {
57
+ if (!this._webhookUrl) throw new Error("sendWebhook requires webhook mode");
58
+ const data = await (await fetch(this._webhookUrl, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({
62
+ msgtype: "text",
63
+ text: { content: text }
64
+ }),
65
+ signal: AbortSignal.timeout(this._timeout)
66
+ })).json();
67
+ if (data.errcode !== 0) throw new Error(`DingTalk webhook error ${data.errcode}: ${data.errmsg}`);
68
+ }
69
+ async sendGroupMessage(conversationId, text) {
70
+ await this._ensureToken();
71
+ const response = await fetch(`${this._newApiBase}/robot/groupMessages/send`, {
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ "x-acs-dingtalk-access-token": this._accessToken
76
+ },
77
+ body: JSON.stringify({
78
+ msgParam: JSON.stringify({ content: text }),
79
+ msgKey: "sampleText",
80
+ openConversationId: conversationId
81
+ }),
82
+ signal: AbortSignal.timeout(this._timeout)
83
+ });
84
+ if (!response.ok) {
85
+ const errorText = await response.text().catch(() => "");
86
+ throw new Error(`DingTalk API error ${response.status}: ${errorText}`);
87
+ }
88
+ }
89
+ async _ensureToken() {
90
+ if (!this._appKey || !this._appSecret) throw new Error("_ensureToken requires bot mode (appKey + appSecret)");
91
+ if (this._accessToken && Date.now() < this._tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS) return;
92
+ const url = `${this._legacyApiBase}/gettoken?appkey=${encodeURIComponent(this._appKey)}&appsecret=${encodeURIComponent(this._appSecret)}`;
93
+ const data = await (await fetch(url, {
94
+ method: "GET",
95
+ signal: AbortSignal.timeout(this._timeout)
96
+ })).json();
97
+ if (data.errcode && data.errcode !== 0) {
98
+ const desc = (data.errmsg || "Unknown error").replace(this._appKey, "***").replace(this._appSecret, "***");
99
+ throw new Error(`DingTalk token error ${data.errcode}: ${desc}`);
100
+ }
101
+ if (!data.access_token) throw new Error("DingTalk token response missing access_token");
102
+ this._accessToken = data.access_token;
103
+ this._tokenExpiresAt = Date.now() + (data.expires_in ?? 7200) * 1e3;
104
+ }
105
+ /**
106
+ * Parse a DingTalk outgoing webhook callback payload.
107
+ *
108
+ * DingTalk sends HTTP POST with:
109
+ * ```json
110
+ * {
111
+ * "conversationId": "cidXXX",
112
+ * "senderNick": "Alice",
113
+ * "senderStaffId": "user123",
114
+ * "text": { "content": "hello" },
115
+ * "msgtype": "text",
116
+ * "msgId": "msgXXX",
117
+ * "createAt": 1700000000000,
118
+ * "conversationType": "2"
119
+ * }
120
+ * ```
121
+ */
122
+ static parseEvent(payload) {
123
+ if (!payload) return null;
124
+ if (payload.msgtype !== "text") return null;
125
+ const content = (payload.text?.content || "").trim();
126
+ if (!content) return null;
127
+ const msgId = payload.msgId;
128
+ const conversationId = payload.conversationId;
129
+ const senderStaffId = payload.senderStaffId || "";
130
+ const senderNick = payload.senderNick || "";
131
+ const conversationType = payload.conversationType || "1";
132
+ const createAt = payload.createAt || Date.now();
133
+ if (!msgId || !conversationId) return null;
134
+ return {
135
+ msgId,
136
+ content,
137
+ conversationId,
138
+ senderStaffId,
139
+ senderNick,
140
+ conversationType,
141
+ createAt
142
+ };
143
+ }
144
+ /** Escape special characters for DingTalk markdown. */
145
+ static escapeMarkdown(text) {
146
+ return text.replace(/([*_~`|\\#])/g, "\\$1");
147
+ }
148
+ };
149
+
150
+ //#endregion
151
+ export { DingTalkClient };
152
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * DingTalkClient — pure DingTalk API wrapper.\n *\n * Two modes:\n * - Webhook: outbound-only, POST to Custom Robot webhook URL\n * - Bot: bidirectional, Enterprise Internal Bot with token refresh\n *\n * Zero external dependencies (uses native fetch).\n * No AFS or application-specific concepts.\n */\n\nconst DEFAULT_LEGACY_API_BASE = \"https://oapi.dingtalk.com\";\nconst DEFAULT_NEW_API_BASE = \"https://api.dingtalk.com/v1.0\";\nconst DEFAULT_TIMEOUT = 30_000;\n\n/** Token refresh margin: refresh 5 minutes before expiry. */\nconst TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1000;\n\nexport interface DingTalkBotOptions {\n appKey: string;\n appSecret: string;\n /** Legacy API base (default: https://oapi.dingtalk.com) */\n legacyApiBase?: string;\n /** New API base (default: https://api.dingtalk.com/v1.0) */\n newApiBase?: string;\n timeout?: number;\n}\n\n/** Parsed DingTalk outgoing webhook event. */\nexport interface DingTalkEvent {\n msgId: string;\n content: string;\n conversationId: string;\n senderStaffId: string;\n senderNick: string;\n conversationType: string;\n createAt: number;\n}\n\nexport class DingTalkClient {\n readonly mode: \"webhook\" | \"bot\";\n private readonly _webhookUrl: string | null;\n private readonly _appKey: string | null;\n private readonly _appSecret: string | null;\n private readonly _legacyApiBase: string;\n private readonly _newApiBase: string;\n private readonly _timeout: number;\n\n private _accessToken: string | null = null;\n private _tokenExpiresAt = 0;\n\n private constructor(opts: {\n mode: \"webhook\" | \"bot\";\n webhookUrl?: string;\n appKey?: string;\n appSecret?: string;\n legacyApiBase?: string;\n newApiBase?: string;\n timeout?: number;\n }) {\n this.mode = opts.mode;\n this._webhookUrl = opts.webhookUrl ?? null;\n this._appKey = opts.appKey ?? null;\n this._appSecret = opts.appSecret ?? null;\n this._legacyApiBase = opts.legacyApiBase ?? DEFAULT_LEGACY_API_BASE;\n this._newApiBase = opts.newApiBase ?? DEFAULT_NEW_API_BASE;\n this._timeout = opts.timeout ?? DEFAULT_TIMEOUT;\n }\n\n // ─── Factory Methods ──────────────────────────────────────\n\n static webhook(accessToken: string, options?: { timeout?: number }): DingTalkClient {\n if (!accessToken) throw new Error(\"DingTalkClient.webhook requires an accessToken\");\n\n const webhookUrl = `${DEFAULT_LEGACY_API_BASE}/robot/send?access_token=${accessToken}`;\n return new DingTalkClient({\n mode: \"webhook\",\n webhookUrl,\n timeout: options?.timeout,\n });\n }\n\n static bot(options: DingTalkBotOptions): DingTalkClient {\n if (!options.appKey) throw new Error(\"DingTalkClient.bot requires an appKey\");\n if (!options.appSecret) throw new Error(\"DingTalkClient.bot requires an appSecret\");\n return new DingTalkClient({\n mode: \"bot\",\n appKey: options.appKey,\n appSecret: options.appSecret,\n legacyApiBase: options.legacyApiBase,\n newApiBase: options.newApiBase,\n timeout: options.timeout,\n });\n }\n\n // ─── Webhook Mode ─────────────────────────────────────────\n\n async sendWebhook(text: string): Promise<void> {\n if (!this._webhookUrl) throw new Error(\"sendWebhook requires webhook mode\");\n\n const response = await fetch(this._webhookUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n msgtype: \"text\",\n text: { content: text },\n }),\n signal: AbortSignal.timeout(this._timeout),\n });\n\n const data = (await response.json()) as { errcode: number; errmsg: string };\n if (data.errcode !== 0) {\n throw new Error(`DingTalk webhook error ${data.errcode}: ${data.errmsg}`);\n }\n }\n\n // ─── Bot Mode ─────────────────────────────────────────────\n\n async sendGroupMessage(conversationId: string, text: string): Promise<void> {\n await this._ensureToken();\n\n const response = await fetch(`${this._newApiBase}/robot/groupMessages/send`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-acs-dingtalk-access-token\": this._accessToken!,\n },\n body: JSON.stringify({\n msgParam: JSON.stringify({ content: text }),\n msgKey: \"sampleText\",\n openConversationId: conversationId,\n }),\n signal: AbortSignal.timeout(this._timeout),\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"\");\n throw new Error(`DingTalk API error ${response.status}: ${errorText}`);\n }\n }\n\n // ─── Token Management ─────────────────────────────────────\n\n async _ensureToken(): Promise<void> {\n if (!this._appKey || !this._appSecret) {\n throw new Error(\"_ensureToken requires bot mode (appKey + appSecret)\");\n }\n\n if (this._accessToken && Date.now() < this._tokenExpiresAt - TOKEN_REFRESH_MARGIN_MS) {\n return;\n }\n\n const url = `${this._legacyApiBase}/gettoken?appkey=${encodeURIComponent(this._appKey)}&appsecret=${encodeURIComponent(this._appSecret)}`;\n\n const response = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(this._timeout),\n });\n\n const data = (await response.json()) as {\n errcode?: number;\n errmsg?: string;\n access_token?: string;\n expires_in?: number;\n };\n\n if (data.errcode && data.errcode !== 0) {\n const desc = (data.errmsg || \"Unknown error\")\n .replace(this._appKey, \"***\")\n .replace(this._appSecret, \"***\");\n throw new Error(`DingTalk token error ${data.errcode}: ${desc}`);\n }\n\n if (!data.access_token) {\n throw new Error(\"DingTalk token response missing access_token\");\n }\n\n this._accessToken = data.access_token;\n this._tokenExpiresAt = Date.now() + (data.expires_in ?? 7200) * 1000;\n }\n\n // ─── Event Parsing ────────────────────────────────────────\n\n /**\n * Parse a DingTalk outgoing webhook callback payload.\n *\n * DingTalk sends HTTP POST with:\n * ```json\n * {\n * \"conversationId\": \"cidXXX\",\n * \"senderNick\": \"Alice\",\n * \"senderStaffId\": \"user123\",\n * \"text\": { \"content\": \"hello\" },\n * \"msgtype\": \"text\",\n * \"msgId\": \"msgXXX\",\n * \"createAt\": 1700000000000,\n * \"conversationType\": \"2\"\n * }\n * ```\n */\n static parseEvent(payload: any): DingTalkEvent | null {\n if (!payload) return null;\n if (payload.msgtype !== \"text\") return null;\n\n const content = (payload.text?.content || \"\").trim();\n if (!content) return null;\n\n const msgId = payload.msgId;\n const conversationId = payload.conversationId;\n const senderStaffId = payload.senderStaffId || \"\";\n const senderNick = payload.senderNick || \"\";\n const conversationType = payload.conversationType || \"1\";\n const createAt = payload.createAt || Date.now();\n\n if (!msgId || !conversationId) return null;\n\n return {\n msgId,\n content,\n conversationId,\n senderStaffId,\n senderNick,\n conversationType,\n createAt,\n };\n }\n\n // ─── Helpers ──────────────────────────────────────────────\n\n /** Escape special characters for DingTalk markdown. */\n static escapeMarkdown(text: string): string {\n return text.replace(/([*_~`|\\\\#])/g, \"\\\\$1\");\n }\n}\n"],"mappings":"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"}