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/CHANGELOG.md +20 -0
- package/README.md +114 -41
- package/clawdbot.plugin.json +1 -1
- package/dist/index.d.ts +162 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +347 -249
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +142 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +106 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +137 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +235 -0
- package/dist/schema.js.map +1 -0
- package/package.json +4 -3
- package/src/index.ts +574 -305
- package/src/runtime.ts +227 -0
- package/src/schema.ts +316 -0
- package/dist/clawdbot.plugin.json +0 -59
- package/dist/package.json +0 -11
package/src/index.ts
CHANGED
|
@@ -1,373 +1,642 @@
|
|
|
1
|
-
import { DWClient
|
|
2
|
-
import
|
|
1
|
+
import { DWClient } from "dingtalk-stream";
|
|
2
|
+
import { getDingTalkRuntime, type ClawdbotCoreRuntime } from "./runtime.js";
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_ID,
|
|
5
|
+
DEFAULT_ACCOUNT_ID,
|
|
6
|
+
DingTalkConfigSchema,
|
|
7
|
+
listDingTalkAccountIds,
|
|
8
|
+
resolveDingTalkAccount,
|
|
9
|
+
resolveDefaultDingTalkAccountId,
|
|
10
|
+
normalizeAccountId,
|
|
11
|
+
setAccountEnabledInConfig,
|
|
12
|
+
deleteAccountFromConfig,
|
|
13
|
+
applyAccountNameToConfig,
|
|
14
|
+
type ClawdbotConfig,
|
|
15
|
+
type ResolvedDingTalkAccount,
|
|
16
|
+
} from "./schema.js";
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Plugin API Types
|
|
20
|
+
// ============================================================================
|
|
3
21
|
|
|
4
|
-
// Define interfaces
|
|
5
22
|
interface ClawdbotPluginApi {
|
|
6
23
|
config: ClawdbotConfig;
|
|
7
|
-
logger:
|
|
8
|
-
runtime:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
registerService(service: any): void;
|
|
24
|
+
logger: Console;
|
|
25
|
+
runtime: ClawdbotCoreRuntime;
|
|
26
|
+
registerChannel(opts: { plugin: ChannelPlugin }): void;
|
|
27
|
+
registerService?(service: unknown): void;
|
|
12
28
|
}
|
|
13
29
|
|
|
14
|
-
interface
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
interface InboundContext {
|
|
31
|
+
Body: string;
|
|
32
|
+
RawBody: string;
|
|
33
|
+
CommandBody: string;
|
|
34
|
+
From: string;
|
|
35
|
+
To: string;
|
|
36
|
+
SessionKey: string;
|
|
37
|
+
AccountId: string;
|
|
38
|
+
ChatType: "direct" | "group";
|
|
39
|
+
SenderName?: string;
|
|
40
|
+
SenderId: string;
|
|
41
|
+
SenderUsername?: string;
|
|
42
|
+
Provider: string;
|
|
43
|
+
Surface: string;
|
|
44
|
+
MessageSid: string;
|
|
45
|
+
Timestamp: number;
|
|
46
|
+
GroupSubject?: string;
|
|
47
|
+
ConversationLabel?: string;
|
|
48
|
+
OriginatingChannel?: string;
|
|
49
|
+
OriginatingTo?: string;
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
interface
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
interface Dispatcher {
|
|
53
|
+
sendFinalReply: (payload: { text?: string; content?: string }) => boolean;
|
|
54
|
+
typing: () => Promise<void>;
|
|
55
|
+
reaction: () => Promise<void>;
|
|
56
|
+
isSynchronous: () => boolean;
|
|
57
|
+
waitForIdle: () => Promise<void>;
|
|
58
|
+
sendBlockReply: (block: { text?: string; delta?: string; content?: string }) => Promise<void>;
|
|
59
|
+
getQueuedCounts: () => { active: number; queued: number; final: number };
|
|
31
60
|
}
|
|
32
61
|
|
|
33
|
-
interface
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
config: DingTalkAccountConfig;
|
|
62
|
+
interface DispatchOptions {
|
|
63
|
+
ctx: InboundContext;
|
|
64
|
+
cfg: ClawdbotConfig;
|
|
65
|
+
dispatcher: Dispatcher;
|
|
66
|
+
replyOptions: Record<string, unknown>;
|
|
39
67
|
}
|
|
40
68
|
|
|
41
|
-
interface
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
createAt: number;
|
|
52
|
-
senderCorpId?: string;
|
|
53
|
-
conversationType: '1' | '2';
|
|
54
|
-
senderId: string;
|
|
55
|
-
text?: {
|
|
56
|
-
content: string;
|
|
69
|
+
interface GatewayContext {
|
|
70
|
+
account: ResolvedDingTalkAccount;
|
|
71
|
+
cfg: ClawdbotConfig;
|
|
72
|
+
runtime: ClawdbotCoreRuntime;
|
|
73
|
+
abortSignal?: AbortSignal;
|
|
74
|
+
log?: {
|
|
75
|
+
info: (msg: string) => void;
|
|
76
|
+
warn: (msg: string) => void;
|
|
77
|
+
error: (msg: string) => void;
|
|
78
|
+
debug?: (msg: string) => void;
|
|
57
79
|
};
|
|
58
|
-
|
|
80
|
+
setStatus?: (status: Record<string, unknown>) => void;
|
|
81
|
+
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
interface ChannelPlugin {
|
|
85
|
+
id: string;
|
|
86
|
+
meta: {
|
|
87
|
+
id: string;
|
|
88
|
+
label: string;
|
|
89
|
+
selectionLabel: string;
|
|
90
|
+
docsPath: string;
|
|
91
|
+
docsLabel: string;
|
|
92
|
+
blurb: string;
|
|
93
|
+
order: number;
|
|
94
|
+
aliases: string[];
|
|
95
|
+
};
|
|
96
|
+
capabilities: {
|
|
97
|
+
chatTypes: readonly string[];
|
|
98
|
+
media?: boolean;
|
|
99
|
+
threads?: boolean;
|
|
100
|
+
};
|
|
101
|
+
reload: { configPrefixes: string[] };
|
|
102
|
+
configSchema: typeof DingTalkConfigSchema;
|
|
103
|
+
config: {
|
|
104
|
+
listAccountIds: (cfg: ClawdbotConfig) => string[];
|
|
105
|
+
resolveAccount: (cfg: ClawdbotConfig, accountId?: string) => ResolvedDingTalkAccount;
|
|
106
|
+
defaultAccountId: (cfg: ClawdbotConfig) => string;
|
|
107
|
+
setAccountEnabled: (opts: { cfg: ClawdbotConfig; accountId: string; enabled: boolean }) => ClawdbotConfig;
|
|
108
|
+
deleteAccount: (opts: { cfg: ClawdbotConfig; accountId: string }) => ClawdbotConfig;
|
|
109
|
+
isConfigured: (account: ResolvedDingTalkAccount) => boolean;
|
|
110
|
+
describeAccount: (account: ResolvedDingTalkAccount) => Record<string, unknown>;
|
|
111
|
+
};
|
|
112
|
+
security?: {
|
|
113
|
+
resolveDmPolicy: (opts: {
|
|
114
|
+
cfg: ClawdbotConfig;
|
|
115
|
+
accountId?: string;
|
|
116
|
+
account: ResolvedDingTalkAccount;
|
|
117
|
+
}) => {
|
|
118
|
+
policy: string;
|
|
119
|
+
allowFrom: string[];
|
|
120
|
+
allowFromPath: string;
|
|
121
|
+
normalizeEntry: (raw: string) => string;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
mentions?: {
|
|
125
|
+
stripPatterns: () => string[];
|
|
126
|
+
};
|
|
127
|
+
groups?: {
|
|
128
|
+
resolveRequireMention: (opts: { cfg: ClawdbotConfig; accountId?: string }) => boolean;
|
|
129
|
+
};
|
|
130
|
+
messaging?: {
|
|
131
|
+
normalizeTarget: (target: string) => string;
|
|
132
|
+
targetResolver?: {
|
|
133
|
+
looksLikeId: (id: string) => boolean;
|
|
134
|
+
hint: string;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
setup?: {
|
|
138
|
+
resolveAccountId: (opts: { accountId?: string }) => string;
|
|
139
|
+
applyAccountName: (opts: { cfg: ClawdbotConfig; accountId: string; name?: string }) => ClawdbotConfig;
|
|
140
|
+
validateInput: (opts: { accountId: string; input: SetupInput }) => string | null;
|
|
141
|
+
applyAccountConfig: (opts: { cfg: ClawdbotConfig; accountId: string; input: SetupInput }) => ClawdbotConfig;
|
|
142
|
+
};
|
|
143
|
+
outbound: {
|
|
144
|
+
deliveryMode: "direct";
|
|
145
|
+
textChunkLimit?: number;
|
|
146
|
+
sendText: (opts: {
|
|
147
|
+
to: string;
|
|
148
|
+
text: string;
|
|
149
|
+
accountId?: string;
|
|
150
|
+
deps?: Record<string, unknown>;
|
|
151
|
+
replyToId?: string;
|
|
152
|
+
}) => Promise<{ channel: string; ok: boolean; error?: string }>;
|
|
153
|
+
sendMedia?: (opts: {
|
|
154
|
+
to: string;
|
|
155
|
+
text: string;
|
|
156
|
+
mediaUrl: string;
|
|
157
|
+
accountId?: string;
|
|
158
|
+
}) => Promise<{ channel: string; ok: boolean; error?: string }>;
|
|
159
|
+
};
|
|
160
|
+
status?: {
|
|
161
|
+
defaultRuntime: {
|
|
162
|
+
accountId: string;
|
|
163
|
+
running: boolean;
|
|
164
|
+
lastStartAt: null;
|
|
165
|
+
lastStopAt: null;
|
|
166
|
+
lastError: null;
|
|
167
|
+
};
|
|
168
|
+
probeAccount: (opts: { account: ResolvedDingTalkAccount; timeoutMs?: number }) => Promise<{
|
|
169
|
+
ok: boolean;
|
|
170
|
+
error?: string;
|
|
171
|
+
bot?: { name?: string };
|
|
172
|
+
}>;
|
|
173
|
+
buildAccountSnapshot: (opts: {
|
|
174
|
+
account: ResolvedDingTalkAccount;
|
|
175
|
+
runtime?: Record<string, unknown>;
|
|
176
|
+
probe?: Record<string, unknown>;
|
|
177
|
+
}) => Record<string, unknown>;
|
|
178
|
+
};
|
|
179
|
+
gateway: {
|
|
180
|
+
startAccount: (ctx: GatewayContext) => Promise<void>;
|
|
181
|
+
};
|
|
73
182
|
}
|
|
74
183
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
name: account?.name,
|
|
81
|
-
enabled: account?.enabled ?? false,
|
|
82
|
-
configured: Boolean(account?.clientId && account?.clientSecret),
|
|
83
|
-
config: account || { clientId: '', clientSecret: '' }
|
|
84
|
-
};
|
|
184
|
+
interface SetupInput {
|
|
185
|
+
name?: string;
|
|
186
|
+
clientId?: string;
|
|
187
|
+
clientSecret?: string;
|
|
188
|
+
useEnv?: boolean;
|
|
85
189
|
}
|
|
86
190
|
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// Channel Meta
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
const meta = {
|
|
196
|
+
id: CHANNEL_ID,
|
|
197
|
+
label: "DingTalk",
|
|
198
|
+
selectionLabel: "DingTalk Bot (Stream)",
|
|
199
|
+
docsPath: "/channels/dingtalk",
|
|
200
|
+
docsLabel: "dingtalk",
|
|
201
|
+
blurb: "DingTalk bot channel plugin (Stream mode)",
|
|
202
|
+
order: 100,
|
|
203
|
+
aliases: ["dt", "ding", "dingtalk"],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Store plugin runtime reference
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
let pluginRuntime: ClawdbotPluginApi | null = null;
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
87
213
|
// DingTalk Channel Plugin
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
docsPath: "/channels/moltbot-dingtalk-stream",
|
|
95
|
-
docsLabel: "dingtalk",
|
|
96
|
-
blurb: "钉钉机器人通道插件 (Stream模式)",
|
|
97
|
-
order: 100,
|
|
98
|
-
aliases: ["dt", "ding"],
|
|
99
|
-
},
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
export const dingtalkPlugin: ChannelPlugin = {
|
|
217
|
+
id: CHANNEL_ID,
|
|
218
|
+
meta,
|
|
219
|
+
|
|
100
220
|
capabilities: {
|
|
101
|
-
chatTypes: ["direct", "group"]
|
|
221
|
+
chatTypes: ["direct", "group"],
|
|
222
|
+
media: true,
|
|
223
|
+
threads: false,
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
|
|
227
|
+
|
|
228
|
+
configSchema: DingTalkConfigSchema,
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// Config Management
|
|
232
|
+
// ============================================================================
|
|
233
|
+
config: {
|
|
234
|
+
listAccountIds: (cfg) => listDingTalkAccountIds(cfg),
|
|
235
|
+
resolveAccount: (cfg, accountId) => resolveDingTalkAccount({ cfg, accountId }),
|
|
236
|
+
defaultAccountId: (cfg) => resolveDefaultDingTalkAccountId(cfg),
|
|
237
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
238
|
+
setAccountEnabledInConfig({ cfg, accountId, enabled }),
|
|
239
|
+
deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfig({ cfg, accountId }),
|
|
240
|
+
isConfigured: (account) => account.configured,
|
|
241
|
+
describeAccount: (account) => ({
|
|
242
|
+
accountId: account.accountId,
|
|
243
|
+
name: account.name,
|
|
244
|
+
enabled: account.enabled,
|
|
245
|
+
configured: account.configured,
|
|
246
|
+
tokenSource: account.tokenSource,
|
|
247
|
+
}),
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// Security (DM Policy)
|
|
252
|
+
// ============================================================================
|
|
253
|
+
security: {
|
|
254
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
255
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
256
|
+
const channelConfig = cfg.channels?.[CHANNEL_ID];
|
|
257
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
258
|
+
const allowFromPath = useAccountPath
|
|
259
|
+
? `channels.${CHANNEL_ID}.accounts.${resolvedAccountId}.dm.`
|
|
260
|
+
: `channels.${CHANNEL_ID}.dm.`;
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
policy: account.config.dm?.policy ?? "open",
|
|
264
|
+
allowFrom: account.config.dm?.allowFrom ?? [],
|
|
265
|
+
allowFromPath,
|
|
266
|
+
normalizeEntry: (raw) => raw.replace(/^dingtalk:/i, ""),
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Mentions
|
|
273
|
+
// ============================================================================
|
|
274
|
+
mentions: {
|
|
275
|
+
stripPatterns: () => ["@\\S+\\s*"],
|
|
102
276
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Groups
|
|
280
|
+
// ============================================================================
|
|
281
|
+
groups: {
|
|
282
|
+
resolveRequireMention: ({ cfg, accountId }) => {
|
|
283
|
+
const account = resolveDingTalkAccount({ cfg, accountId });
|
|
284
|
+
return account.config.requireMention ?? true;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Messaging
|
|
290
|
+
// ============================================================================
|
|
291
|
+
messaging: {
|
|
292
|
+
normalizeTarget: (target) => {
|
|
293
|
+
if (target.startsWith("dingtalk:")) return target;
|
|
294
|
+
if (target.startsWith("group:")) return `dingtalk:${target}`;
|
|
295
|
+
if (target.startsWith("user:")) return `dingtalk:${target}`;
|
|
296
|
+
return `dingtalk:${target}`;
|
|
297
|
+
},
|
|
298
|
+
targetResolver: {
|
|
299
|
+
looksLikeId: (id) => /^[a-zA-Z0-9_-]+$/.test(id),
|
|
300
|
+
hint: "<conversationId|user:ID>",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
// ============================================================================
|
|
305
|
+
// Setup (Account Configuration)
|
|
306
|
+
// ============================================================================
|
|
307
|
+
setup: {
|
|
308
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
309
|
+
|
|
310
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
311
|
+
applyAccountNameToConfig({ cfg, accountId, name }),
|
|
312
|
+
|
|
313
|
+
validateInput: ({ accountId, input }) => {
|
|
314
|
+
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
315
|
+
return "Environment variables can only be used for the default account";
|
|
316
|
+
}
|
|
317
|
+
if (!input.useEnv && (!input.clientId || !input.clientSecret)) {
|
|
318
|
+
return "DingTalk requires clientId and clientSecret";
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
324
|
+
const namedConfig = applyAccountNameToConfig({
|
|
325
|
+
cfg,
|
|
326
|
+
accountId,
|
|
327
|
+
name: input.name,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
331
|
+
return {
|
|
332
|
+
...namedConfig,
|
|
333
|
+
channels: {
|
|
334
|
+
...namedConfig.channels,
|
|
335
|
+
[CHANNEL_ID]: {
|
|
336
|
+
...namedConfig.channels?.[CHANNEL_ID],
|
|
337
|
+
enabled: true,
|
|
338
|
+
...(input.useEnv ? {} : { clientId: input.clientId, clientSecret: input.clientSecret }),
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
...namedConfig,
|
|
346
|
+
channels: {
|
|
347
|
+
...namedConfig.channels,
|
|
348
|
+
[CHANNEL_ID]: {
|
|
349
|
+
...namedConfig.channels?.[CHANNEL_ID],
|
|
350
|
+
enabled: true,
|
|
351
|
+
accounts: {
|
|
352
|
+
...namedConfig.channels?.[CHANNEL_ID]?.accounts,
|
|
353
|
+
[accountId]: {
|
|
354
|
+
...namedConfig.channels?.[CHANNEL_ID]?.accounts?.[accountId],
|
|
355
|
+
enabled: true,
|
|
356
|
+
clientId: input.clientId,
|
|
357
|
+
clientSecret: input.clientSecret,
|
|
126
358
|
},
|
|
127
359
|
},
|
|
128
360
|
},
|
|
129
361
|
},
|
|
130
|
-
}
|
|
362
|
+
};
|
|
131
363
|
},
|
|
132
364
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// Outbound (Send Messages)
|
|
368
|
+
// ============================================================================
|
|
369
|
+
outbound: {
|
|
370
|
+
deliveryMode: "direct",
|
|
371
|
+
textChunkLimit: 2000,
|
|
372
|
+
|
|
373
|
+
sendText: async ({ to, text, accountId }) => {
|
|
374
|
+
const result = await getDingTalkRuntime().channel.dingtalk.sendMessage(to, text, {
|
|
375
|
+
accountId,
|
|
376
|
+
});
|
|
377
|
+
return { channel: CHANNEL_ID, ...result };
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
381
|
+
const result = await getDingTalkRuntime().channel.dingtalk.sendMessage(to, text, {
|
|
382
|
+
accountId,
|
|
383
|
+
mediaUrl,
|
|
384
|
+
});
|
|
385
|
+
return { channel: CHANNEL_ID, ...result };
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Status (Probe & Monitoring)
|
|
391
|
+
// ============================================================================
|
|
392
|
+
status: {
|
|
393
|
+
defaultRuntime: {
|
|
394
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
395
|
+
running: false,
|
|
396
|
+
lastStartAt: null,
|
|
397
|
+
lastStopAt: null,
|
|
398
|
+
lastError: null,
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
probeAccount: async ({ account, timeoutMs }) => {
|
|
402
|
+
if (!account.clientId || !account.clientSecret) {
|
|
403
|
+
return { ok: false, error: "Missing clientId or clientSecret" };
|
|
404
|
+
}
|
|
405
|
+
return getDingTalkRuntime().channel.dingtalk.probe(
|
|
406
|
+
account.clientId,
|
|
407
|
+
account.clientSecret,
|
|
408
|
+
timeoutMs
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
139
413
|
accountId: account.accountId,
|
|
140
414
|
name: account.name,
|
|
141
415
|
enabled: account.enabled,
|
|
142
416
|
configured: account.configured,
|
|
417
|
+
tokenSource: account.tokenSource,
|
|
418
|
+
running: (runtime as Record<string, unknown>)?.running ?? false,
|
|
419
|
+
lastStartAt: (runtime as Record<string, unknown>)?.lastStartAt ?? null,
|
|
420
|
+
lastStopAt: (runtime as Record<string, unknown>)?.lastStopAt ?? null,
|
|
421
|
+
lastError: (runtime as Record<string, unknown>)?.lastError ?? null,
|
|
422
|
+
probe,
|
|
143
423
|
}),
|
|
144
424
|
},
|
|
425
|
+
|
|
426
|
+
// ============================================================================
|
|
427
|
+
// Gateway (Start/Stop Bot)
|
|
428
|
+
// ============================================================================
|
|
145
429
|
gateway: {
|
|
146
|
-
startAccount: async (ctx
|
|
147
|
-
const account
|
|
148
|
-
const config = account.config;
|
|
430
|
+
startAccount: async (ctx) => {
|
|
431
|
+
const { account, cfg, abortSignal, log, statusSink } = ctx;
|
|
149
432
|
const accountId = account.accountId;
|
|
433
|
+
const core = pluginRuntime?.runtime;
|
|
150
434
|
|
|
151
|
-
if (!
|
|
152
|
-
|
|
435
|
+
if (!account.clientId || !account.clientSecret) {
|
|
436
|
+
log?.warn?.(`[${accountId}] Missing clientId or clientSecret`);
|
|
153
437
|
return;
|
|
154
438
|
}
|
|
155
439
|
|
|
156
|
-
|
|
440
|
+
if (!core?.channel?.reply) {
|
|
441
|
+
log?.error?.(`[${accountId}] runtime.channel.reply not available`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
157
444
|
|
|
445
|
+
log?.info?.(`[${accountId}] Starting DingTalk Stream client`);
|
|
446
|
+
|
|
447
|
+
// Probe 检测凭据
|
|
158
448
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (message.sessionWebhook) {
|
|
174
|
-
sessionWebhooks.set(convoId, message.sessionWebhook);
|
|
175
|
-
}
|
|
449
|
+
const probe = await getDingTalkRuntime().channel.dingtalk.probe(
|
|
450
|
+
account.clientId,
|
|
451
|
+
account.clientSecret,
|
|
452
|
+
2500
|
|
453
|
+
);
|
|
454
|
+
if (probe.ok) {
|
|
455
|
+
log?.info?.(`[${accountId}] Credentials verified successfully`);
|
|
456
|
+
ctx.setStatus?.({ accountId, probe });
|
|
457
|
+
} else {
|
|
458
|
+
log?.warn?.(`[${accountId}] Credential verification failed: ${probe.error}`);
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
log?.debug?.(`[${accountId}] Probe failed: ${String(err)}`);
|
|
462
|
+
}
|
|
176
463
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
});
|
|
464
|
+
const client = new DWClient({
|
|
465
|
+
clientId: account.clientId,
|
|
466
|
+
clientSecret: account.clientSecret,
|
|
467
|
+
});
|
|
268
468
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
469
|
+
const handleMessage = async (res: { data: string; headers?: { messageId?: string } }) => {
|
|
470
|
+
try {
|
|
471
|
+
const message = JSON.parse(res.data);
|
|
472
|
+
const textContent = message.text?.content || "";
|
|
473
|
+
const senderId = message.senderId;
|
|
474
|
+
const convoId = message.conversationId;
|
|
273
475
|
|
|
274
|
-
|
|
275
|
-
await dispatchPromise;
|
|
476
|
+
log?.info?.(`[${accountId}] Received message from ${message.senderNick || senderId}: ${textContent}`);
|
|
276
477
|
|
|
277
|
-
|
|
278
|
-
if (!replySent && replyBuffer) {
|
|
279
|
-
ctx.log?.info?.(`[${accountId}] Sending accumulated buffer from blocks (len=${replyBuffer.length}).`);
|
|
280
|
-
await sendToDingTalk(replyBuffer);
|
|
281
|
-
}
|
|
478
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
282
479
|
|
|
283
|
-
|
|
284
|
-
|
|
480
|
+
if (!textContent) return;
|
|
481
|
+
|
|
482
|
+
const rawBody = textContent;
|
|
483
|
+
const cleanedText = textContent.replace(/@\S+\s*/g, "").trim();
|
|
484
|
+
|
|
485
|
+
const chatType = String(message.conversationType) === "2" ? "group" : "direct";
|
|
486
|
+
|
|
487
|
+
// Store session webhook with multiple keys for flexible lookup
|
|
488
|
+
if (message.sessionWebhook) {
|
|
489
|
+
getDingTalkRuntime().channel.dingtalk.setSessionWebhook(convoId, message.sessionWebhook);
|
|
490
|
+
if (chatType === "direct" && senderId) {
|
|
491
|
+
getDingTalkRuntime().channel.dingtalk.setSessionWebhook(senderId, message.sessionWebhook);
|
|
492
|
+
getDingTalkRuntime().channel.dingtalk.setSessionWebhook(`dingtalk:user:${senderId}`, message.sessionWebhook);
|
|
493
|
+
}
|
|
494
|
+
if (chatType === "group" && convoId) {
|
|
495
|
+
getDingTalkRuntime().channel.dingtalk.setSessionWebhook(`dingtalk:channel:${convoId}`, message.sessionWebhook);
|
|
285
496
|
}
|
|
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
497
|
}
|
|
290
|
-
};
|
|
291
498
|
|
|
292
|
-
|
|
293
|
-
|
|
499
|
+
const route = core.channel.routing?.resolveAgentRoute?.({
|
|
500
|
+
cfg,
|
|
501
|
+
channel: CHANNEL_ID,
|
|
502
|
+
accountId,
|
|
503
|
+
peer: {
|
|
504
|
+
kind: chatType === "group" ? "group" : "direct",
|
|
505
|
+
id: chatType === "group" ? convoId : senderId,
|
|
506
|
+
},
|
|
507
|
+
}) ?? { agentId: "main", sessionKey: `dingtalk:${convoId}`, accountId };
|
|
508
|
+
|
|
509
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions?.(cfg) ?? {};
|
|
510
|
+
const body = core.channel.reply.formatAgentEnvelope?.({
|
|
511
|
+
channel: "DingTalk",
|
|
512
|
+
from: message.senderNick ?? message.senderId,
|
|
513
|
+
timestamp: message.createAt,
|
|
514
|
+
envelope: envelopeOptions,
|
|
515
|
+
body: cleanedText,
|
|
516
|
+
}) ?? cleanedText;
|
|
517
|
+
|
|
518
|
+
const ctxPayload: InboundContext = {
|
|
519
|
+
Body: body,
|
|
520
|
+
RawBody: rawBody,
|
|
521
|
+
CommandBody: cleanedText,
|
|
522
|
+
From: `dingtalk:user:${senderId}`,
|
|
523
|
+
To: chatType === "group" ? `dingtalk:channel:${convoId}` : `dingtalk:user:${senderId}`,
|
|
524
|
+
SessionKey: route.sessionKey,
|
|
525
|
+
AccountId: route.accountId,
|
|
526
|
+
ChatType: chatType,
|
|
527
|
+
ConversationLabel: chatType === "group" ? convoId : undefined,
|
|
528
|
+
SenderName: message.senderNick,
|
|
529
|
+
SenderId: senderId,
|
|
530
|
+
SenderUsername: message.senderNick,
|
|
531
|
+
Provider: "dingtalk",
|
|
532
|
+
Surface: "dingtalk",
|
|
533
|
+
MessageSid: message.msgId,
|
|
534
|
+
Timestamp: message.createAt,
|
|
535
|
+
GroupSubject: chatType === "group" ? convoId : undefined,
|
|
536
|
+
OriginatingChannel: CHANNEL_ID,
|
|
537
|
+
OriginatingTo: chatType === "group" ? `dingtalk:channel:${convoId}` : `dingtalk:user:${senderId}`,
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const finalizedCtx = core.channel.reply.finalizeInboundContext(ctxPayload);
|
|
541
|
+
|
|
542
|
+
const storePath = core.channel.session?.resolveStorePath?.(
|
|
543
|
+
(cfg as Record<string, unknown>).session,
|
|
544
|
+
{ agentId: route.agentId }
|
|
545
|
+
) ?? "";
|
|
546
|
+
|
|
547
|
+
if (core.channel.session?.recordInboundSession) {
|
|
548
|
+
await core.channel.session.recordInboundSession({
|
|
549
|
+
storePath,
|
|
550
|
+
sessionKey: route.sessionKey,
|
|
551
|
+
ctx: finalizedCtx,
|
|
552
|
+
onRecordError: (err) => {
|
|
553
|
+
log?.error?.(`[${accountId}] Failed to record session: ${String(err)}`);
|
|
554
|
+
},
|
|
555
|
+
});
|
|
556
|
+
}
|
|
294
557
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
ctx.log?.info?.(`[${accountId}] DingTalk Stream client connected`);
|
|
558
|
+
if (res.headers?.messageId) {
|
|
559
|
+
client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
|
|
560
|
+
}
|
|
299
561
|
|
|
300
|
-
|
|
301
|
-
ctx.abortSignal?.addEventListener('abort', () => {
|
|
302
|
-
ctx.log?.info?.(`[${accountId}] stopping DingTalk Stream client`);
|
|
303
|
-
client.disconnect();
|
|
304
|
-
activeClients.delete(accountId);
|
|
305
|
-
});
|
|
562
|
+
const DINGTALK_TEXT_LIMIT = 2000;
|
|
306
563
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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;
|
|
564
|
+
const deliverDingTalkReply = async (payload: { text?: string; content?: string; mediaUrls?: string[] }) => {
|
|
565
|
+
const text = payload.text || payload.content || "";
|
|
566
|
+
if (!text) {
|
|
567
|
+
log?.warn?.(`[${accountId}] Received empty payload`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
318
570
|
|
|
319
|
-
|
|
320
|
-
|
|
571
|
+
log?.info?.(`[${accountId}] Sending reply: ${text.substring(0, 50)}...`);
|
|
572
|
+
|
|
573
|
+
const chunkMode = core.channel.text?.resolveChunkMode?.(cfg, CHANNEL_ID, accountId) ?? "smart";
|
|
574
|
+
const chunks = core.channel.text?.chunkMarkdownTextWithMode?.(text, DINGTALK_TEXT_LIMIT, chunkMode) ?? [text];
|
|
575
|
+
|
|
576
|
+
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
|
577
|
+
if (!chunk) continue;
|
|
578
|
+
const result = await getDingTalkRuntime().channel.dingtalk.sendMessage(convoId, chunk, {
|
|
579
|
+
accountId,
|
|
580
|
+
});
|
|
321
581
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
return { ok: true as const };
|
|
331
|
-
} catch (error) {
|
|
332
|
-
// Fall through to webhookUrl
|
|
333
|
-
}
|
|
334
|
-
}
|
|
582
|
+
if (result.ok) {
|
|
583
|
+
log?.info?.(`[${accountId}] Reply sent successfully`);
|
|
584
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
585
|
+
} else {
|
|
586
|
+
log?.error?.(`[${accountId}] Failed to send reply: ${result.error}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
335
590
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
591
|
+
log?.info?.(`[${accountId}] Using dispatchReplyWithBufferedBlockDispatcher`);
|
|
592
|
+
|
|
593
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
594
|
+
ctx: finalizedCtx,
|
|
595
|
+
cfg,
|
|
596
|
+
dispatcherOptions: {
|
|
597
|
+
deliver: deliverDingTalkReply,
|
|
598
|
+
onError: (err, info) => {
|
|
599
|
+
log?.error?.(`[${accountId}] DingTalk ${info.kind} reply failed: ${String(err)}`);
|
|
600
|
+
},
|
|
601
|
+
},
|
|
344
602
|
});
|
|
345
|
-
|
|
603
|
+
|
|
604
|
+
log?.info?.(`[${accountId}] dispatchReplyWithBufferedBlockDispatcher completed`);
|
|
346
605
|
} catch (error) {
|
|
347
|
-
|
|
606
|
+
log?.error?.(
|
|
607
|
+
`[${accountId}] Error processing message: ${error instanceof Error ? error.message : String(error)}`
|
|
608
|
+
);
|
|
348
609
|
}
|
|
349
|
-
}
|
|
610
|
+
};
|
|
350
611
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
612
|
+
client.registerCallbackListener("/v1.0/im/bot/messages/get", handleMessage);
|
|
613
|
+
|
|
614
|
+
await client.connect();
|
|
615
|
+
getDingTalkRuntime().channel.dingtalk.setClient(accountId, client);
|
|
616
|
+
log?.info?.(`[${accountId}] DingTalk Stream client connected`);
|
|
355
617
|
|
|
618
|
+
abortSignal?.addEventListener("abort", () => {
|
|
619
|
+
log?.info?.(`[${accountId}] Stopping DingTalk Stream client`);
|
|
620
|
+
client.disconnect();
|
|
621
|
+
getDingTalkRuntime().channel.dingtalk.removeClient(accountId);
|
|
622
|
+
});
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
};
|
|
356
626
|
|
|
627
|
+
// ============================================================================
|
|
628
|
+
// Plugin Export
|
|
629
|
+
// ============================================================================
|
|
357
630
|
|
|
358
|
-
// Plugin object format required by Clawdbot
|
|
359
631
|
const plugin = {
|
|
360
|
-
id:
|
|
632
|
+
id: CHANNEL_ID,
|
|
361
633
|
name: "DingTalk Channel",
|
|
362
|
-
description: "DingTalk channel plugin
|
|
363
|
-
|
|
364
|
-
type: "object" as const,
|
|
365
|
-
properties: {}
|
|
366
|
-
},
|
|
634
|
+
description: "DingTalk channel plugin (Stream mode)",
|
|
635
|
+
|
|
367
636
|
register(api: ClawdbotPluginApi) {
|
|
368
637
|
pluginRuntime = api;
|
|
369
|
-
api.registerChannel({ plugin:
|
|
370
|
-
}
|
|
638
|
+
api.registerChannel({ plugin: dingtalkPlugin });
|
|
639
|
+
},
|
|
371
640
|
};
|
|
372
641
|
|
|
373
|
-
export default plugin;
|
|
642
|
+
export default plugin;
|