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/dist/index.js CHANGED
@@ -1,302 +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 axios_1 = __importDefault(require("axios"));
8
- // Store plugin runtime
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
- // Store session webhooks for reply
11
- const sessionWebhooks = new Map();
12
- // Store active clients for each account
13
- const activeClients = new Map();
14
- // Helper functions
15
- function listDingTalkAccountIds(cfg) {
16
- const accounts = cfg.channels?.['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
- const dingTalkChannelPlugin = {
32
- id: "moltbot-dingtalk-stream",
33
- meta: {
34
- id: "moltbot-dingtalk-stream",
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
- reload: { configPrefixes: ["channels.moltbot-dingtalk-stream"] },
47
- configSchema: {
48
- type: "object",
49
- properties: {
50
- channels: {
51
- type: "object",
52
- properties: {
53
- 'moltbot-dingtalk-stream': {
54
- type: "object",
55
- properties: {
56
- accounts: {
57
- type: "object",
58
- additionalProperties: {
59
- type: "object",
60
- properties: {
61
- enabled: { type: "boolean" },
62
- clientId: { type: "string" },
63
- clientSecret: { type: "string" },
64
- webhookUrl: { type: "string" },
65
- name: { type: "string" },
66
- },
67
- required: ["clientId", "clientSecret"],
68
- },
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
- config: {
77
- listAccountIds: (cfg) => listDingTalkAccountIds(cfg),
78
- resolveAccount: (cfg, accountId) => resolveDingTalkAccount({ cfg, accountId }),
79
- defaultAccountId: (_cfg) => 'default',
80
- isConfigured: (account) => account.configured,
81
- describeAccount: (account) => ({
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.account;
91
- const config = account.config;
217
+ const { account, cfg, abortSignal, log, statusSink } = ctx;
92
218
  const accountId = account.accountId;
93
- if (!config.clientId || !config.clientSecret) {
94
- ctx.log?.warn?.(`[${accountId}] missing clientId or clientSecret`);
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
- ctx.log?.info?.(`[${accountId}] starting DingTalk Stream client`);
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 client = new dingtalk_stream_1.DWClient({
100
- clientId: config.clientId,
101
- clientSecret: config.clientSecret,
102
- });
103
- // Helper to safely handle messages
104
- const handleMessage = async (res) => {
105
- try {
106
- const message = JSON.parse(res.data);
107
- const textContent = message.text?.content || "";
108
- const senderId = message.senderId;
109
- const convoId = message.conversationId;
110
- const msgId = message.msgId;
111
- // Store session webhook if provided (DingTalk Stream mode provides this for replies)
112
- if (message.sessionWebhook) {
113
- sessionWebhooks.set(convoId, message.sessionWebhook);
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
- // Log reception
116
- ctx.log?.info?.(`[${accountId}] received message from ${message.senderNick || senderId}: ${textContent}`);
117
- // Filter out empty messages
118
- if (!textContent)
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
- // Simple text cleaning (remove @bot mentions if possible, though DingTalk usually gives clean content or we might need to parse entities)
121
- const cleanedText = textContent.replace(/@\w+\s*/g, '').trim();
122
- // Forward the message to Clawdbot for processing
123
- if (pluginRuntime?.runtime?.channel?.reply) {
124
- const replyModule = pluginRuntime.runtime.channel.reply;
125
- const chatType = String(message.conversationType) === '2' ? 'group' : 'direct';
126
- // From 地址: 用于标识发送者
127
- // - 群聊: channel:group:<groupId>:<senderId>
128
- // - 私聊: channel:<senderId>
129
- const fromAddress = chatType === 'group'
130
- ? `moltbot-dingtalk-stream:group:${convoId}:${senderId}`
131
- : `moltbot-dingtalk-stream:${senderId}`;
132
- const ctxPayload = {
133
- Body: cleanedText,
134
- RawBody: textContent,
135
- CommandBody: cleanedText,
136
- From: fromAddress,
137
- To: 'bot',
138
- // SessionKey 根据 chatType 设置:
139
- // - 群聊: 使用 group:<conversationId> 格式让所有群成员共享上下文
140
- // - 私聊: 使用 dm:<senderId> 格式让每个用户有独立的会话上下文
141
- SessionKey: chatType === 'group' ? `group:${convoId}` : `dm:${senderId}`,
142
- AccountId: accountId,
143
- ChatType: chatType,
144
- SenderName: message.senderNick,
145
- SenderId: senderId,
146
- Provider: 'moltbot-dingtalk-stream',
147
- Surface: 'moltbot-dingtalk-stream',
148
- MessageSid: message.msgId,
149
- Timestamp: message.createAt,
150
- // 群聊相关元数据
151
- GroupSubject: chatType === 'group' ? (message.conversationId) : undefined,
152
- ConversationLabel: chatType === 'group' ? `钉钉群:${convoId}` : `钉钉私聊:${message.senderNick || senderId}`,
153
- };
154
- const finalizedCtx = replyModule.finalizeInboundContext(ctxPayload);
155
- let replyBuffer = "";
156
- let replySent = false;
157
- const sendToDingTalk = async (text) => {
158
- if (!text)
159
- return;
160
- if (replySent) {
161
- ctx.log?.info?.(`[${accountId}] Reply already sent, skipping buffer flush.`);
162
- return;
163
- }
164
- const replyWebhook = sessionWebhooks.get(convoId) || config.webhookUrl;
165
- if (!replyWebhook) {
166
- ctx.log?.error?.(`[${accountId}] No webhook to reply to ${convoId}`);
167
- return;
168
- }
169
- try {
170
- await axios_1.default.post(replyWebhook, {
171
- msgtype: "text",
172
- text: { content: text }
173
- }, { headers: { 'Content-Type': 'application/json' } });
174
- replySent = true;
175
- ctx.log?.info?.(`[${accountId}] Reply sent successfully.`);
176
- }
177
- catch (e) {
178
- ctx.log?.error?.(`[${accountId}] Failed to send reply: ${e}`);
179
- }
180
- };
181
- const dispatcher = {
182
- sendFinalReply: (payload) => {
183
- const text = payload.text || payload.content || '';
184
- sendToDingTalk(text).catch(e => ctx.log?.error?.(`[${accountId}] sendToDingTalk failed: ${e}`));
185
- return true;
186
- },
187
- typing: async () => { },
188
- reaction: async () => { },
189
- isSynchronous: () => false,
190
- waitForIdle: async () => { },
191
- sendBlockReply: async (block) => {
192
- // Accumulate text from blocks
193
- const text = block.text || block.delta || block.content || '';
194
- if (text) {
195
- replyBuffer += text;
196
- }
197
- },
198
- getQueuedCounts: () => ({ active: 0, queued: 0, final: 0 })
199
- };
200
- // Internal dispatch
201
- const dispatchPromise = replyModule.dispatchReplyFromConfig({
202
- ctx: finalizedCtx,
203
- cfg: pluginRuntime.config,
204
- dispatcher: dispatcher,
205
- 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,
206
339
  });
207
- // ACK immediately to prevent retries
208
- if (res.headers && res.headers.messageId) {
209
- client.socketCallBackResponse(res.headers.messageId, { status: "SUCCEED" });
340
+ if (result.ok) {
341
+ log?.info?.(`[${accountId}] Reply sent successfully`);
342
+ statusSink?.({ lastOutboundAt: Date.now() });
210
343
  }
211
- // Wait for run to finish
212
- await dispatchPromise;
213
- // If final reply wasn't called but we have buffer (streaming case where agent didn't return final payload?)
214
- if (!replySent && replyBuffer) {
215
- ctx.log?.info?.(`[${accountId}] Sending accumulated buffer from blocks (len=${replyBuffer.length}).`);
216
- await sendToDingTalk(replyBuffer);
344
+ else {
345
+ log?.error?.(`[${accountId}] Failed to send reply: ${result.error}`);
217
346
  }
218
347
  }
219
- else {
220
- ctx.log?.error?.(`[${accountId}] runtime.channel.reply not available`);
221
- }
222
- }
223
- catch (error) {
224
- ctx.log?.error?.(`[${accountId}] error processing message: ${error instanceof Error ? error.message : String(error)}`);
225
- console.error('DingTalk Handler Error:', error);
226
- }
227
- };
228
- // Register callback for robot messages
229
- client.registerCallbackListener('/v1.0/im/bot/messages/get', handleMessage);
230
- // Connect to DingTalk Stream
231
- await client.connect();
232
- activeClients.set(accountId, client);
233
- ctx.log?.info?.(`[${accountId}] DingTalk Stream client connected`);
234
- // Handle abort signal for cleanup
235
- ctx.abortSignal?.addEventListener('abort', () => {
236
- ctx.log?.info?.(`[${accountId}] stopping DingTalk Stream client`);
237
- client.disconnect();
238
- activeClients.delete(accountId);
239
- });
240
- }
241
- catch (error) {
242
- ctx.log?.error?.(`[${accountId}] failed to start: ${error instanceof Error ? error.message : String(error)}`);
243
- throw error;
244
- }
245
- },
246
- },
247
- outbound: {
248
- deliveryMode: "direct",
249
- sendText: async (opts) => {
250
- const { text, account, target } = opts;
251
- const config = account.config;
252
- // Try session webhook first (for replies)
253
- const sessionWebhook = sessionWebhooks.get(target);
254
- if (sessionWebhook) {
255
- try {
256
- await axios_1.default.post(sessionWebhook, {
257
- msgtype: "text",
258
- text: { content: text }
259
- }, {
260
- headers: { 'Content-Type': 'application/json' }
261
- });
262
- return { ok: true };
263
- }
264
- catch (error) {
265
- // Fall through to webhookUrl
266
- }
267
- }
268
- // Fallback to webhookUrl for proactive messages
269
- if (config?.webhookUrl) {
270
- try {
271
- await axios_1.default.post(config.webhookUrl, {
272
- msgtype: "text",
273
- text: { content: text }
274
- }, {
275
- 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
+ },
276
359
  });
277
- return { ok: true };
360
+ log?.info?.(`[${accountId}] dispatchReplyWithBufferedBlockDispatcher completed`);
278
361
  }
279
362
  catch (error) {
280
- return { ok: false, error: error instanceof Error ? error.message : String(error) };
363
+ log?.error?.(`[${accountId}] Error processing message: ${error instanceof Error ? error.message : String(error)}`);
281
364
  }
282
- }
283
- return { ok: false, error: "No webhook available for sending messages" };
284
- }
285
- }
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
+ },
286
377
  };
287
- // Plugin object format required by Clawdbot
378
+ // ============================================================================
379
+ // Plugin Export
380
+ // ============================================================================
288
381
  const plugin = {
289
- id: "moltbot-dingtalk-stream",
382
+ id: schema_js_1.CHANNEL_ID,
290
383
  name: "DingTalk Channel",
291
- description: "DingTalk channel plugin using Stream mode",
292
- configSchema: {
293
- type: "object",
294
- properties: {}
295
- },
384
+ description: "DingTalk channel plugin (Stream mode)",
296
385
  register(api) {
297
386
  pluginRuntime = api;
298
- api.registerChannel({ plugin: dingTalkChannelPlugin });
299
- }
387
+ api.registerChannel({ plugin: exports.dingtalkPlugin });
388
+ },
300
389
  };
301
390
  exports.default = plugin;
302
391
  //# sourceMappingURL=index.js.map