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/dist/index.js
CHANGED
|
@@ -1,293 +1,391 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dingtalkPlugin = void 0;
|
|
6
4
|
const dingtalk_stream_1 = require("dingtalk-stream");
|
|
7
|
-
const
|
|
8
|
-
|
|
5
|
+
const runtime_js_1 = require("./runtime.js");
|
|
6
|
+
const schema_js_1 = require("./schema.js");
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Channel Meta
|
|
9
|
+
// ============================================================================
|
|
10
|
+
const meta = {
|
|
11
|
+
id: schema_js_1.CHANNEL_ID,
|
|
12
|
+
label: "DingTalk",
|
|
13
|
+
selectionLabel: "DingTalk Bot (Stream)",
|
|
14
|
+
docsPath: "/channels/dingtalk",
|
|
15
|
+
docsLabel: "dingtalk",
|
|
16
|
+
blurb: "DingTalk bot channel plugin (Stream mode)",
|
|
17
|
+
order: 100,
|
|
18
|
+
aliases: ["dt", "ding", "dingtalk"],
|
|
19
|
+
};
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Store plugin runtime reference
|
|
22
|
+
// ============================================================================
|
|
9
23
|
let pluginRuntime = null;
|
|
10
|
-
//
|
|
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?.['moltbot-dingtalk-stream']?.accounts;
|
|
17
|
-
return accounts ? Object.keys(accounts) : [];
|
|
18
|
-
}
|
|
19
|
-
function resolveDingTalkAccount(opts) {
|
|
20
|
-
const { cfg, accountId = 'default' } = opts;
|
|
21
|
-
const account = cfg.channels?.['moltbot-dingtalk-stream']?.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
|
-
}
|
|
24
|
+
// ============================================================================
|
|
30
25
|
// DingTalk Channel Plugin
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
label: "钉钉",
|
|
36
|
-
selectionLabel: "DingTalk Bot (Stream)",
|
|
37
|
-
docsPath: "/channels/moltbot-dingtalk-stream",
|
|
38
|
-
docsLabel: "dingtalk",
|
|
39
|
-
blurb: "钉钉机器人通道插件 (Stream模式)",
|
|
40
|
-
order: 100,
|
|
41
|
-
aliases: ["dt", "ding"],
|
|
42
|
-
},
|
|
26
|
+
// ============================================================================
|
|
27
|
+
exports.dingtalkPlugin = {
|
|
28
|
+
id: schema_js_1.CHANNEL_ID,
|
|
29
|
+
meta,
|
|
43
30
|
capabilities: {
|
|
44
31
|
chatTypes: ["direct", "group"],
|
|
32
|
+
media: true,
|
|
33
|
+
threads: false,
|
|
34
|
+
},
|
|
35
|
+
reload: { configPrefixes: [`channels.${schema_js_1.CHANNEL_ID}`] },
|
|
36
|
+
configSchema: schema_js_1.DingTalkConfigSchema,
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Config Management
|
|
39
|
+
// ============================================================================
|
|
40
|
+
config: {
|
|
41
|
+
listAccountIds: (cfg) => (0, schema_js_1.listDingTalkAccountIds)(cfg),
|
|
42
|
+
resolveAccount: (cfg, accountId) => (0, schema_js_1.resolveDingTalkAccount)({ cfg, accountId }),
|
|
43
|
+
defaultAccountId: (cfg) => (0, schema_js_1.resolveDefaultDingTalkAccountId)(cfg),
|
|
44
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => (0, schema_js_1.setAccountEnabledInConfig)({ cfg, accountId, enabled }),
|
|
45
|
+
deleteAccount: ({ cfg, accountId }) => (0, schema_js_1.deleteAccountFromConfig)({ cfg, accountId }),
|
|
46
|
+
isConfigured: (account) => account.configured,
|
|
47
|
+
describeAccount: (account) => ({
|
|
48
|
+
accountId: account.accountId,
|
|
49
|
+
name: account.name,
|
|
50
|
+
enabled: account.enabled,
|
|
51
|
+
configured: account.configured,
|
|
52
|
+
tokenSource: account.tokenSource,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Security (DM Policy)
|
|
57
|
+
// ============================================================================
|
|
58
|
+
security: {
|
|
59
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
60
|
+
const resolvedAccountId = accountId ?? account.accountId ?? schema_js_1.DEFAULT_ACCOUNT_ID;
|
|
61
|
+
const channelConfig = cfg.channels?.[schema_js_1.CHANNEL_ID];
|
|
62
|
+
const useAccountPath = Boolean(channelConfig?.accounts?.[resolvedAccountId]);
|
|
63
|
+
const allowFromPath = useAccountPath
|
|
64
|
+
? `channels.${schema_js_1.CHANNEL_ID}.accounts.${resolvedAccountId}.dm.`
|
|
65
|
+
: `channels.${schema_js_1.CHANNEL_ID}.dm.`;
|
|
66
|
+
return {
|
|
67
|
+
policy: account.config.dm?.policy ?? "open",
|
|
68
|
+
allowFrom: account.config.dm?.allowFrom ?? [],
|
|
69
|
+
allowFromPath,
|
|
70
|
+
normalizeEntry: (raw) => raw.replace(/^dingtalk:/i, ""),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Mentions
|
|
76
|
+
// ============================================================================
|
|
77
|
+
mentions: {
|
|
78
|
+
stripPatterns: () => ["@\\S+\\s*"],
|
|
79
|
+
},
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Groups
|
|
82
|
+
// ============================================================================
|
|
83
|
+
groups: {
|
|
84
|
+
resolveRequireMention: ({ cfg, accountId }) => {
|
|
85
|
+
const account = (0, schema_js_1.resolveDingTalkAccount)({ cfg, accountId });
|
|
86
|
+
return account.config.requireMention ?? true;
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// Messaging
|
|
91
|
+
// ============================================================================
|
|
92
|
+
messaging: {
|
|
93
|
+
normalizeTarget: (target) => {
|
|
94
|
+
if (target.startsWith("dingtalk:"))
|
|
95
|
+
return target;
|
|
96
|
+
if (target.startsWith("group:"))
|
|
97
|
+
return `dingtalk:${target}`;
|
|
98
|
+
if (target.startsWith("user:"))
|
|
99
|
+
return `dingtalk:${target}`;
|
|
100
|
+
return `dingtalk:${target}`;
|
|
101
|
+
},
|
|
102
|
+
targetResolver: {
|
|
103
|
+
looksLikeId: (id) => /^[a-zA-Z0-9_-]+$/.test(id),
|
|
104
|
+
hint: "<conversationId|user:ID>",
|
|
105
|
+
},
|
|
45
106
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Setup (Account Configuration)
|
|
109
|
+
// ============================================================================
|
|
110
|
+
setup: {
|
|
111
|
+
resolveAccountId: ({ accountId }) => (0, schema_js_1.normalizeAccountId)(accountId),
|
|
112
|
+
applyAccountName: ({ cfg, accountId, name }) => (0, schema_js_1.applyAccountNameToConfig)({ cfg, accountId, name }),
|
|
113
|
+
validateInput: ({ accountId, input }) => {
|
|
114
|
+
if (input.useEnv && accountId !== schema_js_1.DEFAULT_ACCOUNT_ID) {
|
|
115
|
+
return "Environment variables can only be used for the default account";
|
|
116
|
+
}
|
|
117
|
+
if (!input.useEnv && (!input.clientId || !input.clientSecret)) {
|
|
118
|
+
return "DingTalk requires clientId and clientSecret";
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
123
|
+
const namedConfig = (0, schema_js_1.applyAccountNameToConfig)({
|
|
124
|
+
cfg,
|
|
125
|
+
accountId,
|
|
126
|
+
name: input.name,
|
|
127
|
+
});
|
|
128
|
+
if (accountId === schema_js_1.DEFAULT_ACCOUNT_ID) {
|
|
129
|
+
return {
|
|
130
|
+
...namedConfig,
|
|
131
|
+
channels: {
|
|
132
|
+
...namedConfig.channels,
|
|
133
|
+
[schema_js_1.CHANNEL_ID]: {
|
|
134
|
+
...namedConfig.channels?.[schema_js_1.CHANNEL_ID],
|
|
135
|
+
enabled: true,
|
|
136
|
+
...(input.useEnv ? {} : { clientId: input.clientId, clientSecret: input.clientSecret }),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...namedConfig,
|
|
143
|
+
channels: {
|
|
144
|
+
...namedConfig.channels,
|
|
145
|
+
[schema_js_1.CHANNEL_ID]: {
|
|
146
|
+
...namedConfig.channels?.[schema_js_1.CHANNEL_ID],
|
|
147
|
+
enabled: true,
|
|
148
|
+
accounts: {
|
|
149
|
+
...namedConfig.channels?.[schema_js_1.CHANNEL_ID]?.accounts,
|
|
150
|
+
[accountId]: {
|
|
151
|
+
...namedConfig.channels?.[schema_js_1.CHANNEL_ID]?.accounts?.[accountId],
|
|
152
|
+
enabled: true,
|
|
153
|
+
clientId: input.clientId,
|
|
154
|
+
clientSecret: input.clientSecret,
|
|
69
155
|
},
|
|
70
156
|
},
|
|
71
157
|
},
|
|
72
158
|
},
|
|
73
|
-
}
|
|
159
|
+
};
|
|
74
160
|
},
|
|
75
161
|
},
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Outbound (Send Messages)
|
|
164
|
+
// ============================================================================
|
|
165
|
+
outbound: {
|
|
166
|
+
deliveryMode: "direct",
|
|
167
|
+
textChunkLimit: 2000,
|
|
168
|
+
sendText: async ({ to, text, accountId }) => {
|
|
169
|
+
const result = await (0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.sendMessage(to, text, {
|
|
170
|
+
accountId,
|
|
171
|
+
});
|
|
172
|
+
return { channel: schema_js_1.CHANNEL_ID, ...result };
|
|
173
|
+
},
|
|
174
|
+
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
175
|
+
const result = await (0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.sendMessage(to, text, {
|
|
176
|
+
accountId,
|
|
177
|
+
mediaUrl,
|
|
178
|
+
});
|
|
179
|
+
return { channel: schema_js_1.CHANNEL_ID, ...result };
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Status (Probe & Monitoring)
|
|
184
|
+
// ============================================================================
|
|
185
|
+
status: {
|
|
186
|
+
defaultRuntime: {
|
|
187
|
+
accountId: schema_js_1.DEFAULT_ACCOUNT_ID,
|
|
188
|
+
running: false,
|
|
189
|
+
lastStartAt: null,
|
|
190
|
+
lastStopAt: null,
|
|
191
|
+
lastError: null,
|
|
192
|
+
},
|
|
193
|
+
probeAccount: async ({ account, timeoutMs }) => {
|
|
194
|
+
if (!account.clientId || !account.clientSecret) {
|
|
195
|
+
return { ok: false, error: "Missing clientId or clientSecret" };
|
|
196
|
+
}
|
|
197
|
+
return (0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.probe(account.clientId, account.clientSecret, timeoutMs);
|
|
198
|
+
},
|
|
199
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
82
200
|
accountId: account.accountId,
|
|
83
201
|
name: account.name,
|
|
84
202
|
enabled: account.enabled,
|
|
85
203
|
configured: account.configured,
|
|
204
|
+
tokenSource: account.tokenSource,
|
|
205
|
+
running: runtime?.running ?? false,
|
|
206
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
207
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
208
|
+
lastError: runtime?.lastError ?? null,
|
|
209
|
+
probe,
|
|
86
210
|
}),
|
|
87
211
|
},
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Gateway (Start/Stop Bot)
|
|
214
|
+
// ============================================================================
|
|
88
215
|
gateway: {
|
|
89
216
|
startAccount: async (ctx) => {
|
|
90
|
-
const account = ctx
|
|
91
|
-
const config = account.config;
|
|
217
|
+
const { account, cfg, abortSignal, log, statusSink } = ctx;
|
|
92
218
|
const accountId = account.accountId;
|
|
93
|
-
|
|
94
|
-
|
|
219
|
+
const core = pluginRuntime?.runtime;
|
|
220
|
+
if (!account.clientId || !account.clientSecret) {
|
|
221
|
+
log?.warn?.(`[${accountId}] Missing clientId or clientSecret`);
|
|
95
222
|
return;
|
|
96
223
|
}
|
|
97
|
-
|
|
224
|
+
if (!core?.channel?.reply) {
|
|
225
|
+
log?.error?.(`[${accountId}] runtime.channel.reply not available`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
log?.info?.(`[${accountId}] Starting DingTalk Stream client`);
|
|
229
|
+
// Probe 检测凭据
|
|
98
230
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
231
|
+
const probe = await (0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.probe(account.clientId, account.clientSecret, 2500);
|
|
232
|
+
if (probe.ok) {
|
|
233
|
+
log?.info?.(`[${accountId}] Credentials verified successfully`);
|
|
234
|
+
ctx.setStatus?.({ accountId, probe });
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
log?.warn?.(`[${accountId}] Credential verification failed: ${probe.error}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
log?.debug?.(`[${accountId}] Probe failed: ${String(err)}`);
|
|
242
|
+
}
|
|
243
|
+
const client = new dingtalk_stream_1.DWClient({
|
|
244
|
+
clientId: account.clientId,
|
|
245
|
+
clientSecret: account.clientSecret,
|
|
246
|
+
});
|
|
247
|
+
const handleMessage = async (res) => {
|
|
248
|
+
try {
|
|
249
|
+
const message = JSON.parse(res.data);
|
|
250
|
+
const textContent = message.text?.content || "";
|
|
251
|
+
const senderId = message.senderId;
|
|
252
|
+
const convoId = message.conversationId;
|
|
253
|
+
log?.info?.(`[${accountId}] Received message from ${message.senderNick || senderId}: ${textContent}`);
|
|
254
|
+
statusSink?.({ lastInboundAt: Date.now() });
|
|
255
|
+
if (!textContent)
|
|
256
|
+
return;
|
|
257
|
+
const rawBody = textContent;
|
|
258
|
+
const cleanedText = textContent.replace(/@\S+\s*/g, "").trim();
|
|
259
|
+
const chatType = String(message.conversationType) === "2" ? "group" : "direct";
|
|
260
|
+
// Store session webhook with multiple keys for flexible lookup
|
|
261
|
+
if (message.sessionWebhook) {
|
|
262
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.setSessionWebhook(convoId, message.sessionWebhook);
|
|
263
|
+
if (chatType === "direct" && senderId) {
|
|
264
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.setSessionWebhook(senderId, message.sessionWebhook);
|
|
265
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.setSessionWebhook(`dingtalk:user:${senderId}`, message.sessionWebhook);
|
|
266
|
+
}
|
|
267
|
+
if (chatType === "group" && convoId) {
|
|
268
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.setSessionWebhook(`dingtalk:channel:${convoId}`, message.sessionWebhook);
|
|
114
269
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
270
|
+
}
|
|
271
|
+
const route = core.channel.routing?.resolveAgentRoute?.({
|
|
272
|
+
cfg,
|
|
273
|
+
channel: schema_js_1.CHANNEL_ID,
|
|
274
|
+
accountId,
|
|
275
|
+
peer: {
|
|
276
|
+
kind: chatType === "group" ? "group" : "direct",
|
|
277
|
+
id: chatType === "group" ? convoId : senderId,
|
|
278
|
+
},
|
|
279
|
+
}) ?? { agentId: "main", sessionKey: `dingtalk:${convoId}`, accountId };
|
|
280
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions?.(cfg) ?? {};
|
|
281
|
+
const body = core.channel.reply.formatAgentEnvelope?.({
|
|
282
|
+
channel: "DingTalk",
|
|
283
|
+
from: message.senderNick ?? message.senderId,
|
|
284
|
+
timestamp: message.createAt,
|
|
285
|
+
envelope: envelopeOptions,
|
|
286
|
+
body: cleanedText,
|
|
287
|
+
}) ?? cleanedText;
|
|
288
|
+
const ctxPayload = {
|
|
289
|
+
Body: body,
|
|
290
|
+
RawBody: rawBody,
|
|
291
|
+
CommandBody: cleanedText,
|
|
292
|
+
From: `dingtalk:user:${senderId}`,
|
|
293
|
+
To: chatType === "group" ? `dingtalk:channel:${convoId}` : `dingtalk:user:${senderId}`,
|
|
294
|
+
SessionKey: route.sessionKey,
|
|
295
|
+
AccountId: route.accountId,
|
|
296
|
+
ChatType: chatType,
|
|
297
|
+
ConversationLabel: chatType === "group" ? convoId : undefined,
|
|
298
|
+
SenderName: message.senderNick,
|
|
299
|
+
SenderId: senderId,
|
|
300
|
+
SenderUsername: message.senderNick,
|
|
301
|
+
Provider: "dingtalk",
|
|
302
|
+
Surface: "dingtalk",
|
|
303
|
+
MessageSid: message.msgId,
|
|
304
|
+
Timestamp: message.createAt,
|
|
305
|
+
GroupSubject: chatType === "group" ? convoId : undefined,
|
|
306
|
+
OriginatingChannel: schema_js_1.CHANNEL_ID,
|
|
307
|
+
OriginatingTo: chatType === "group" ? `dingtalk:channel:${convoId}` : `dingtalk:user:${senderId}`,
|
|
308
|
+
};
|
|
309
|
+
const finalizedCtx = core.channel.reply.finalizeInboundContext(ctxPayload);
|
|
310
|
+
const storePath = core.channel.session?.resolveStorePath?.(cfg.session, { agentId: route.agentId }) ?? "";
|
|
311
|
+
if (core.channel.session?.recordInboundSession) {
|
|
312
|
+
await core.channel.session.recordInboundSession({
|
|
313
|
+
storePath,
|
|
314
|
+
sessionKey: route.sessionKey,
|
|
315
|
+
ctx: finalizedCtx,
|
|
316
|
+
onRecordError: (err) => {
|
|
317
|
+
log?.error?.(`[${accountId}] Failed to record session: ${String(err)}`);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (res.headers?.messageId) {
|
|
322
|
+
client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
|
|
323
|
+
}
|
|
324
|
+
const DINGTALK_TEXT_LIMIT = 2000;
|
|
325
|
+
const deliverDingTalkReply = async (payload) => {
|
|
326
|
+
const text = payload.text || payload.content || "";
|
|
327
|
+
if (!text) {
|
|
328
|
+
log?.warn?.(`[${accountId}] Received empty payload`);
|
|
119
329
|
return;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
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: {}
|
|
330
|
+
}
|
|
331
|
+
log?.info?.(`[${accountId}] Sending reply: ${text.substring(0, 50)}...`);
|
|
332
|
+
const chunkMode = core.channel.text?.resolveChunkMode?.(cfg, schema_js_1.CHANNEL_ID, accountId) ?? "smart";
|
|
333
|
+
const chunks = core.channel.text?.chunkMarkdownTextWithMode?.(text, DINGTALK_TEXT_LIMIT, chunkMode) ?? [text];
|
|
334
|
+
for (const chunk of chunks.length > 0 ? chunks : [text]) {
|
|
335
|
+
if (!chunk)
|
|
336
|
+
continue;
|
|
337
|
+
const result = await (0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.sendMessage(convoId, chunk, {
|
|
338
|
+
accountId,
|
|
197
339
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
340
|
+
if (result.ok) {
|
|
341
|
+
log?.info?.(`[${accountId}] Reply sent successfully`);
|
|
342
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
201
343
|
}
|
|
202
|
-
|
|
203
|
-
|
|
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);
|
|
344
|
+
else {
|
|
345
|
+
log?.error?.(`[${accountId}] Failed to send reply: ${result.error}`);
|
|
208
346
|
}
|
|
209
347
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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' }
|
|
348
|
+
};
|
|
349
|
+
log?.info?.(`[${accountId}] Using dispatchReplyWithBufferedBlockDispatcher`);
|
|
350
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
351
|
+
ctx: finalizedCtx,
|
|
352
|
+
cfg,
|
|
353
|
+
dispatcherOptions: {
|
|
354
|
+
deliver: deliverDingTalkReply,
|
|
355
|
+
onError: (err, info) => {
|
|
356
|
+
log?.error?.(`[${accountId}] DingTalk ${info.kind} reply failed: ${String(err)}`);
|
|
357
|
+
},
|
|
358
|
+
},
|
|
267
359
|
});
|
|
268
|
-
|
|
360
|
+
log?.info?.(`[${accountId}] dispatchReplyWithBufferedBlockDispatcher completed`);
|
|
269
361
|
}
|
|
270
362
|
catch (error) {
|
|
271
|
-
|
|
363
|
+
log?.error?.(`[${accountId}] Error processing message: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
364
|
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
365
|
+
};
|
|
366
|
+
client.registerCallbackListener("/v1.0/im/bot/messages/get", handleMessage);
|
|
367
|
+
await client.connect();
|
|
368
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.setClient(accountId, client);
|
|
369
|
+
log?.info?.(`[${accountId}] DingTalk Stream client connected`);
|
|
370
|
+
abortSignal?.addEventListener("abort", () => {
|
|
371
|
+
log?.info?.(`[${accountId}] Stopping DingTalk Stream client`);
|
|
372
|
+
client.disconnect();
|
|
373
|
+
(0, runtime_js_1.getDingTalkRuntime)().channel.dingtalk.removeClient(accountId);
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
},
|
|
277
377
|
};
|
|
278
|
-
//
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// Plugin Export
|
|
380
|
+
// ============================================================================
|
|
279
381
|
const plugin = {
|
|
280
|
-
id:
|
|
382
|
+
id: schema_js_1.CHANNEL_ID,
|
|
281
383
|
name: "DingTalk Channel",
|
|
282
|
-
description: "DingTalk channel plugin
|
|
283
|
-
configSchema: {
|
|
284
|
-
type: "object",
|
|
285
|
-
properties: {}
|
|
286
|
-
},
|
|
384
|
+
description: "DingTalk channel plugin (Stream mode)",
|
|
287
385
|
register(api) {
|
|
288
386
|
pluginRuntime = api;
|
|
289
|
-
api.registerChannel({ plugin:
|
|
290
|
-
}
|
|
387
|
+
api.registerChannel({ plugin: exports.dingtalkPlugin });
|
|
388
|
+
},
|
|
291
389
|
};
|
|
292
390
|
exports.default = plugin;
|
|
293
391
|
//# sourceMappingURL=index.js.map
|