moltbot-dingtalk-stream 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # DingTalk Channel for Moltbot
2
+
3
+ A Moltbot channel plugin for DingTalk (钉钉) using **Stream Mode** for seamless integration.
4
+
5
+ ## Features
6
+
7
+ - **Stream Mode**: Uses WebSocket for receiving messages (no public IP required)
8
+ - **Zero Configuration**: No webhook setup, ngrok, or firewall configuration needed
9
+ - **Single/Group Chat**: Supports both direct messages and group mentions
10
+ - **Easy Setup**: Just configure your DingTalk app credentials
11
+
12
+ ## Proactive Messaging (CLI)
13
+
14
+ You can send messages to DingTalk conversations using the Clawdbot CLI. You need the `conversationId` (which you can find in the logs when a message is received).
15
+
16
+ ```bash
17
+ clawdbot send --channel dingtalk --to <conversationId> "Hello from CLI"
18
+ ```
19
+
20
+ ## Troubleshooting
21
+
22
+ - **Logs**: Check `~/.clawdbot/logs/gateway.log` for debug information.
23
+ - **Connection**: Ensure your server has outbound internet access to DingTalk servers.
24
+ - **Permissions**: Verify your DingTalk app has the necessary robot permissions.
25
+
26
+ ## Quick Start
27
+
28
+ 1. Install the plugin:
29
+ ```bash
30
+ npm install moltbot-dingtalk-stream
31
+ ```
32
+
33
+ 2. Configure in your moltbot config:
34
+ ```json
35
+ {
36
+ "channels": {
37
+ "dingtalk": {
38
+ "accounts": {
39
+ "default": {
40
+ "enabled": true,
41
+ "clientId": "YOUR_APP_KEY",
42
+ "clientSecret": "YOUR_APP_SECRET"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ 3. Set up your DingTalk app:
51
+ - Create an enterprise internal app at [DingTalk Developer Console](https://open.dingtalk.com/)
52
+ - Add Robot capability with **Stream Mode** enabled
53
+ - Use the AppKey as `clientId` and AppSecret as `clientSecret`
54
+
55
+ ## Documentation
56
+
57
+ See [DEVELOPMENT.md](./DEVELOPMENT.md) for detailed setup and configuration.
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,19 @@
1
+ {
2
+ "id": "dingtalk-channel",
3
+ "name": "DingTalk Channel",
4
+ "version": "2.0.0",
5
+ "description": "Custom channel for integrating Clawdbot with DingTalk (Stream Mode)",
6
+ "author": "Your Name",
7
+ "license": "MIT",
8
+ "extensions": [
9
+ "./index.js"
10
+ ],
11
+ "channels": [
12
+ "dingtalk"
13
+ ],
14
+ "configSchema": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "properties": {}
18
+ }
19
+ }
@@ -0,0 +1,39 @@
1
+ interface ClawdbotPluginApi {
2
+ config: ClawdbotConfig;
3
+ logger: any;
4
+ runtime: any;
5
+ postMessage(params: any): Promise<void>;
6
+ registerChannel(opts: {
7
+ plugin: any;
8
+ }): void;
9
+ registerService(service: any): void;
10
+ }
11
+ interface ClawdbotConfig {
12
+ channels?: {
13
+ dingtalk?: {
14
+ accounts?: {
15
+ [key: string]: DingTalkAccountConfig;
16
+ };
17
+ };
18
+ [key: string]: any;
19
+ };
20
+ }
21
+ interface DingTalkAccountConfig {
22
+ enabled?: boolean;
23
+ clientId: string;
24
+ clientSecret: string;
25
+ webhookUrl?: string;
26
+ name?: string;
27
+ }
28
+ declare const plugin: {
29
+ id: string;
30
+ name: string;
31
+ description: string;
32
+ configSchema: {
33
+ type: "object";
34
+ properties: {};
35
+ };
36
+ register(api: ClawdbotPluginApi): void;
37
+ };
38
+ export default plugin;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,UAAU,iBAAiB;IACzB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,GAAG,CAAC;IACZ,OAAO,EAAE,GAAG,CAAC;IACb,WAAW,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,GAAG,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7C,eAAe,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;CACrC;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE;YACT,QAAQ,CAAC,EAAE;gBACT,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,CAAC;aACtC,CAAC;SACH,CAAC;QACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;CACH;AAED,UAAU,qBAAqB;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAwUD,QAAA,MAAM,MAAM;;;;;;;;kBAQI,iBAAiB;CAIhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const dingtalk_stream_1 = require("dingtalk-stream");
7
+ const axios_1 = __importDefault(require("axios"));
8
+ // Store plugin runtime
9
+ let pluginRuntime = null;
10
+ // Store session webhooks for reply
11
+ const sessionWebhooks = new Map();
12
+ // Store active clients for each account
13
+ const activeClients = new Map();
14
+ // Helper functions
15
+ function listDingTalkAccountIds(cfg) {
16
+ const accounts = cfg.channels?.dingtalk?.accounts;
17
+ return accounts ? Object.keys(accounts) : [];
18
+ }
19
+ function resolveDingTalkAccount(opts) {
20
+ const { cfg, accountId = 'default' } = opts;
21
+ const account = cfg.channels?.dingtalk?.accounts?.[accountId];
22
+ return {
23
+ accountId,
24
+ name: account?.name,
25
+ enabled: account?.enabled ?? false,
26
+ configured: Boolean(account?.clientId && account?.clientSecret),
27
+ config: account || { clientId: '', clientSecret: '' }
28
+ };
29
+ }
30
+ // DingTalk Channel Plugin
31
+ const dingTalkChannelPlugin = {
32
+ id: "dingtalk",
33
+ meta: {
34
+ id: "dingtalk",
35
+ label: "钉钉",
36
+ selectionLabel: "DingTalk Bot (Stream)",
37
+ docsPath: "/channels/dingtalk",
38
+ docsLabel: "dingtalk",
39
+ blurb: "钉钉机器人通道插件 (Stream模式)",
40
+ order: 100,
41
+ aliases: ["dt", "ding"],
42
+ },
43
+ capabilities: {
44
+ chatTypes: ["direct", "group"],
45
+ },
46
+ reload: { configPrefixes: ["channels.dingtalk"] },
47
+ configSchema: {
48
+ type: "object",
49
+ properties: {
50
+ channels: {
51
+ type: "object",
52
+ properties: {
53
+ dingtalk: {
54
+ type: "object",
55
+ properties: {
56
+ accounts: {
57
+ type: "object",
58
+ additionalProperties: {
59
+ type: "object",
60
+ properties: {
61
+ enabled: { type: "boolean" },
62
+ clientId: { type: "string" },
63
+ clientSecret: { type: "string" },
64
+ webhookUrl: { type: "string" },
65
+ name: { type: "string" },
66
+ },
67
+ required: ["clientId", "clientSecret"],
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ config: {
77
+ listAccountIds: (cfg) => listDingTalkAccountIds(cfg),
78
+ resolveAccount: (cfg, accountId) => resolveDingTalkAccount({ cfg, accountId }),
79
+ defaultAccountId: (_cfg) => 'default',
80
+ isConfigured: (account) => account.configured,
81
+ describeAccount: (account) => ({
82
+ accountId: account.accountId,
83
+ name: account.name,
84
+ enabled: account.enabled,
85
+ configured: account.configured,
86
+ }),
87
+ },
88
+ gateway: {
89
+ startAccount: async (ctx) => {
90
+ const account = ctx.account;
91
+ const config = account.config;
92
+ const accountId = account.accountId;
93
+ if (!config.clientId || !config.clientSecret) {
94
+ ctx.log?.warn?.(`[${accountId}] missing clientId or clientSecret`);
95
+ return;
96
+ }
97
+ ctx.log?.info?.(`[${accountId}] starting DingTalk Stream client`);
98
+ try {
99
+ const client = new dingtalk_stream_1.DWClient({
100
+ clientId: config.clientId,
101
+ clientSecret: config.clientSecret,
102
+ });
103
+ // Helper to safely handle messages
104
+ const handleMessage = async (res) => {
105
+ try {
106
+ const message = JSON.parse(res.data);
107
+ const textContent = message.text?.content || "";
108
+ const senderId = message.senderId;
109
+ const convoId = message.conversationId;
110
+ const msgId = message.msgId;
111
+ // Store session webhook if provided (DingTalk Stream mode provides this for replies)
112
+ if (message.sessionWebhook) {
113
+ sessionWebhooks.set(convoId, message.sessionWebhook);
114
+ }
115
+ // Log reception
116
+ ctx.log?.info?.(`[${accountId}] received message from ${message.senderNick || senderId}: ${textContent}`);
117
+ // Filter out empty messages
118
+ if (!textContent)
119
+ return;
120
+ // Simple text cleaning (remove @bot mentions if possible, though DingTalk usually gives clean content or we might need to parse entities)
121
+ const cleanedText = textContent.replace(/@\w+\s*/g, '').trim();
122
+ // Forward the message to Clawdbot for processing
123
+ if (pluginRuntime?.runtime?.channel?.reply) {
124
+ const replyModule = pluginRuntime.runtime.channel.reply;
125
+ const chatType = String(message.conversationType) === '2' ? 'group' : 'direct';
126
+ const fromAddress = chatType === 'group' ? `dingtalk:group:${convoId}` : `dingtalk:${senderId}`;
127
+ const ctxPayload = {
128
+ Body: cleanedText,
129
+ RawBody: textContent,
130
+ CommandBody: cleanedText,
131
+ From: fromAddress,
132
+ To: 'bot',
133
+ SessionKey: `dingtalk:${convoId}`,
134
+ AccountId: accountId,
135
+ ChatType: chatType,
136
+ SenderName: message.senderNick,
137
+ SenderId: senderId,
138
+ Provider: 'dingtalk',
139
+ Surface: 'dingtalk',
140
+ MessageSid: message.msgId,
141
+ Timestamp: message.createAt,
142
+ // Required for some logic
143
+ GroupSubject: chatType === 'group' ? (message.conversationId) : undefined,
144
+ };
145
+ const finalizedCtx = replyModule.finalizeInboundContext(ctxPayload);
146
+ let replyBuffer = "";
147
+ let replySent = false;
148
+ const sendToDingTalk = async (text) => {
149
+ if (!text)
150
+ return;
151
+ if (replySent) {
152
+ ctx.log?.info?.(`[${accountId}] Reply already sent, skipping buffer flush.`);
153
+ return;
154
+ }
155
+ const replyWebhook = sessionWebhooks.get(convoId) || config.webhookUrl;
156
+ if (!replyWebhook) {
157
+ ctx.log?.error?.(`[${accountId}] No webhook to reply to ${convoId}`);
158
+ return;
159
+ }
160
+ try {
161
+ await axios_1.default.post(replyWebhook, {
162
+ msgtype: "text",
163
+ text: { content: text }
164
+ }, { headers: { 'Content-Type': 'application/json' } });
165
+ replySent = true;
166
+ ctx.log?.info?.(`[${accountId}] Reply sent successfully.`);
167
+ }
168
+ catch (e) {
169
+ ctx.log?.error?.(`[${accountId}] Failed to send reply: ${e}`);
170
+ }
171
+ };
172
+ const dispatcher = {
173
+ sendFinalReply: (payload) => {
174
+ const text = payload.text || payload.content || '';
175
+ sendToDingTalk(text).catch(e => ctx.log?.error?.(`[${accountId}] sendToDingTalk failed: ${e}`));
176
+ return true;
177
+ },
178
+ typing: async () => { },
179
+ reaction: async () => { },
180
+ isSynchronous: () => false,
181
+ waitForIdle: async () => { },
182
+ sendBlockReply: async (block) => {
183
+ // Accumulate text from blocks
184
+ const text = block.text || block.delta || block.content || '';
185
+ if (text) {
186
+ replyBuffer += text;
187
+ }
188
+ },
189
+ getQueuedCounts: () => ({ active: 0, queued: 0, final: 0 })
190
+ };
191
+ // Internal dispatch
192
+ const dispatchPromise = replyModule.dispatchReplyFromConfig({
193
+ ctx: finalizedCtx,
194
+ cfg: pluginRuntime.config,
195
+ dispatcher: dispatcher,
196
+ replyOptions: {}
197
+ });
198
+ // ACK immediately to prevent retries
199
+ if (res.headers && res.headers.messageId) {
200
+ client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
201
+ }
202
+ // Wait for run to finish
203
+ await dispatchPromise;
204
+ // If final reply wasn't called but we have buffer (streaming case where agent didn't return final payload?)
205
+ if (!replySent && replyBuffer) {
206
+ ctx.log?.info?.(`[${accountId}] Sending accumulated buffer from blocks (len=${replyBuffer.length}).`);
207
+ await sendToDingTalk(replyBuffer);
208
+ }
209
+ }
210
+ else {
211
+ ctx.log?.error?.(`[${accountId}] runtime.channel.reply not available`);
212
+ }
213
+ }
214
+ catch (error) {
215
+ ctx.log?.error?.(`[${accountId}] error processing message: ${error instanceof Error ? error.message : String(error)}`);
216
+ console.error('DingTalk Handler Error:', error);
217
+ }
218
+ };
219
+ // Register callback for robot messages
220
+ client.registerCallbackListener('/v1.0/im/bot/messages/get', handleMessage);
221
+ // Connect to DingTalk Stream
222
+ await client.connect();
223
+ activeClients.set(accountId, client);
224
+ ctx.log?.info?.(`[${accountId}] DingTalk Stream client connected`);
225
+ // Handle abort signal for cleanup
226
+ ctx.abortSignal?.addEventListener('abort', () => {
227
+ ctx.log?.info?.(`[${accountId}] stopping DingTalk Stream client`);
228
+ client.disconnect();
229
+ activeClients.delete(accountId);
230
+ });
231
+ }
232
+ catch (error) {
233
+ ctx.log?.error?.(`[${accountId}] failed to start: ${error instanceof Error ? error.message : String(error)}`);
234
+ throw error;
235
+ }
236
+ },
237
+ },
238
+ outbound: {
239
+ deliveryMode: "direct",
240
+ sendText: async (opts) => {
241
+ const { text, account, target } = opts;
242
+ const config = account.config;
243
+ // Try session webhook first (for replies)
244
+ const sessionWebhook = sessionWebhooks.get(target);
245
+ if (sessionWebhook) {
246
+ try {
247
+ await axios_1.default.post(sessionWebhook, {
248
+ msgtype: "text",
249
+ text: { content: text }
250
+ }, {
251
+ headers: { 'Content-Type': 'application/json' }
252
+ });
253
+ return { ok: true };
254
+ }
255
+ catch (error) {
256
+ // Fall through to webhookUrl
257
+ }
258
+ }
259
+ // Fallback to webhookUrl for proactive messages
260
+ if (config?.webhookUrl) {
261
+ try {
262
+ await axios_1.default.post(config.webhookUrl, {
263
+ msgtype: "text",
264
+ text: { content: text }
265
+ }, {
266
+ headers: { 'Content-Type': 'application/json' }
267
+ });
268
+ return { ok: true };
269
+ }
270
+ catch (error) {
271
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
272
+ }
273
+ }
274
+ return { ok: false, error: "No webhook available for sending messages" };
275
+ }
276
+ }
277
+ };
278
+ // Plugin object format required by Clawdbot
279
+ const plugin = {
280
+ id: "dingtalk-channel",
281
+ name: "DingTalk Channel",
282
+ description: "DingTalk channel plugin using Stream mode",
283
+ configSchema: {
284
+ type: "object",
285
+ properties: {}
286
+ },
287
+ register(api) {
288
+ pluginRuntime = api;
289
+ api.registerChannel({ plugin: dingTalkChannelPlugin });
290
+ }
291
+ };
292
+ exports.default = plugin;
293
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,qDAA+D;AAC/D,kDAA0B;AA2D1B,uBAAuB;AACvB,IAAI,aAAa,GAA6B,IAAI,CAAC;AAEnD,mCAAmC;AACnC,MAAM,eAAe,GAAwB,IAAI,GAAG,EAAE,CAAC;AACvD,wCAAwC;AACxC,MAAM,aAAa,GAA0B,IAAI,GAAG,EAAE,CAAC;AAEvD,mBAAmB;AACnB,SAAS,sBAAsB,CAAC,GAAmB;IACjD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;IAClD,OAAO,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAiD;IAC/E,MAAM,EAAE,GAAG,EAAE,SAAS,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5C,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,CAAC;IAC9D,OAAO;QACL,SAAS;QACT,IAAI,EAAE,OAAO,EAAE,IAAI;QACnB,OAAO,EAAE,OAAO,EAAE,OAAO,IAAI,KAAK;QAClC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,IAAI,OAAO,EAAE,YAAY,CAAC;QAC/D,MAAM,EAAE,OAAO,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;KACtD,CAAC;AACJ,CAAC;AAED,0BAA0B;AAC1B,MAAM,qBAAqB,GAAG;IAC5B,EAAE,EAAE,UAAU;IACd,IAAI,EAAE;QACJ,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,IAAI;QACX,cAAc,EAAE,uBAAuB;QACvC,QAAQ,EAAE,oBAAoB;QAC9B,SAAS,EAAE,UAAU;QACrB,KAAK,EAAE,sBAAsB;QAC7B,KAAK,EAAE,GAAG;QACV,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC;KACxB;IACD,YAAY,EAAE;QACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAU;KACxC;IACD,MAAM,EAAE,EAAE,cAAc,EAAE,CAAC,mBAAmB,CAAC,EAAE;IACjD,YAAY,EAAE;QACZ,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAiB;gBACvB,UAAU,EAAE;oBACV,QAAQ,EAAE;wBACR,IAAI,EAAE,QAAiB;wBACvB,UAAU,EAAE;4BACV,QAAQ,EAAE;gCACR,IAAI,EAAE,QAAiB;gCACvB,oBAAoB,EAAE;oCACpB,IAAI,EAAE,QAAiB;oCACvB,UAAU,EAAE;wCACV,OAAO,EAAE,EAAE,IAAI,EAAE,SAAkB,EAAE;wCACrC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;wCACrC,YAAY,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;wCACzC,UAAU,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;wCACvC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;qCAClC;oCACD,QAAQ,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC;iCACvC;6BACF;yBACF;qBACF;iBACF;aACF;SACF;KACF;IACD,MAAM,EAAE;QACN,cAAc,EAAE,CAAC,GAAmB,EAAE,EAAE,CAAC,sBAAsB,CAAC,GAAG,CAAC;QACpE,cAAc,EAAE,CAAC,GAAmB,EAAE,SAAkB,EAAE,EAAE,CAAC,sBAAsB,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;QACvG,gBAAgB,EAAE,CAAC,IAAoB,EAAE,EAAE,CAAC,SAAS;QACrD,YAAY,EAAE,CAAC,OAAgC,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU;QACtE,eAAe,EAAE,CAAC,OAAgC,EAAE,EAAE,CAAC,CAAC;YACtD,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,UAAU,EAAE,OAAO,CAAC,UAAU;SAC/B,CAAC;KACH;IACD,OAAO,EAAE;QACP,YAAY,EAAE,KAAK,EAAE,GAAQ,EAAE,EAAE;YAC/B,MAAM,OAAO,GAA4B,GAAG,CAAC,OAAO,CAAC;YACrD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC9B,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YAEpC,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;gBAC7C,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,oCAAoC,CAAC,CAAC;gBACnE,OAAO;YACT,CAAC;YAED,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,mCAAmC,CAAC,CAAC;YAElE,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,0BAAQ,CAAC;oBAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ;oBACzB,YAAY,EAAE,MAAM,CAAC,YAAY;iBAClC,CAAC,CAAC;gBAEH,mCAAmC;gBACnC,MAAM,aAAa,GAAG,KAAK,EAAE,GAAQ,EAAE,EAAE;oBACvC,IAAI,CAAC;wBACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;wBACrC,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC;wBAChD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;wBAClC,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC;wBACvC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;wBAC5B,qFAAqF;wBACrF,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;4BAC3B,eAAe,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;wBACvD,CAAC;wBAED,gBAAgB;wBAChB,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,2BAA2B,OAAO,CAAC,UAAU,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC,CAAC;wBAE1G,4BAA4B;wBAC5B,IAAI,CAAC,WAAW;4BAAE,OAAO;wBAEzB,0IAA0I;wBAC1I,MAAM,WAAW,GAAG,WAAW,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;wBAE/D,iDAAiD;wBACjD,IAAI,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;4BAC3C,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC;4BACxD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;4BAC/E,MAAM,WAAW,GAAG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC,CAAC,YAAY,QAAQ,EAAE,CAAC;4BAEhG,MAAM,UAAU,GAAG;gCACjB,IAAI,EAAE,WAAW;gCACjB,OAAO,EAAE,WAAW;gCACpB,WAAW,EAAE,WAAW;gCACxB,IAAI,EAAE,WAAW;gCACjB,EAAE,EAAE,KAAK;gCACT,UAAU,EAAE,YAAY,OAAO,EAAE;gCACjC,SAAS,EAAE,SAAS;gCACpB,QAAQ,EAAE,QAAQ;gCAClB,UAAU,EAAE,OAAO,CAAC,UAAU;gCAC9B,QAAQ,EAAE,QAAQ;gCAClB,QAAQ,EAAE,UAAU;gCACpB,OAAO,EAAE,UAAU;gCACnB,UAAU,EAAE,OAAO,CAAC,KAAK;gCACzB,SAAS,EAAE,OAAO,CAAC,QAAQ;gCAC3B,0BAA0B;gCAC1B,YAAY,EAAE,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS;6BAC1E,CAAC;4BAEF,MAAM,YAAY,GAAG,WAAW,CAAC,sBAAsB,CAAC,UAAU,CAAC,CAAC;4BAEpE,IAAI,WAAW,GAAG,EAAE,CAAC;4BACrB,IAAI,SAAS,GAAG,KAAK,CAAC;4BAEtB,MAAM,cAAc,GAAG,KAAK,EAAE,IAAY,EAAE,EAAE;gCAC5C,IAAI,CAAC,IAAI;oCAAE,OAAO;gCAClB,IAAI,SAAS,EAAE,CAAC;oCACd,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,8CAA8C,CAAC,CAAC;oCAC7E,OAAO;gCACT,CAAC;gCAED,MAAM,YAAY,GAAG,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC;gCACvE,IAAI,CAAC,YAAY,EAAE,CAAC;oCAClB,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,4BAA4B,OAAO,EAAE,CAAC,CAAC;oCACrE,OAAO;gCACT,CAAC;gCAED,IAAI,CAAC;oCACH,MAAM,eAAK,CAAC,IAAI,CAAC,YAAY,EAAE;wCAC7B,OAAO,EAAE,MAAM;wCACf,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;qCACxB,EAAE,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;oCACxD,SAAS,GAAG,IAAI,CAAC;oCACjB,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,4BAA4B,CAAC,CAAC;gCAC7D,CAAC;gCAAC,OAAO,CAAC,EAAE,CAAC;oCACX,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,2BAA2B,CAAC,EAAE,CAAC,CAAC;gCAChE,CAAC;4BACH,CAAC,CAAC;4BAEF,MAAM,UAAU,GAAG;gCACjB,cAAc,EAAE,CAAC,OAAY,EAAE,EAAE;oCAC/B,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;oCACnD,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,4BAA4B,CAAC,EAAE,CAAC,CAAC,CAAC;oCAChG,OAAO,IAAI,CAAC;gCACd,CAAC;gCACD,MAAM,EAAE,KAAK,IAAI,EAAE,GAAG,CAAC;gCACvB,QAAQ,EAAE,KAAK,IAAI,EAAE,GAAG,CAAC;gCACzB,aAAa,EAAE,GAAG,EAAE,CAAC,KAAK;gCAC1B,WAAW,EAAE,KAAK,IAAI,EAAE,GAAG,CAAC;gCAC5B,cAAc,EAAE,KAAK,EAAE,KAAU,EAAE,EAAE;oCACnC,8BAA8B;oCAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;oCAC9D,IAAI,IAAI,EAAE,CAAC;wCACT,WAAW,IAAI,IAAI,CAAC;oCACtB,CAAC;gCACH,CAAC;gCACD,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;6BAC5D,CAAC;4BAEF,oBAAoB;4BACpB,MAAM,eAAe,GAAG,WAAW,CAAC,uBAAuB,CAAC;gCAC1D,GAAG,EAAE,YAAY;gCACjB,GAAG,EAAE,aAAa,CAAC,MAAM;gCACzB,UAAU,EAAE,UAAU;gCACtB,YAAY,EAAE,EAAE;6BACjB,CAAC,CAAC;4BAEH,qCAAqC;4BACrC,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;gCACzC,MAAM,CAAC,sBAAsB,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;4BAC9E,CAAC;4BAED,yBAAyB;4BACzB,MAAM,eAAe,CAAC;4BAEtB,4GAA4G;4BAC5G,IAAI,CAAC,SAAS,IAAI,WAAW,EAAE,CAAC;gCAC9B,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,iDAAiD,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC;gCACtG,MAAM,cAAc,CAAC,WAAW,CAAC,CAAC;4BACpC,CAAC;wBAEH,CAAC;6BAAM,CAAC;4BACN,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,uCAAuC,CAAC,CAAC;wBACzE,CAAC;oBACH,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACf,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,+BAA+B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;wBACvH,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;oBAClD,CAAC;gBACH,CAAC,CAAC;gBAEF,uCAAuC;gBACvC,MAAM,CAAC,wBAAwB,CAAC,2BAA2B,EAAE,aAAa,CAAC,CAAC;gBAE5E,6BAA6B;gBAC7B,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;gBACvB,aAAa,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;gBACrC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,oCAAoC,CAAC,CAAC;gBAEnE,kCAAkC;gBAClC,GAAG,CAAC,WAAW,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;oBAC9C,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,IAAI,SAAS,mCAAmC,CAAC,CAAC;oBAClE,MAAM,CAAC,UAAU,EAAE,CAAC;oBACpB,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBAClC,CAAC,CAAC,CAAC;YAEL,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,IAAI,SAAS,sBAAsB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBAC9G,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;KACF;IACD,QAAQ,EAAE;QACR,YAAY,EAAE,QAAiB;QAC/B,QAAQ,EAAE,KAAK,EAAE,IAA2F,EAAE,EAAE;YAC9G,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;YACvC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAE9B,0CAA0C;YAC1C,MAAM,cAAc,GAAG,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAEnD,IAAI,cAAc,EAAE,CAAC;gBACnB,IAAI,CAAC;oBACH,MAAM,eAAK,CAAC,IAAI,CAAC,cAAc,EAAE;wBAC/B,OAAO,EAAE,MAAM;wBACf,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;qBACxB,EAAE;wBACD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;qBAChD,CAAC,CAAC;oBACH,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,CAAC;gBAC/B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,6BAA6B;gBAC/B,CAAC;YACH,CAAC;YAED,gDAAgD;YAChD,IAAI,MAAM,EAAE,UAAU,EAAE,CAAC;gBACvB,IAAI,CAAC;oBACH,MAAM,eAAK,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;wBAClC,OAAO,EAAE,MAAM;wBACf,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;qBACxB,EAAE;wBACD,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;qBAChD,CAAC,CAAC;oBACH,OAAO,EAAE,EAAE,EAAE,IAAa,EAAE,CAAC;gBAC/B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/F,CAAC;YACH,CAAC;YAED,OAAO,EAAE,EAAE,EAAE,KAAc,EAAE,KAAK,EAAE,2CAA2C,EAAE,CAAC;QACpF,CAAC;KACF;CACF,CAAC;AAIF,4CAA4C;AAC5C,MAAM,MAAM,GAAG;IACb,EAAE,EAAE,kBAAkB;IACtB,IAAI,EAAE,kBAAkB;IACxB,WAAW,EAAE,2CAA2C;IACxD,YAAY,EAAE;QACZ,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE,EAAE;KACf;IACD,QAAQ,CAAC,GAAsB;QAC7B,aAAa,GAAG,GAAG,CAAC;QACpB,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC,CAAC;IACzD,CAAC;CACF,CAAC;AAEF,kBAAe,MAAM,CAAC"}
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "moltbot-dingtalk-stream",
3
+ "version": "1.0.1",
4
+ "description": "DingTalk channel plugin for Clawdbot (Stream Mode)",
5
+ "main": "index.js",
6
+ "clawdbot": {
7
+ "extensions": [
8
+ "./index.js"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "channels": {
3
+ "dingtalk": {
4
+ "accounts": {
5
+ "local_test": {
6
+ "enabled": true,
7
+ "clientId": "YOUR_APP_KEY_HERE",
8
+ "clientSecret": "YOUR_APP_SECRET_HERE",
9
+ "webhookUrl": ""
10
+ }
11
+ }
12
+ }
13
+ },
14
+ "plugins": {
15
+ "entries": {
16
+ "dingtalk-channel": {
17
+ "path": "./dist/index.js"
18
+ }
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "id": "dingtalk-channel",
3
+ "name": "DingTalk Channel",
4
+ "version": "2.0.0",
5
+ "description": "Custom channel for integrating Moltbot with DingTalk (Stream Mode)",
6
+ "author": "Your Name",
7
+ "license": "MIT",
8
+ "extensions": {
9
+ "channels": [
10
+ "./dist/index.js"
11
+ ]
12
+ },
13
+ "configSchema": {
14
+ "type": "object",
15
+ "properties": {
16
+ "channels": {
17
+ "type": "object",
18
+ "properties": {
19
+ "dingtalk": {
20
+ "type": "object",
21
+ "properties": {
22
+ "accounts": {
23
+ "type": "object",
24
+ "additionalProperties": {
25
+ "type": "object",
26
+ "properties": {
27
+ "enabled": {
28
+ "type": "boolean",
29
+ "description": "Whether this account is enabled"
30
+ },
31
+ "clientId": {
32
+ "type": "string",
33
+ "description": "AppKey from DingTalk developer console"
34
+ },
35
+ "clientSecret": {
36
+ "type": "string",
37
+ "description": "AppSecret from DingTalk developer console"
38
+ },
39
+ "webhookUrl": {
40
+ "type": "string",
41
+ "description": "Optional webhook URL for proactive messages"
42
+ }
43
+ },
44
+ "required": [
45
+ "clientId",
46
+ "clientSecret"
47
+ ]
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "moltbot-dingtalk-stream",
3
+ "version": "1.0.1",
4
+ "description": "DingTalk custom channel plugin for Moltbot/Clawdbot",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "prepublishOnly": "npm run build"
11
+ },
12
+ "keywords": [
13
+ "moltbot",
14
+ "clawdbot",
15
+ "plugin",
16
+ "dingtalk",
17
+ "channel",
18
+ "chatbot"
19
+ ],
20
+ "author": "Your Name",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "axios": "^1.6.0",
24
+ "dingtalk-stream": "^2.1.4"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.0.0",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "clawdbot": {
31
+ "extensions": {
32
+ "channels": [
33
+ "./dist/index.js"
34
+ ]
35
+ }
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,373 @@
1
+ import { DWClient, DWClientDownStream } from 'dingtalk-stream';
2
+ import axios from 'axios';
3
+
4
+ // Define interfaces
5
+ interface ClawdbotPluginApi {
6
+ config: ClawdbotConfig;
7
+ logger: any;
8
+ runtime: any;
9
+ postMessage(params: any): Promise<void>;
10
+ registerChannel(opts: { plugin: any }): void;
11
+ registerService(service: any): void;
12
+ }
13
+
14
+ interface ClawdbotConfig {
15
+ channels?: {
16
+ dingtalk?: {
17
+ accounts?: {
18
+ [key: string]: DingTalkAccountConfig;
19
+ };
20
+ };
21
+ [key: string]: any;
22
+ };
23
+ }
24
+
25
+ interface DingTalkAccountConfig {
26
+ enabled?: boolean;
27
+ clientId: string;
28
+ clientSecret: string;
29
+ webhookUrl?: string;
30
+ name?: string;
31
+ }
32
+
33
+ interface ResolvedDingTalkAccount {
34
+ accountId: string;
35
+ name?: string;
36
+ enabled: boolean;
37
+ configured: boolean;
38
+ config: DingTalkAccountConfig;
39
+ }
40
+
41
+ interface DingTalkRobotMessage {
42
+ conversationId: string;
43
+ chatbotCorpId: string;
44
+ chatbotUserId: string;
45
+ msgId: string;
46
+ senderNick: string;
47
+ isAdmin: boolean;
48
+ senderStaffId?: string;
49
+ sessionWebhook: string;
50
+ sessionWebhookExpiredTime: number;
51
+ createAt: number;
52
+ senderCorpId?: string;
53
+ conversationType: '1' | '2';
54
+ senderId: string;
55
+ text?: {
56
+ content: string;
57
+ };
58
+ msgtype: string;
59
+ }
60
+
61
+ // Store plugin runtime
62
+ let pluginRuntime: ClawdbotPluginApi | null = null;
63
+
64
+ // Store session webhooks for reply
65
+ const sessionWebhooks: Map<string, string> = new Map();
66
+ // Store active clients for each account
67
+ const activeClients: Map<string, DWClient> = new Map();
68
+
69
+ // Helper functions
70
+ function listDingTalkAccountIds(cfg: ClawdbotConfig): string[] {
71
+ const accounts = cfg.channels?.dingtalk?.accounts;
72
+ return accounts ? Object.keys(accounts) : [];
73
+ }
74
+
75
+ function resolveDingTalkAccount(opts: { cfg: ClawdbotConfig; accountId?: string }): ResolvedDingTalkAccount {
76
+ const { cfg, accountId = 'default' } = opts;
77
+ const account = cfg.channels?.dingtalk?.accounts?.[accountId];
78
+ return {
79
+ accountId,
80
+ name: account?.name,
81
+ enabled: account?.enabled ?? false,
82
+ configured: Boolean(account?.clientId && account?.clientSecret),
83
+ config: account || { clientId: '', clientSecret: '' }
84
+ };
85
+ }
86
+
87
+ // DingTalk Channel Plugin
88
+ const dingTalkChannelPlugin = {
89
+ id: "dingtalk",
90
+ meta: {
91
+ id: "dingtalk",
92
+ label: "钉钉",
93
+ selectionLabel: "DingTalk Bot (Stream)",
94
+ docsPath: "/channels/dingtalk",
95
+ docsLabel: "dingtalk",
96
+ blurb: "钉钉机器人通道插件 (Stream模式)",
97
+ order: 100,
98
+ aliases: ["dt", "ding"],
99
+ },
100
+ capabilities: {
101
+ chatTypes: ["direct", "group"] as const,
102
+ },
103
+ reload: { configPrefixes: ["channels.dingtalk"] },
104
+ configSchema: {
105
+ type: "object" as const,
106
+ properties: {
107
+ channels: {
108
+ type: "object" as const,
109
+ properties: {
110
+ dingtalk: {
111
+ type: "object" as const,
112
+ properties: {
113
+ accounts: {
114
+ type: "object" as const,
115
+ additionalProperties: {
116
+ type: "object" as const,
117
+ properties: {
118
+ enabled: { type: "boolean" as const },
119
+ clientId: { type: "string" as const },
120
+ clientSecret: { type: "string" as const },
121
+ webhookUrl: { type: "string" as const },
122
+ name: { type: "string" as const },
123
+ },
124
+ required: ["clientId", "clientSecret"],
125
+ },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ },
132
+ },
133
+ config: {
134
+ listAccountIds: (cfg: ClawdbotConfig) => listDingTalkAccountIds(cfg),
135
+ resolveAccount: (cfg: ClawdbotConfig, accountId?: string) => resolveDingTalkAccount({ cfg, accountId }),
136
+ defaultAccountId: (_cfg: ClawdbotConfig) => 'default',
137
+ isConfigured: (account: ResolvedDingTalkAccount) => account.configured,
138
+ describeAccount: (account: ResolvedDingTalkAccount) => ({
139
+ accountId: account.accountId,
140
+ name: account.name,
141
+ enabled: account.enabled,
142
+ configured: account.configured,
143
+ }),
144
+ },
145
+ gateway: {
146
+ startAccount: async (ctx: any) => {
147
+ const account: ResolvedDingTalkAccount = ctx.account;
148
+ const config = account.config;
149
+ const accountId = account.accountId;
150
+
151
+ if (!config.clientId || !config.clientSecret) {
152
+ ctx.log?.warn?.(`[${accountId}] missing clientId or clientSecret`);
153
+ return;
154
+ }
155
+
156
+ ctx.log?.info?.(`[${accountId}] starting DingTalk Stream client`);
157
+
158
+ try {
159
+ const client = new DWClient({
160
+ clientId: config.clientId,
161
+ clientSecret: config.clientSecret,
162
+ });
163
+
164
+ // Helper to safely handle messages
165
+ const handleMessage = async (res: any) => {
166
+ try {
167
+ const message = JSON.parse(res.data);
168
+ const textContent = message.text?.content || "";
169
+ const senderId = message.senderId;
170
+ const convoId = message.conversationId;
171
+ const msgId = message.msgId;
172
+ // Store session webhook if provided (DingTalk Stream mode provides this for replies)
173
+ if (message.sessionWebhook) {
174
+ sessionWebhooks.set(convoId, message.sessionWebhook);
175
+ }
176
+
177
+ // Log reception
178
+ ctx.log?.info?.(`[${accountId}] received message from ${message.senderNick || senderId}: ${textContent}`);
179
+
180
+ // Filter out empty messages
181
+ if (!textContent) return;
182
+
183
+ // Simple text cleaning (remove @bot mentions if possible, though DingTalk usually gives clean content or we might need to parse entities)
184
+ const cleanedText = textContent.replace(/@\w+\s*/g, '').trim();
185
+
186
+ // Forward the message to Clawdbot for processing
187
+ if (pluginRuntime?.runtime?.channel?.reply) {
188
+ const replyModule = pluginRuntime.runtime.channel.reply;
189
+ const chatType = String(message.conversationType) === '2' ? 'group' : 'direct';
190
+ const fromAddress = chatType === 'group' ? `dingtalk:group:${convoId}` : `dingtalk:${senderId}`;
191
+
192
+ const ctxPayload = {
193
+ Body: cleanedText,
194
+ RawBody: textContent,
195
+ CommandBody: cleanedText,
196
+ From: fromAddress,
197
+ To: 'bot',
198
+ SessionKey: `dingtalk:${convoId}`,
199
+ AccountId: accountId,
200
+ ChatType: chatType,
201
+ SenderName: message.senderNick,
202
+ SenderId: senderId,
203
+ Provider: 'dingtalk',
204
+ Surface: 'dingtalk',
205
+ MessageSid: message.msgId,
206
+ Timestamp: message.createAt,
207
+ // Required for some logic
208
+ GroupSubject: chatType === 'group' ? (message.conversationId) : undefined,
209
+ };
210
+
211
+ const finalizedCtx = replyModule.finalizeInboundContext(ctxPayload);
212
+
213
+ let replyBuffer = "";
214
+ let replySent = false;
215
+
216
+ const sendToDingTalk = async (text: string) => {
217
+ if (!text) return;
218
+ if (replySent) {
219
+ ctx.log?.info?.(`[${accountId}] Reply already sent, skipping buffer flush.`);
220
+ return;
221
+ }
222
+
223
+ const replyWebhook = sessionWebhooks.get(convoId) || config.webhookUrl;
224
+ if (!replyWebhook) {
225
+ ctx.log?.error?.(`[${accountId}] No webhook to reply to ${convoId}`);
226
+ return;
227
+ }
228
+
229
+ try {
230
+ await axios.post(replyWebhook, {
231
+ msgtype: "text",
232
+ text: { content: text }
233
+ }, { headers: { 'Content-Type': 'application/json' } });
234
+ replySent = true;
235
+ ctx.log?.info?.(`[${accountId}] Reply sent successfully.`);
236
+ } catch (e) {
237
+ ctx.log?.error?.(`[${accountId}] Failed to send reply: ${e}`);
238
+ }
239
+ };
240
+
241
+ const dispatcher = {
242
+ sendFinalReply: (payload: any) => {
243
+ const text = payload.text || payload.content || '';
244
+ sendToDingTalk(text).catch(e => ctx.log?.error?.(`[${accountId}] sendToDingTalk failed: ${e}`));
245
+ return true;
246
+ },
247
+ typing: async () => { },
248
+ reaction: async () => { },
249
+ isSynchronous: () => false,
250
+ waitForIdle: async () => { },
251
+ sendBlockReply: async (block: any) => {
252
+ // Accumulate text from blocks
253
+ const text = block.text || block.delta || block.content || '';
254
+ if (text) {
255
+ replyBuffer += text;
256
+ }
257
+ },
258
+ getQueuedCounts: () => ({ active: 0, queued: 0, final: 0 })
259
+ };
260
+
261
+ // Internal dispatch
262
+ const dispatchPromise = replyModule.dispatchReplyFromConfig({
263
+ ctx: finalizedCtx,
264
+ cfg: pluginRuntime.config,
265
+ dispatcher: dispatcher,
266
+ replyOptions: {}
267
+ });
268
+
269
+ // ACK immediately to prevent retries
270
+ if (res.headers && res.headers.messageId) {
271
+ client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
272
+ }
273
+
274
+ // Wait for run to finish
275
+ await dispatchPromise;
276
+
277
+ // If final reply wasn't called but we have buffer (streaming case where agent didn't return final payload?)
278
+ if (!replySent && replyBuffer) {
279
+ ctx.log?.info?.(`[${accountId}] Sending accumulated buffer from blocks (len=${replyBuffer.length}).`);
280
+ await sendToDingTalk(replyBuffer);
281
+ }
282
+
283
+ } else {
284
+ ctx.log?.error?.(`[${accountId}] runtime.channel.reply not available`);
285
+ }
286
+ } catch (error) {
287
+ ctx.log?.error?.(`[${accountId}] error processing message: ${error instanceof Error ? error.message : String(error)}`);
288
+ console.error('DingTalk Handler Error:', error);
289
+ }
290
+ };
291
+
292
+ // Register callback for robot messages
293
+ client.registerCallbackListener('/v1.0/im/bot/messages/get', handleMessage);
294
+
295
+ // Connect to DingTalk Stream
296
+ await client.connect();
297
+ activeClients.set(accountId, client);
298
+ ctx.log?.info?.(`[${accountId}] DingTalk Stream client connected`);
299
+
300
+ // Handle abort signal for cleanup
301
+ ctx.abortSignal?.addEventListener('abort', () => {
302
+ ctx.log?.info?.(`[${accountId}] stopping DingTalk Stream client`);
303
+ client.disconnect();
304
+ activeClients.delete(accountId);
305
+ });
306
+
307
+ } catch (error) {
308
+ ctx.log?.error?.(`[${accountId}] failed to start: ${error instanceof Error ? error.message : String(error)}`);
309
+ throw error;
310
+ }
311
+ },
312
+ },
313
+ outbound: {
314
+ deliveryMode: "direct" as const,
315
+ sendText: async (opts: { text: string; account: ResolvedDingTalkAccount; target: string; senderId?: string }) => {
316
+ const { text, account, target } = opts;
317
+ const config = account.config;
318
+
319
+ // Try session webhook first (for replies)
320
+ const sessionWebhook = sessionWebhooks.get(target);
321
+
322
+ if (sessionWebhook) {
323
+ try {
324
+ await axios.post(sessionWebhook, {
325
+ msgtype: "text",
326
+ text: { content: text }
327
+ }, {
328
+ headers: { 'Content-Type': 'application/json' }
329
+ });
330
+ return { ok: true as const };
331
+ } catch (error) {
332
+ // Fall through to webhookUrl
333
+ }
334
+ }
335
+
336
+ // Fallback to webhookUrl for proactive messages
337
+ if (config?.webhookUrl) {
338
+ try {
339
+ await axios.post(config.webhookUrl, {
340
+ msgtype: "text",
341
+ text: { content: text }
342
+ }, {
343
+ headers: { 'Content-Type': 'application/json' }
344
+ });
345
+ return { ok: true as const };
346
+ } catch (error) {
347
+ return { ok: false as const, error: error instanceof Error ? error.message : String(error) };
348
+ }
349
+ }
350
+
351
+ return { ok: false as const, error: "No webhook available for sending messages" };
352
+ }
353
+ }
354
+ };
355
+
356
+
357
+
358
+ // Plugin object format required by Clawdbot
359
+ const plugin = {
360
+ id: "dingtalk-channel",
361
+ name: "DingTalk Channel",
362
+ description: "DingTalk channel plugin using Stream mode",
363
+ configSchema: {
364
+ type: "object" as const,
365
+ properties: {}
366
+ },
367
+ register(api: ClawdbotPluginApi) {
368
+ pluginRuntime = api;
369
+ api.registerChannel({ plugin: dingTalkChannelPlugin });
370
+ }
371
+ };
372
+
373
+ export default plugin;
@@ -0,0 +1,20 @@
1
+ {
2
+ "channels": {
3
+ "dingtalk": {
4
+ "accounts": {
5
+ "test": {
6
+ "enabled": true,
7
+ "clientId": "YOUR_APP_KEY",
8
+ "clientSecret": "YOUR_APP_SECRET"
9
+ }
10
+ }
11
+ }
12
+ },
13
+ "plugins": {
14
+ "entries": {
15
+ "dingtalk-channel": {
16
+ "path": "./dist/index.js"
17
+ }
18
+ }
19
+ }
20
+ }
package/test-local.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Script to test the DingTalk plugin with local Clawdbot
3
+ * This script will:
4
+ * 1. Validate the plugin structure
5
+ * 2. Create a test configuration
6
+ * 3. Provide instructions for testing
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { execSync } = require('child_process');
12
+
13
+ console.log('🧪 Testing DingTalk Plugin (Stream Mode)\n');
14
+
15
+ // Check if the plugin built successfully
16
+ const distDir = path.join(__dirname, 'dist');
17
+ if (!fs.existsSync(distDir)) {
18
+ console.error('❌ Dist directory does not exist. Please run `npm run build` first.');
19
+ process.exit(1);
20
+ }
21
+
22
+ const indexJs = path.join(distDir, 'index.js');
23
+ if (!fs.existsSync(indexJs)) {
24
+ console.error('❌ dist/index.js does not exist. Please run `npm run build` first.');
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log('✅ Plugin built successfully');
29
+
30
+ // Check if Clawdbot is available
31
+ try {
32
+ execSync('which clawdbot', { stdio: 'pipe' });
33
+ console.log('✅ Clawdbot/Moltbot is available');
34
+ } catch (error) {
35
+ console.log('⚠️ Clawdbot/Moltbot not found, but you can still install manually');
36
+ }
37
+
38
+ // Create a sample configuration for testing
39
+ const testConfig = {
40
+ channels: {
41
+ dingtalk: {
42
+ accounts: {
43
+ local_test: {
44
+ enabled: true,
45
+ clientId: "YOUR_APP_KEY_HERE",
46
+ clientSecret: "YOUR_APP_SECRET_HERE"
47
+ }
48
+ }
49
+ }
50
+ },
51
+ plugins: {
52
+ entries: {
53
+ "dingtalk-channel": {
54
+ path: path.resolve(__dirname, 'dist/index.js')
55
+ }
56
+ }
57
+ }
58
+ };
59
+
60
+ const configPath = path.join(__dirname, 'local-test-config.json');
61
+ fs.writeFileSync(configPath, JSON.stringify(testConfig, null, 2));
62
+ console.log(`✅ Created test configuration at ${configPath}`);
63
+
64
+ // Provide instructions
65
+ console.log('\n📋 Testing Instructions:\n');
66
+
67
+ console.log('1. Set up your DingTalk app:');
68
+ console.log(' - Go to https://open.dingtalk.com/');
69
+ console.log(' - Create an enterprise internal app');
70
+ console.log(' - Add Robot capability with Stream Mode enabled');
71
+ console.log(' - Get your AppKey (clientId) and AppSecret (clientSecret)\n');
72
+
73
+ console.log('2. Update the test config:');
74
+ console.log(` - Edit ${configPath}`);
75
+ console.log(' - Replace YOUR_APP_KEY_HERE with your AppKey');
76
+ console.log(' - Replace YOUR_APP_SECRET_HERE with your AppSecret\n');
77
+
78
+ console.log('3. Build and test:');
79
+ console.log(' npm run build');
80
+ console.log(` clawdbot --config ${configPath}\n`);
81
+
82
+ console.log('4. Test by sending a message to your robot in DingTalk.');
83
+ console.log(' For group chats, @mention the robot.\n');
84
+
85
+ console.log('💡 Tips:');
86
+ console.log('- No ngrok or public IP needed! Stream mode connects outbound.');
87
+ console.log('- Check Clawdbot logs at ~/.clawdbot/logs/ for debugging');
88
+ console.log('- Session webhooks for replies expire after ~2 hours');
89
+
90
+ console.log('\n🚀 You\'re ready to test your DingTalk Stream plugin!');
package/test-plugin.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Simple test script to validate the plugin structure
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // Check if necessary files exist
9
+ const requiredFiles = [
10
+ 'package.json',
11
+ 'moltbot.plugin.json',
12
+ 'tsconfig.json',
13
+ 'src/index.ts',
14
+ ];
15
+
16
+ console.log('🔍 Validating plugin structure...\n');
17
+
18
+ let allGood = true;
19
+
20
+ for (const file of requiredFiles) {
21
+ const filePath = path.join(__dirname, file);
22
+ if (fs.existsSync(filePath)) {
23
+ console.log(`✅ ${file}`);
24
+ } else {
25
+ console.log(`❌ ${file} - MISSING`);
26
+ allGood = false;
27
+ }
28
+ }
29
+
30
+ // Check if src directory exists and has content
31
+ const srcDir = path.join(__dirname, 'src');
32
+ if (fs.existsSync(srcDir)) {
33
+ const srcFiles = fs.readdirSync(srcDir);
34
+ console.log(`\n📁 Source files: ${srcFiles.length} files`);
35
+ srcFiles.forEach(file => console.log(` - ${file}`));
36
+ } else {
37
+ console.log('\n❌ src directory missing');
38
+ allGood = false;
39
+ }
40
+
41
+ console.log('\n' + '='.repeat(50));
42
+
43
+ if (allGood) {
44
+ console.log('🎉 Plugin structure looks good!');
45
+ console.log('\nNext steps:');
46
+ console.log('1. Run `npm install` to install dependencies');
47
+ console.log('2. Run `npm run build` to compile TypeScript');
48
+ console.log('3. Test the plugin with your local Clawdbot');
49
+ } else {
50
+ console.log('❌ Some files are missing. Please check the structure.');
51
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "removeComments": false,
16
+ "noImplicitAny": true,
17
+ "strictNullChecks": true,
18
+ "strictFunctionTypes": true,
19
+ "noImplicitThis": true,
20
+ "noImplicitReturns": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedIndexedAccess": true,
23
+ "noImplicitOverride": true,
24
+ "allowUnreachableCode": false,
25
+ "allowUnusedLabels": false
26
+ },
27
+ "include": [
28
+ "src/**/*"
29
+ ],
30
+ "exclude": [
31
+ "node_modules",
32
+ "dist"
33
+ ]
34
+ }