moltbot-dingtalk-stream 1.0.8 → 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 -258
- 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 -314
- 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,382 +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
|
-
// From 地址: 用于标识发送者
|
|
191
|
-
// - 群聊: channel:group:<groupId>:<senderId>
|
|
192
|
-
// - 私聊: channel:<senderId>
|
|
193
|
-
const fromAddress = chatType === 'group'
|
|
194
|
-
? `moltbot-dingtalk-stream:group:${convoId}:${senderId}`
|
|
195
|
-
: `moltbot-dingtalk-stream:${senderId}`;
|
|
196
|
-
|
|
197
|
-
const ctxPayload = {
|
|
198
|
-
Body: cleanedText,
|
|
199
|
-
RawBody: textContent,
|
|
200
|
-
CommandBody: cleanedText,
|
|
201
|
-
From: fromAddress,
|
|
202
|
-
To: 'bot',
|
|
203
|
-
// SessionKey 根据 chatType 设置:
|
|
204
|
-
// - 群聊: 使用 group:<conversationId> 格式让所有群成员共享上下文
|
|
205
|
-
// - 私聊: 使用 dm:<senderId> 格式让每个用户有独立的会话上下文
|
|
206
|
-
SessionKey: chatType === 'group' ? `group:${convoId}` : `dm:${senderId}`,
|
|
207
|
-
AccountId: accountId,
|
|
208
|
-
ChatType: chatType,
|
|
209
|
-
SenderName: message.senderNick,
|
|
210
|
-
SenderId: senderId,
|
|
211
|
-
Provider: 'moltbot-dingtalk-stream',
|
|
212
|
-
Surface: 'moltbot-dingtalk-stream',
|
|
213
|
-
MessageSid: message.msgId,
|
|
214
|
-
Timestamp: message.createAt,
|
|
215
|
-
// 群聊相关元数据
|
|
216
|
-
GroupSubject: chatType === 'group' ? (message.conversationId) : undefined,
|
|
217
|
-
ConversationLabel: chatType === 'group' ? `钉钉群:${convoId}` : `钉钉私聊:${message.senderNick || senderId}`,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const finalizedCtx = replyModule.finalizeInboundContext(ctxPayload);
|
|
221
|
-
|
|
222
|
-
let replyBuffer = "";
|
|
223
|
-
let replySent = false;
|
|
224
|
-
|
|
225
|
-
const sendToDingTalk = async (text: string) => {
|
|
226
|
-
if (!text) return;
|
|
227
|
-
if (replySent) {
|
|
228
|
-
ctx.log?.info?.(`[${accountId}] Reply already sent, skipping buffer flush.`);
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const replyWebhook = sessionWebhooks.get(convoId) || config.webhookUrl;
|
|
233
|
-
if (!replyWebhook) {
|
|
234
|
-
ctx.log?.error?.(`[${accountId}] No webhook to reply to ${convoId}`);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
await axios.post(replyWebhook, {
|
|
240
|
-
msgtype: "text",
|
|
241
|
-
text: { content: text }
|
|
242
|
-
}, { headers: { 'Content-Type': 'application/json' } });
|
|
243
|
-
replySent = true;
|
|
244
|
-
ctx.log?.info?.(`[${accountId}] Reply sent successfully.`);
|
|
245
|
-
} catch (e) {
|
|
246
|
-
ctx.log?.error?.(`[${accountId}] Failed to send reply: ${e}`);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const dispatcher = {
|
|
251
|
-
sendFinalReply: (payload: any) => {
|
|
252
|
-
const text = payload.text || payload.content || '';
|
|
253
|
-
sendToDingTalk(text).catch(e => ctx.log?.error?.(`[${accountId}] sendToDingTalk failed: ${e}`));
|
|
254
|
-
return true;
|
|
255
|
-
},
|
|
256
|
-
typing: async () => { },
|
|
257
|
-
reaction: async () => { },
|
|
258
|
-
isSynchronous: () => false,
|
|
259
|
-
waitForIdle: async () => { },
|
|
260
|
-
sendBlockReply: async (block: any) => {
|
|
261
|
-
// Accumulate text from blocks
|
|
262
|
-
const text = block.text || block.delta || block.content || '';
|
|
263
|
-
if (text) {
|
|
264
|
-
replyBuffer += text;
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
getQueuedCounts: () => ({ active: 0, queued: 0, final: 0 })
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
// Internal dispatch
|
|
271
|
-
const dispatchPromise = replyModule.dispatchReplyFromConfig({
|
|
272
|
-
ctx: finalizedCtx,
|
|
273
|
-
cfg: pluginRuntime.config,
|
|
274
|
-
dispatcher: dispatcher,
|
|
275
|
-
replyOptions: {}
|
|
276
|
-
});
|
|
464
|
+
const client = new DWClient({
|
|
465
|
+
clientId: account.clientId,
|
|
466
|
+
clientSecret: account.clientSecret,
|
|
467
|
+
});
|
|
277
468
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
282
475
|
|
|
283
|
-
|
|
284
|
-
await dispatchPromise;
|
|
476
|
+
log?.info?.(`[${accountId}] Received message from ${message.senderNick || senderId}: ${textContent}`);
|
|
285
477
|
|
|
286
|
-
|
|
287
|
-
if (!replySent && replyBuffer) {
|
|
288
|
-
ctx.log?.info?.(`[${accountId}] Sending accumulated buffer from blocks (len=${replyBuffer.length}).`);
|
|
289
|
-
await sendToDingTalk(replyBuffer);
|
|
290
|
-
}
|
|
478
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
291
479
|
|
|
292
|
-
|
|
293
|
-
|
|
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);
|
|
294
496
|
}
|
|
295
|
-
} catch (error) {
|
|
296
|
-
ctx.log?.error?.(`[${accountId}] error processing message: ${error instanceof Error ? error.message : String(error)}`);
|
|
297
|
-
console.error('DingTalk Handler Error:', error);
|
|
298
497
|
}
|
|
299
|
-
};
|
|
300
498
|
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|
|
303
557
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ctx.log?.info?.(`[${accountId}] DingTalk Stream client connected`);
|
|
558
|
+
if (res.headers?.messageId) {
|
|
559
|
+
client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
|
|
560
|
+
}
|
|
308
561
|
|
|
309
|
-
|
|
310
|
-
ctx.abortSignal?.addEventListener('abort', () => {
|
|
311
|
-
ctx.log?.info?.(`[${accountId}] stopping DingTalk Stream client`);
|
|
312
|
-
client.disconnect();
|
|
313
|
-
activeClients.delete(accountId);
|
|
314
|
-
});
|
|
562
|
+
const DINGTALK_TEXT_LIMIT = 2000;
|
|
315
563
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
outbound: {
|
|
323
|
-
deliveryMode: "direct" as const,
|
|
324
|
-
sendText: async (opts: { text: string; account: ResolvedDingTalkAccount; target: string; senderId?: string }) => {
|
|
325
|
-
const { text, account, target } = opts;
|
|
326
|
-
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
|
+
}
|
|
327
570
|
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
});
|
|
330
581
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
return { ok: true as const };
|
|
340
|
-
} catch (error) {
|
|
341
|
-
// Fall through to webhookUrl
|
|
342
|
-
}
|
|
343
|
-
}
|
|
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
|
+
};
|
|
344
590
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
+
},
|
|
353
602
|
});
|
|
354
|
-
|
|
603
|
+
|
|
604
|
+
log?.info?.(`[${accountId}] dispatchReplyWithBufferedBlockDispatcher completed`);
|
|
355
605
|
} catch (error) {
|
|
356
|
-
|
|
606
|
+
log?.error?.(
|
|
607
|
+
`[${accountId}] Error processing message: ${error instanceof Error ? error.message : String(error)}`
|
|
608
|
+
);
|
|
357
609
|
}
|
|
358
|
-
}
|
|
610
|
+
};
|
|
359
611
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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`);
|
|
364
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
|
+
};
|
|
365
626
|
|
|
627
|
+
// ============================================================================
|
|
628
|
+
// Plugin Export
|
|
629
|
+
// ============================================================================
|
|
366
630
|
|
|
367
|
-
// Plugin object format required by Clawdbot
|
|
368
631
|
const plugin = {
|
|
369
|
-
id:
|
|
632
|
+
id: CHANNEL_ID,
|
|
370
633
|
name: "DingTalk Channel",
|
|
371
|
-
description: "DingTalk channel plugin
|
|
372
|
-
|
|
373
|
-
type: "object" as const,
|
|
374
|
-
properties: {}
|
|
375
|
-
},
|
|
634
|
+
description: "DingTalk channel plugin (Stream mode)",
|
|
635
|
+
|
|
376
636
|
register(api: ClawdbotPluginApi) {
|
|
377
637
|
pluginRuntime = api;
|
|
378
|
-
api.registerChannel({ plugin:
|
|
379
|
-
}
|
|
638
|
+
api.registerChannel({ plugin: dingtalkPlugin });
|
|
639
|
+
},
|
|
380
640
|
};
|
|
381
641
|
|
|
382
|
-
export default plugin;
|
|
642
|
+
export default plugin;
|