moltbot-dingtalk-stream 1.0.7 → 1.0.9

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/src/runtime.ts ADDED
@@ -0,0 +1,227 @@
1
+ import { DWClient } from "dingtalk-stream";
2
+ import axios from "axios";
3
+
4
+ // Types for Clawdbot core runtime (obtained from pluginRuntime)
5
+ export interface ClawdbotCoreRuntime {
6
+ channel: {
7
+ routing: {
8
+ resolveAgentRoute: (opts: {
9
+ cfg: unknown;
10
+ channel: string;
11
+ accountId: string;
12
+ peer: { kind: "direct" | "group"; id: string };
13
+ }) => { agentId: string; sessionKey: string; accountId: string };
14
+ };
15
+ reply: {
16
+ formatAgentEnvelope: (opts: {
17
+ channel: string;
18
+ from: string;
19
+ timestamp?: number;
20
+ previousTimestamp?: number | null;
21
+ envelope: unknown;
22
+ body: string;
23
+ }) => string;
24
+ resolveEnvelopeFormatOptions: (cfg: unknown) => unknown;
25
+ resolveHumanDelayConfig: (cfg: unknown, agentId?: string) => unknown;
26
+ finalizeInboundContext: (ctx: unknown) => unknown;
27
+ createReplyDispatcherWithTyping: (opts: {
28
+ responsePrefix?: string;
29
+ responsePrefixContextProvider?: () => Promise<string>;
30
+ humanDelay?: unknown;
31
+ deliver: (payload: { text?: string; content?: string; mediaUrls?: string[] }) => Promise<void>;
32
+ onError?: (err: unknown, info: { kind: string }) => void;
33
+ onReplyStart?: () => void;
34
+ onIdle?: () => void;
35
+ }) => {
36
+ dispatcher: unknown;
37
+ replyOptions: Record<string, unknown>;
38
+ markDispatchIdle: () => void;
39
+ };
40
+ dispatchReplyFromConfig: (opts: {
41
+ ctx: unknown;
42
+ cfg: unknown;
43
+ dispatcher: unknown;
44
+ replyOptions?: Record<string, unknown>;
45
+ }) => Promise<{ queuedFinal: boolean; counts: { final: number } }>;
46
+ dispatchReplyWithBufferedBlockDispatcher: (opts: {
47
+ ctx: unknown;
48
+ cfg: unknown;
49
+ dispatcherOptions: {
50
+ deliver: (payload: { text?: string; content?: string }) => Promise<void>;
51
+ onError?: (err: unknown, info: { kind: string }) => void;
52
+ };
53
+ }) => Promise<void>;
54
+ };
55
+ session: {
56
+ resolveStorePath: (
57
+ storeConfig: unknown,
58
+ opts: { agentId: string }
59
+ ) => string;
60
+ readSessionUpdatedAt?: (opts: { storePath: string; sessionKey: string }) => number | null;
61
+ recordInboundSession: (opts: {
62
+ storePath: string;
63
+ sessionKey: string;
64
+ ctx: unknown;
65
+ onRecordError: (err: unknown) => void;
66
+ }) => Promise<void>;
67
+ };
68
+ text: {
69
+ resolveMarkdownTableMode: (opts: {
70
+ cfg: unknown;
71
+ channel: string;
72
+ accountId: string;
73
+ }) => "off" | "plain" | "markdown" | "bullets" | "code";
74
+ chunkMarkdownTextWithMode: (text: string, limit: number, mode: string) => string[];
75
+ resolveChunkMode: (cfg: unknown, channel: string, accountId?: string) => string;
76
+ };
77
+ };
78
+ logging: {
79
+ shouldLogVerbose: () => boolean;
80
+ getChildLogger: (opts: { module: string }) => {
81
+ info: (msg: string) => void;
82
+ warn: (msg: string) => void;
83
+ error: (msg: string) => void;
84
+ debug?: (msg: string) => void;
85
+ };
86
+ };
87
+ }
88
+
89
+ // Types
90
+ export interface DingTalkRuntime {
91
+ channel: {
92
+ dingtalk: {
93
+ sendMessage: (
94
+ target: string,
95
+ text: string,
96
+ opts?: { accountId?: string; mediaUrl?: string }
97
+ ) => Promise<{ ok: boolean; error?: string }>;
98
+ probe: (
99
+ clientId: string,
100
+ clientSecret: string,
101
+ timeoutMs?: number
102
+ ) => Promise<{ ok: boolean; error?: string; bot?: { name?: string } }>;
103
+ getClient: (accountId: string) => DWClient | undefined;
104
+ setClient: (accountId: string, client: DWClient) => void;
105
+ removeClient: (accountId: string) => void;
106
+ setSessionWebhook: (conversationId: string, webhook: string) => void;
107
+ getSessionWebhook: (conversationId: string) => string | undefined;
108
+ };
109
+ };
110
+ logging: {
111
+ shouldLogVerbose: () => boolean;
112
+ };
113
+ }
114
+
115
+ // Storage
116
+ const activeClients = new Map<string, DWClient>();
117
+ const sessionWebhooks = new Map<string, string>();
118
+
119
+ // Runtime implementation
120
+ const dingtalkRuntime: DingTalkRuntime = {
121
+ channel: {
122
+ dingtalk: {
123
+ sendMessage: async (target, text, opts = {}) => {
124
+ // Try multiple formats to find webhook
125
+ // target can be: "dingtalk:user:xxx", "dingtalk:channel:xxx", or plain conversationId
126
+ const normalizedTarget = target
127
+ .replace(/^dingtalk:(user|channel|group):/, "")
128
+ .replace(/^dingtalk:/, "");
129
+
130
+ let webhook = sessionWebhooks.get(target)
131
+ || sessionWebhooks.get(normalizedTarget);
132
+
133
+ // Fallback: iterate all keys for partial match
134
+ if (!webhook) {
135
+ for (const [key, value] of sessionWebhooks.entries()) {
136
+ if (key.includes(normalizedTarget) || normalizedTarget.includes(key)) {
137
+ webhook = value;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+
143
+ if (!webhook) {
144
+ console.error(`[DingTalk] No webhook for target: ${target}, normalized: ${normalizedTarget}, available keys: ${Array.from(sessionWebhooks.keys()).join(", ")}`);
145
+ return { ok: false, error: `No webhook available for target: ${target}` };
146
+ }
147
+
148
+ try {
149
+ const payload: Record<string, unknown> = {
150
+ msgtype: "text",
151
+ text: { content: text },
152
+ };
153
+
154
+ // Use markdown format for media (DingTalk text messages don't support embedded images)
155
+ if (opts.mediaUrl) {
156
+ payload.msgtype = "markdown";
157
+ payload.markdown = {
158
+ title: "Message",
159
+ text: `${text}\n\n![image](${opts.mediaUrl})`,
160
+ };
161
+ delete payload.text;
162
+ }
163
+
164
+ console.log(`[DingTalk] Sending to webhook: ${webhook.substring(0, 50)}...`);
165
+ await axios.post(webhook, payload, {
166
+ headers: { "Content-Type": "application/json" },
167
+ timeout: 10000,
168
+ });
169
+ return { ok: true };
170
+ } catch (error) {
171
+ console.error(`[DingTalk] Send failed:`, error);
172
+ return {
173
+ ok: false,
174
+ error: error instanceof Error ? error.message : String(error),
175
+ };
176
+ }
177
+ },
178
+
179
+ probe: async (clientId, clientSecret, timeoutMs = 5000) => {
180
+ try {
181
+ // Verify credentials by fetching access_token from DingTalk API
182
+ const response = await axios.post(
183
+ "https://api.dingtalk.com/v1.0/oauth2/accessToken",
184
+ { appKey: clientId, appSecret: clientSecret },
185
+ {
186
+ headers: { "Content-Type": "application/json" },
187
+ timeout: timeoutMs,
188
+ }
189
+ );
190
+
191
+ if (response.data?.accessToken) {
192
+ return { ok: true, bot: { name: "DingTalk Bot" } };
193
+ }
194
+ return { ok: false, error: "Invalid credentials" };
195
+ } catch (error) {
196
+ return {
197
+ ok: false,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ };
200
+ }
201
+ },
202
+
203
+ getClient: (accountId) => activeClients.get(accountId),
204
+ setClient: (accountId, client) => activeClients.set(accountId, client),
205
+ removeClient: (accountId) => activeClients.delete(accountId),
206
+ setSessionWebhook: (conversationId, webhook) =>
207
+ sessionWebhooks.set(conversationId, webhook),
208
+ getSessionWebhook: (conversationId) => sessionWebhooks.get(conversationId),
209
+ },
210
+ },
211
+ logging: {
212
+ shouldLogVerbose: () => process.env.DEBUG === "true",
213
+ },
214
+ };
215
+
216
+ let runtimeInstance: DingTalkRuntime | null = null;
217
+
218
+ export function getDingTalkRuntime(): DingTalkRuntime {
219
+ if (!runtimeInstance) {
220
+ runtimeInstance = dingtalkRuntime;
221
+ }
222
+ return runtimeInstance;
223
+ }
224
+
225
+ export function setDingTalkRuntime(runtime: DingTalkRuntime): void {
226
+ runtimeInstance = runtime;
227
+ }
package/src/schema.ts ADDED
@@ -0,0 +1,316 @@
1
+ import type { z } from "zod";
2
+
3
+ // Channel ID
4
+ export const CHANNEL_ID = "moltbot-dingtalk-stream";
5
+ export const DEFAULT_ACCOUNT_ID = "default";
6
+
7
+ // DingTalk account configuration interface
8
+ export interface DingTalkAccountConfig {
9
+ enabled?: boolean;
10
+ clientId?: string;
11
+ clientSecret?: string;
12
+ webhookUrl?: string;
13
+ name?: string;
14
+ // Group settings
15
+ groupPolicy?: "open" | "allowlist";
16
+ requireMention?: boolean;
17
+ // DM settings
18
+ dm?: {
19
+ policy?: "open" | "pairing" | "allowlist";
20
+ allowFrom?: string[];
21
+ };
22
+ }
23
+
24
+ // Resolved account with computed fields
25
+ export interface ResolvedDingTalkAccount {
26
+ accountId: string;
27
+ name?: string;
28
+ enabled: boolean;
29
+ configured: boolean;
30
+ clientId: string;
31
+ clientSecret: string;
32
+ tokenSource: "config" | "env" | "none";
33
+ config: DingTalkAccountConfig;
34
+ }
35
+
36
+ // Channel configuration interface
37
+ export interface DingTalkChannelConfig {
38
+ enabled?: boolean;
39
+ clientId?: string;
40
+ clientSecret?: string;
41
+ webhookUrl?: string;
42
+ name?: string;
43
+ groupPolicy?: "open" | "allowlist";
44
+ requireMention?: boolean;
45
+ dm?: {
46
+ policy?: "open" | "pairing" | "allowlist";
47
+ allowFrom?: string[];
48
+ };
49
+ accounts?: Record<string, DingTalkAccountConfig>;
50
+ }
51
+
52
+ // Full config interface
53
+ export interface ClawdbotConfig {
54
+ channels?: {
55
+ [CHANNEL_ID]?: DingTalkChannelConfig;
56
+ defaults?: {
57
+ groupPolicy?: "open" | "allowlist";
58
+ };
59
+ [key: string]: unknown;
60
+ };
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ // Config schema for validation
65
+ export const DingTalkConfigSchema = {
66
+ type: "object" as const,
67
+ properties: {
68
+ enabled: { type: "boolean" as const },
69
+ clientId: { type: "string" as const },
70
+ clientSecret: { type: "string" as const },
71
+ webhookUrl: { type: "string" as const },
72
+ name: { type: "string" as const },
73
+ groupPolicy: { type: "string" as const, enum: ["open", "allowlist"] },
74
+ requireMention: { type: "boolean" as const },
75
+ dm: {
76
+ type: "object" as const,
77
+ properties: {
78
+ policy: { type: "string" as const, enum: ["open", "pairing", "allowlist"] },
79
+ allowFrom: { type: "array" as const, items: { type: "string" as const } },
80
+ },
81
+ },
82
+ accounts: {
83
+ type: "object" as const,
84
+ additionalProperties: {
85
+ type: "object" as const,
86
+ properties: {
87
+ enabled: { type: "boolean" as const },
88
+ clientId: { type: "string" as const },
89
+ clientSecret: { type: "string" as const },
90
+ webhookUrl: { type: "string" as const },
91
+ name: { type: "string" as const },
92
+ },
93
+ required: ["clientId", "clientSecret"],
94
+ },
95
+ },
96
+ },
97
+ };
98
+
99
+ // Helper functions
100
+ export function listDingTalkAccountIds(cfg: ClawdbotConfig): string[] {
101
+ const channelConfig = cfg.channels?.[CHANNEL_ID];
102
+ if (!channelConfig) return [];
103
+
104
+ const accountIds: string[] = [];
105
+
106
+ // Check for top-level (default) account
107
+ if (channelConfig.clientId && channelConfig.clientSecret) {
108
+ accountIds.push(DEFAULT_ACCOUNT_ID);
109
+ }
110
+
111
+ // Check for named accounts
112
+ const accounts = channelConfig.accounts;
113
+ if (accounts) {
114
+ for (const id of Object.keys(accounts)) {
115
+ if (id !== DEFAULT_ACCOUNT_ID || !accountIds.includes(DEFAULT_ACCOUNT_ID)) {
116
+ accountIds.push(id);
117
+ }
118
+ }
119
+ }
120
+
121
+ return accountIds;
122
+ }
123
+
124
+ export function resolveDingTalkAccount(opts: {
125
+ cfg: ClawdbotConfig;
126
+ accountId?: string;
127
+ }): ResolvedDingTalkAccount {
128
+ const { cfg, accountId = DEFAULT_ACCOUNT_ID } = opts;
129
+ const channelConfig = cfg.channels?.[CHANNEL_ID];
130
+
131
+ // Try to get account config
132
+ let accountConfig: DingTalkAccountConfig | undefined;
133
+ let tokenSource: "config" | "env" | "none" = "none";
134
+
135
+ if (accountId === DEFAULT_ACCOUNT_ID) {
136
+ // For default account, check top-level first, then accounts.default
137
+ if (channelConfig?.clientId && channelConfig?.clientSecret) {
138
+ accountConfig = {
139
+ enabled: channelConfig.enabled,
140
+ clientId: channelConfig.clientId,
141
+ clientSecret: channelConfig.clientSecret,
142
+ webhookUrl: channelConfig.webhookUrl,
143
+ name: channelConfig.name,
144
+ groupPolicy: channelConfig.groupPolicy,
145
+ requireMention: channelConfig.requireMention,
146
+ dm: channelConfig.dm,
147
+ };
148
+ tokenSource = "config";
149
+ } else if (channelConfig?.accounts?.[DEFAULT_ACCOUNT_ID]) {
150
+ accountConfig = channelConfig.accounts[DEFAULT_ACCOUNT_ID];
151
+ tokenSource = "config";
152
+ } else if (process.env.DINGTALK_CLIENT_ID && process.env.DINGTALK_CLIENT_SECRET) {
153
+ accountConfig = {
154
+ enabled: true,
155
+ clientId: process.env.DINGTALK_CLIENT_ID,
156
+ clientSecret: process.env.DINGTALK_CLIENT_SECRET,
157
+ webhookUrl: process.env.DINGTALK_WEBHOOK_URL,
158
+ };
159
+ tokenSource = "env";
160
+ }
161
+ } else {
162
+ // Named account
163
+ accountConfig = channelConfig?.accounts?.[accountId];
164
+ if (accountConfig) {
165
+ tokenSource = "config";
166
+ }
167
+ }
168
+
169
+ const config = accountConfig || { clientId: "", clientSecret: "" };
170
+
171
+ return {
172
+ accountId,
173
+ name: config.name,
174
+ enabled: config.enabled ?? true,
175
+ configured: Boolean(config.clientId?.trim() && config.clientSecret?.trim()),
176
+ clientId: config.clientId || "",
177
+ clientSecret: config.clientSecret || "",
178
+ tokenSource,
179
+ config,
180
+ };
181
+ }
182
+
183
+ export function resolveDefaultDingTalkAccountId(cfg: ClawdbotConfig): string {
184
+ const accountIds = listDingTalkAccountIds(cfg);
185
+ return accountIds.includes(DEFAULT_ACCOUNT_ID)
186
+ ? DEFAULT_ACCOUNT_ID
187
+ : accountIds[0] || DEFAULT_ACCOUNT_ID;
188
+ }
189
+
190
+ export function normalizeAccountId(accountId?: string): string {
191
+ if (!accountId || accountId === "default" || accountId === "") {
192
+ return DEFAULT_ACCOUNT_ID;
193
+ }
194
+ return accountId.toLowerCase().replace(/[^a-z0-9_-]/g, "_");
195
+ }
196
+
197
+ export function setAccountEnabledInConfig(opts: {
198
+ cfg: ClawdbotConfig;
199
+ accountId: string;
200
+ enabled: boolean;
201
+ }): ClawdbotConfig {
202
+ const { cfg, accountId, enabled } = opts;
203
+ const channelConfig = cfg.channels?.[CHANNEL_ID] || {};
204
+
205
+ if (accountId === DEFAULT_ACCOUNT_ID && channelConfig.clientId) {
206
+ // Top-level default account
207
+ return {
208
+ ...cfg,
209
+ channels: {
210
+ ...cfg.channels,
211
+ [CHANNEL_ID]: {
212
+ ...channelConfig,
213
+ enabled,
214
+ },
215
+ },
216
+ };
217
+ }
218
+
219
+ // Named account
220
+ return {
221
+ ...cfg,
222
+ channels: {
223
+ ...cfg.channels,
224
+ [CHANNEL_ID]: {
225
+ ...channelConfig,
226
+ accounts: {
227
+ ...channelConfig.accounts,
228
+ [accountId]: {
229
+ ...channelConfig.accounts?.[accountId],
230
+ enabled,
231
+ },
232
+ },
233
+ },
234
+ },
235
+ };
236
+ }
237
+
238
+ export function deleteAccountFromConfig(opts: {
239
+ cfg: ClawdbotConfig;
240
+ accountId: string;
241
+ }): ClawdbotConfig {
242
+ const { cfg, accountId } = opts;
243
+ const channelConfig = cfg.channels?.[CHANNEL_ID];
244
+
245
+ if (!channelConfig) return cfg;
246
+
247
+ if (accountId === DEFAULT_ACCOUNT_ID && channelConfig.clientId) {
248
+ // Remove top-level credentials
249
+ const { clientId, clientSecret, webhookUrl, name, ...rest } = channelConfig;
250
+ return {
251
+ ...cfg,
252
+ channels: {
253
+ ...cfg.channels,
254
+ [CHANNEL_ID]: rest,
255
+ },
256
+ };
257
+ }
258
+
259
+ // Remove named account
260
+ if (channelConfig.accounts?.[accountId]) {
261
+ const { [accountId]: removed, ...remainingAccounts } = channelConfig.accounts;
262
+ return {
263
+ ...cfg,
264
+ channels: {
265
+ ...cfg.channels,
266
+ [CHANNEL_ID]: {
267
+ ...channelConfig,
268
+ accounts: remainingAccounts,
269
+ },
270
+ },
271
+ };
272
+ }
273
+
274
+ return cfg;
275
+ }
276
+
277
+ export function applyAccountNameToConfig(opts: {
278
+ cfg: ClawdbotConfig;
279
+ accountId: string;
280
+ name?: string;
281
+ }): ClawdbotConfig {
282
+ const { cfg, accountId, name } = opts;
283
+ if (!name) return cfg;
284
+
285
+ const channelConfig = cfg.channels?.[CHANNEL_ID] || {};
286
+
287
+ if (accountId === DEFAULT_ACCOUNT_ID && channelConfig.clientId) {
288
+ return {
289
+ ...cfg,
290
+ channels: {
291
+ ...cfg.channels,
292
+ [CHANNEL_ID]: {
293
+ ...channelConfig,
294
+ name,
295
+ },
296
+ },
297
+ };
298
+ }
299
+
300
+ return {
301
+ ...cfg,
302
+ channels: {
303
+ ...cfg.channels,
304
+ [CHANNEL_ID]: {
305
+ ...channelConfig,
306
+ accounts: {
307
+ ...channelConfig.accounts,
308
+ [accountId]: {
309
+ ...channelConfig.accounts?.[accountId],
310
+ name,
311
+ },
312
+ },
313
+ },
314
+ },
315
+ };
316
+ }
@@ -1,59 +0,0 @@
1
- {
2
- "id": "moltbot-dingtalk-stream",
3
- "name": "DingTalk Stream Channel",
4
- "version": "1.0.7",
5
- "description": "Custom channel for integrating Moltbot with DingTalk (Stream Mode)",
6
- "author": "Jack",
7
- "license": "MIT",
8
- "extensions": {
9
- "channels": [
10
- "./index.js"
11
- ]
12
- },
13
- "channels": [
14
- "moltbot-dingtalk-stream"
15
- ],
16
- "configSchema": {
17
- "type": "object",
18
- "properties": {
19
- "channels": {
20
- "type": "object",
21
- "properties": {
22
- "moltbot-dingtalk-stream": {
23
- "type": "object",
24
- "properties": {
25
- "accounts": {
26
- "type": "object",
27
- "additionalProperties": {
28
- "type": "object",
29
- "properties": {
30
- "enabled": {
31
- "type": "boolean",
32
- "description": "Whether this account is enabled"
33
- },
34
- "clientId": {
35
- "type": "string",
36
- "description": "AppKey from DingTalk developer console"
37
- },
38
- "clientSecret": {
39
- "type": "string",
40
- "description": "AppSecret from DingTalk developer console"
41
- },
42
- "webhookUrl": {
43
- "type": "string",
44
- "description": "Optional webhook URL for proactive messages"
45
- }
46
- },
47
- "required": [
48
- "clientId",
49
- "clientSecret"
50
- ]
51
- }
52
- }
53
- }
54
- }
55
- }
56
- }
57
- }
58
- }
59
- }
package/dist/package.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "name": "moltbot-dingtalk-stream",
3
- "version": "1.0.7",
4
- "description": "DingTalk channel plugin for Clawdbot (Stream Mode)",
5
- "main": "index.js",
6
- "clawdbot": {
7
- "extensions": [
8
- "./index.js"
9
- ]
10
- }
11
- }