@huo15/dingtalk-connector-pro 1.0.0 → 1.0.2

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.
Files changed (60) hide show
  1. package/dist/hooks/init.js +16 -0
  2. package/hooks/init.js +18 -0
  3. package/package.json +25 -11
  4. package/CHANGELOG.md +0 -485
  5. package/docs/AGENT_ROUTING.md +0 -335
  6. package/docs/DEAP_AGENT_GUIDE.en.md +0 -115
  7. package/docs/DEAP_AGENT_GUIDE.md +0 -115
  8. package/docs/images/dingtalk.svg +0 -1
  9. package/docs/images/image-1.png +0 -0
  10. package/docs/images/image-2.png +0 -0
  11. package/docs/images/image-3.png +0 -0
  12. package/docs/images/image-4.png +0 -0
  13. package/docs/images/image-5.png +0 -0
  14. package/docs/images/image-6.png +0 -0
  15. package/docs/images/image-7.png +0 -0
  16. package/install-beta.sh +0 -438
  17. package/install-npm.sh +0 -167
  18. package/openclaw.plugin.json +0 -498
  19. package/src/channel.ts +0 -463
  20. package/src/config/accounts.ts +0 -242
  21. package/src/config/schema.ts +0 -148
  22. package/src/core/connection.ts +0 -722
  23. package/src/core/message-handler.ts +0 -1700
  24. package/src/core/provider.ts +0 -111
  25. package/src/core/state.ts +0 -54
  26. package/src/directory.ts +0 -95
  27. package/src/docs.ts +0 -293
  28. package/src/gateway-methods.ts +0 -404
  29. package/src/onboarding.ts +0 -413
  30. package/src/policy.ts +0 -32
  31. package/src/probe.ts +0 -212
  32. package/src/reply-dispatcher.ts +0 -630
  33. package/src/runtime.ts +0 -32
  34. package/src/sdk/helpers.ts +0 -322
  35. package/src/sdk/types.ts +0 -513
  36. package/src/secret-input.ts +0 -19
  37. package/src/services/media/audio.ts +0 -54
  38. package/src/services/media/chunk-upload.ts +0 -296
  39. package/src/services/media/common.ts +0 -155
  40. package/src/services/media/file.ts +0 -70
  41. package/src/services/media/image.ts +0 -81
  42. package/src/services/media/index.ts +0 -10
  43. package/src/services/media/video.ts +0 -162
  44. package/src/services/media.ts +0 -1136
  45. package/src/services/messaging/card.ts +0 -342
  46. package/src/services/messaging/index.ts +0 -17
  47. package/src/services/messaging/send.ts +0 -141
  48. package/src/services/messaging.ts +0 -1013
  49. package/src/targets.ts +0 -45
  50. package/src/types/index.ts +0 -59
  51. package/src/utils/agent.ts +0 -63
  52. package/src/utils/async.ts +0 -51
  53. package/src/utils/constants.ts +0 -27
  54. package/src/utils/http-client.ts +0 -37
  55. package/src/utils/index.ts +0 -8
  56. package/src/utils/logger.ts +0 -78
  57. package/src/utils/session.ts +0 -147
  58. package/src/utils/token.ts +0 -93
  59. package/src/utils/utils-legacy.ts +0 -454
  60. package/tsconfig.json +0 -20
@@ -1,630 +0,0 @@
1
- // 类型定义
2
- interface ClawdbotConfig {
3
- [key: string]: any;
4
- }
5
-
6
- interface RuntimeEnv {
7
- log?: (...args: any[]) => void;
8
- error?: (...args: any[]) => void;
9
- warn?: (...args: any[]) => void;
10
- debug?: (...args: any[]) => void;
11
- info?: (...args: any[]) => void;
12
- [key: string]: any;
13
- }
14
-
15
- interface ReplyPayload {
16
- text?: string;
17
- [key: string]: any;
18
- }
19
-
20
- // ✅ 动态导入 channel-runtime 模块
21
- const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime") as any;
22
-
23
- const {
24
- createReplyPrefixOptions,
25
- createTypingCallbacks,
26
- logTypingFailure,
27
- } = channelRuntimeModule;
28
-
29
- import { createLoggerFromConfig } from "./utils/logger.ts";
30
- import { resolveDingtalkAccount } from "./config/accounts.ts";
31
- import { getDingtalkRuntime } from "./runtime.ts";
32
- import type { DingtalkConfig } from "./types/index.ts";
33
- import {
34
- createAICardForTarget,
35
- finishAICard,
36
- streamAICard,
37
- type AICardInstance,
38
- type AICardTarget,
39
- } from "./services/messaging/card.ts";
40
- import { sendMessage } from "./services/messaging.ts";
41
- import { getOapiAccessToken } from "./utils/token.ts";
42
- import {
43
- processLocalImages,
44
- processVideoMarkers,
45
- processAudioMarkers,
46
- processFileMarkers,
47
- } from "./services/media/index.ts";
48
-
49
-
50
- export type CreateDingtalkReplyDispatcherParams = {
51
- cfg: ClawdbotConfig;
52
- agentId: string;
53
- runtime: RuntimeEnv;
54
- conversationId: string;
55
- senderId: string;
56
- isDirect: boolean;
57
- accountId?: string;
58
- messageCreateTimeMs?: number;
59
- sessionWebhook: string;
60
- asyncMode?: boolean;
61
- /** 队列繁忙时预先创建的 AI Card,startStreaming 时直接复用而非新建 */
62
- preCreatedCard?: AICardInstance;
63
- };
64
-
65
- export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatcherParams) {
66
- const core = getDingtalkRuntime();
67
- const {
68
- cfg,
69
- agentId,
70
- conversationId,
71
- senderId,
72
- isDirect,
73
- accountId,
74
- sessionWebhook,
75
- asyncMode = false,
76
- preCreatedCard,
77
- } = params;
78
-
79
- const account = resolveDingtalkAccount({ cfg, accountId });
80
- const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
81
- cfg,
82
- agentId,
83
- channel: "dingtalk-connector",
84
- accountId,
85
- });
86
-
87
- // ✅ 读取 debug 配置
88
- const log = createLoggerFromConfig(account.config, `DingTalk:${accountId}`);
89
-
90
- // AI Card 状态管理
91
- let currentCardTarget: AICardTarget | null = null;
92
- let accumulatedText = "";
93
- const deliveredFinalTexts = new Set<string>();
94
-
95
- // 异步模式:累积完整响应
96
- let asyncModeFullResponse = "";
97
-
98
- // ✅ 节流控制:避免频繁调用钉钉 API 导致 QPS 限流
99
- let lastUpdateTime = 0;
100
- const updateInterval = 500; // 最小更新间隔 500ms(钉钉 QPS 限制:40 次/秒,保守起见设为 0.5 秒)
101
-
102
- // ✅ 错误兜底:防止重复发送错误消息
103
- const deliveredErrorTypes = new Set<string>();
104
- let lastErrorTime = 0;
105
- const ERROR_COOLDOWN = 60000; // 错误消息冷却时间 1 分钟
106
-
107
- // ============ 错误兜底函数 ============
108
-
109
- /**
110
- * 发送兜底错误消息,确保用户始终能收到反馈
111
- */
112
- const sendFallbackErrorMessage = async (
113
- errorType: 'mediaProcess' | 'sendMessage' | 'unknown',
114
- originalError?: string,
115
- forceSend: boolean = false
116
- ) => {
117
- const now = Date.now();
118
- const errorKey = `${errorType}:${conversationId}:${senderId}`;
119
-
120
- // 防止重复发送相同类型的错误消息
121
- if (!forceSend && deliveredErrorTypes.has(errorKey)) {
122
- log.debug(`[DingTalk][Fallback] 跳过重复错误消息:${errorType}`);
123
- return;
124
- }
125
-
126
- // 冷却时间控制
127
- if (!forceSend && now - lastErrorTime < ERROR_COOLDOWN) {
128
- log.debug(`[DingTalk][Fallback] 冷却时间内,跳过错误消息`);
129
- return;
130
- }
131
-
132
- const errorMessages = {
133
- mediaProcess: '⚠️ 媒体文件处理失败,已发送文字回复',
134
- sendMessage: '⚠️ 消息发送失败,请稍后重试',
135
- unknown: '⚠️ 抱歉,处理您的请求时出错,请稍后重试',
136
- };
137
-
138
- const errorMessage = errorMessages[errorType];
139
- log.warn(`[DingTalk][Fallback] ${errorMessage}, error: ${originalError}`);
140
-
141
- try {
142
- await sendMessage(
143
- account.config as DingtalkConfig,
144
- sessionWebhook,
145
- errorMessage,
146
- {
147
- useMarkdown: false,
148
- log: params.runtime.log,
149
- }
150
- );
151
- deliveredErrorTypes.add(errorKey);
152
- lastErrorTime = now;
153
- log.info(`[DingTalk][Fallback] ✅ 错误消息发送成功`);
154
- } catch (fallbackErr: any) {
155
- log.error(`[DingTalk][Fallback] ❌ 错误消息发送失败:${fallbackErr.message}`);
156
- }
157
- };
158
-
159
- // 打字指示器回调(钉钉暂不支持,预留接口)
160
- const typingCallbacks = createTypingCallbacks({
161
- start: async () => {
162
- // 钉钉暂不支持打字指示器
163
- },
164
- stop: async () => {
165
- // 钉钉暂不支持打字指示器
166
- },
167
- onStartError: (err: any) =>
168
- logTypingFailure({
169
- log: (message: any) => params.runtime.log?.(message),
170
- channel: "dingtalk-connector",
171
- action: "start",
172
- error: err,
173
- }),
174
- onStopError: (err: any) =>
175
- logTypingFailure({
176
- log: (message: any) => params.runtime.log?.(message),
177
- channel: "dingtalk-connector",
178
- action: "stop",
179
- error: err,
180
- }),
181
- });
182
-
183
- const textChunkLimit = core.channel.text.resolveTextChunkLimit(
184
- cfg,
185
- "dingtalk-connector",
186
- accountId,
187
- { fallbackLimit: 4000 }
188
- );
189
- const chunkMode = core.channel.text.resolveChunkMode(cfg, "dingtalk-connector");
190
-
191
- // 流式 AI Card 支持
192
- const streamingEnabled = (account.config as any)?.streaming !== false;
193
- // 用 Promise 保存 AI Card 的创建过程,避免 final 消息到达时轮询等待
194
- let cardCreationPromise: Promise<void> | null = null;
195
-
196
- const startStreaming = (): Promise<void> => {
197
- // 如果已经有创建中的 Promise,直接复用,避免并发创建
198
- if (cardCreationPromise) {
199
- return cardCreationPromise;
200
- }
201
- // 如果 AI Card 已存在,直接返回已完成的 Promise
202
- if (currentCardTarget) {
203
- return Promise.resolve();
204
- }
205
-
206
- cardCreationPromise = (async () => {
207
- // 异步模式下禁用流式 AI Card
208
- if (asyncMode) {
209
- log.info(`[DingTalk][startStreaming] 异步模式,跳过 AI Card 创建`);
210
- return;
211
- }
212
- if (!streamingEnabled) {
213
- log.info(`[DingTalk][startStreaming] 流式功能被禁用,跳过 AI Card 创建`);
214
- return;
215
- }
216
-
217
- // 若队列繁忙时已预先创建了 Card(显示排队 ACK 文案),直接复用,无需新建
218
- // 这样用户看到的是同一条消息从 ACK 文案更新为最终结果,而不是多出一条消息
219
- if (preCreatedCard) {
220
- log.info(`[DingTalk][startStreaming] 复用预创建 AI Card,cardInstanceId=${preCreatedCard.cardInstanceId}`);
221
- currentCardTarget = preCreatedCard as any;
222
- accumulatedText = "";
223
- return;
224
- }
225
-
226
- log.info(`[DingTalk][startStreaming] 开始创建 AI Card...`);
227
-
228
- try {
229
- const target: AICardTarget = isDirect
230
- ? { type: 'user', userId: senderId }
231
- : { type: 'group', openConversationId: conversationId };
232
-
233
- log.info(`[DingTalk][startStreaming] 目标:${JSON.stringify(target)}`);
234
-
235
- const card = await createAICardForTarget(
236
- account.config as DingtalkConfig,
237
- target,
238
- log
239
- );
240
- currentCardTarget = card as any;
241
- accumulatedText = "";
242
-
243
- if (card) {
244
- log.info(`[DingTalk][startStreaming] ✅ AI Card 创建成功`);
245
- } else {
246
- log.warn(`[DingTalk][startStreaming] AI Card 创建返回 null,静默降级到普通消息模式`);
247
- }
248
- } catch (error: any) {
249
- log.error(`[DingTalk][startStreaming] ❌ AI Card 创建失败:${error?.message || String(error)},静默降级到普通消息模式`);
250
- currentCardTarget = null;
251
- } finally {
252
- // 创建完成后清空 Promise,允许下次重新创建
253
- cardCreationPromise = null;
254
- }
255
- })();
256
-
257
- return cardCreationPromise;
258
- };
259
-
260
- const closeStreaming: () => Promise<void> = async () => {
261
- // 立即捕获并清空,防止并发调用重复执行(竞争条件保护)
262
- // closeStreaming 可能被 onIdle 和 onError 同时触发,若不在此处清空,
263
- // 第一次调用的 finally 块会将 currentCardTarget 置 null,
264
- // 导致第二次调用的 finishAICard 收到 null 参数而崩溃
265
- const cardSnapshot = currentCardTarget;
266
- if (!cardSnapshot) {
267
- log.info(`[DingTalk][closeStreaming] 无 AI Card,跳过关闭`);
268
- return;
269
- }
270
- currentCardTarget = null;
271
-
272
- log.info(`[DingTalk][closeStreaming] 开始关闭 AI Card...`);
273
-
274
- try {
275
- // 处理媒体标记
276
- let finalText = accumulatedText;
277
-
278
- // ✅ 如果累积的文本为空,使用默认提示文案
279
- if (!finalText.trim()) {
280
- finalText = '✅ 任务执行完成(无文本输出)';
281
- log.info(`[DingTalk][closeStreaming] 累积文本为空,使用默认提示文案`);
282
- }
283
-
284
- // 获取 oapiToken 用于媒体处理
285
- const oapiToken = await getOapiAccessToken(account.config as DingtalkConfig);
286
-
287
- // ✅ 构建正确的 target(单聊用 senderId,群聊用 conversationId)
288
- const target: AICardTarget = isDirect
289
- ? { type: 'user', userId: senderId }
290
- : { type: 'group', openConversationId: conversationId };
291
-
292
- log.info(`[DingTalk][closeStreaming] 开始处理媒体文件,target=${JSON.stringify(target)}`);
293
-
294
- if (oapiToken) {
295
- // 处理本地图片
296
- finalText = await processLocalImages(finalText, oapiToken, log);
297
-
298
- // ✅ 先处理 Markdown 标记格式的媒体文件
299
- finalText = await processVideoMarkers(
300
- finalText,
301
- '',
302
- account.config as DingtalkConfig,
303
- oapiToken,
304
- log,
305
- true, // ✅ 使用主动 API 模式
306
- target
307
- );
308
- finalText = await processAudioMarkers(
309
- finalText,
310
- '',
311
- account.config as DingtalkConfig,
312
- oapiToken,
313
- log,
314
- true, // ✅ 使用主动 API 模式
315
- target
316
- );
317
- finalText = await processFileMarkers(
318
- finalText,
319
- '',
320
- account.config as DingtalkConfig,
321
- oapiToken,
322
- log,
323
- true, // ✅ 使用主动 API 模式
324
- target
325
- );
326
-
327
- // ✅ 处理裸露的本地文件路径(绕过 OpenClaw SDK 的 bug)
328
- log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
329
- const { processRawMediaPaths } = await import('./services/media');
330
- finalText = await processRawMediaPaths(
331
- finalText,
332
- account.config as DingtalkConfig,
333
- oapiToken,
334
- log,
335
- target
336
- );
337
- log.info(`[DingTalk][closeStreaming] processRawMediaPaths 处理完成`);
338
- } else {
339
- log.warn(`[DingTalk][closeStreaming] oapiToken 为空,跳过媒体处理`);
340
- }
341
-
342
- log.info(`[DingTalk][closeStreaming] 准备调用 finishAICard,文本长度=${finalText.length}`);
343
- await finishAICard(
344
- cardSnapshot as any,
345
- finalText,
346
- account.config as DingtalkConfig,
347
- log
348
- );
349
- log.info(`[DingTalk][closeStreaming] ✅ AI Card 关闭成功`);
350
- } catch (error: any) {
351
- log.error(`[DingTalk][closeStreaming] ❌ AI Card 关闭失败:${error?.message || String(error)}`);
352
- // ✅ 媒体处理或关闭失败时,降级发送普通消息
353
- await sendFallbackErrorMessage('mediaProcess', error?.message || String(error));
354
-
355
- // 尝试用普通消息发送累积的文本
356
- if (accumulatedText.trim()) {
357
- try {
358
- log.info(`[DingTalk][closeStreaming] 降级发送普通消息`);
359
- await sendMessage(
360
- account.config as DingtalkConfig,
361
- sessionWebhook,
362
- accumulatedText,
363
- {
364
- useMarkdown: true,
365
- log: params.runtime.log,
366
- }
367
- );
368
- log.info(`[DingTalk][closeStreaming] ✅ 降级发送成功`);
369
- } catch (sendErr: any) {
370
- log.error(`[DingTalk][closeStreaming] ❌ 降级发送失败:${sendErr.message}`);
371
- }
372
- }
373
- } finally {
374
- // currentCardTarget 已在函数开头清空,此处只需重置累积文本
375
- accumulatedText = "";
376
- }
377
- };
378
-
379
- const { dispatcher, replyOptions, markDispatchIdle } =
380
- core.channel.reply.createReplyDispatcherWithTyping({
381
- ...prefixOptions,
382
- humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
383
- onReplyStart: () => {
384
- log.info(`[DingTalk][onReplyStart] 开始回复,流式 enabled=${streamingEnabled}`);
385
- // 每次 onReplyStart 都是全新的回复周期,清空去重集合
386
- deliveredFinalTexts.clear();
387
- if (streamingEnabled) {
388
- // fire-and-forget:提前创建 AI Card,onPartialReply 会等待创建完成
389
- void startStreaming();
390
- }
391
- typingCallbacks.onActive?.();
392
- },
393
- deliver: async (payload, info) => {
394
- let text = payload.text ?? "";
395
-
396
- log.info(`[DingTalk][deliver] 被调用:kind=${info?.kind}, textLength=${text.length}, hasText=${Boolean(text.trim())}`);
397
-
398
- // ✅ 在 final 响应时,先处理裸露的文件路径
399
- if (info?.kind === "final" && text.trim()) {
400
- const target: AICardTarget = isDirect
401
- ? { type: 'user', userId: senderId }
402
- : { type: 'group', openConversationId: conversationId };
403
-
404
- try {
405
- const oapiToken = await getOapiAccessToken(account.config as DingtalkConfig);
406
- if (oapiToken) {
407
- log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
408
- const { processRawMediaPaths } = await import('./services/media');
409
- text = await processRawMediaPaths(
410
- text,
411
- account.config as DingtalkConfig,
412
- oapiToken,
413
- log,
414
- target
415
- );
416
- log.info(`[DingTalk][deliver] 裸露文件路径处理完成`);
417
- }
418
- } catch (err: any) {
419
- log.error(`[DingTalk][deliver] 处理裸露文件路径失败:${err.message}`);
420
- }
421
- }
422
-
423
- const hasText = Boolean(text.trim());
424
- const skipTextForDuplicateFinal =
425
- info?.kind === "final" && hasText && deliveredFinalTexts.has(text);
426
-
427
- // ✅ 如果是 final 响应且没有文本,使用默认提示文案
428
- if (info?.kind === "final" && !hasText) {
429
- text = '✅ 任务执行完成(无文本输出)';
430
- log.info(`[DingTalk][deliver] final 响应无文本,使用默认提示文案`);
431
- }
432
-
433
- const shouldDeliverText = Boolean(text.trim()) && !skipTextForDuplicateFinal;
434
-
435
- if (!shouldDeliverText) {
436
- log.info(`[DingTalk][deliver] 跳过发送:hasText=${hasText}, skipTextForDuplicateFinal=${skipTextForDuplicateFinal}`);
437
- return;
438
- }
439
-
440
- // 异步模式:只累积响应,不发送
441
- if (asyncMode) {
442
- log.info(`[DingTalk][deliver] 异步模式,累积响应`);
443
- asyncModeFullResponse = text;
444
- return;
445
- }
446
-
447
- // block 消息:Agent 的中间 status update
448
- // 追加到同一张流式 AI Card 里(delta 模式),不单独创建新卡片
449
- // 如果流式 AI Card 未启用,直接丢弃 block(不发送)
450
- if (info?.kind === "block") {
451
- if (!streamingEnabled) {
452
- log.info(`[DingTalk][deliver] block 消息,流式未启用,丢弃`);
453
- return;
454
- }
455
- log.info(`[DingTalk][deliver] block 消息,追加到流式 AI Card,文本长度=${text.length}`);
456
- // 确保 AI Card 已创建(startStreaming 内部会复用已有的 cardCreationPromise)
457
- await startStreaming();
458
- // AI Card 已就绪,用 streamAICard 更新内容(仅展示当前 block 文本,不累积到 accumulatedText)
459
- // accumulatedText 专门给 onPartialReply 的流式更新使用,block 不能污染它
460
- if (currentCardTarget) {
461
- const now = Date.now();
462
- if (now - lastUpdateTime >= updateInterval) {
463
- try {
464
- await streamAICard(
465
- currentCardTarget as any,
466
- text,
467
- false,
468
- account.config as DingtalkConfig,
469
- log
470
- );
471
- lastUpdateTime = now;
472
- log.info(`[DingTalk][deliver] ✅ block 更新到 AI Card 成功`);
473
- } catch (streamErr: any) {
474
- log.error(`[DingTalk][deliver] ❌ block 更新 AI Card 失败:${streamErr.message}`);
475
- }
476
- }
477
- } else {
478
- log.warn(`[DingTalk][deliver] block 消息:AI Card 创建失败,丢弃该 block`);
479
- }
480
- return;
481
- }
482
-
483
- // 流式模式的 final 处理
484
- if (info?.kind === "final" && streamingEnabled) {
485
- log.info(`[DingTalk][deliver] final 响应,流式模式`);
486
- // await startStreaming() 确保 AI Card 创建完成后再处理 final
487
- await startStreaming();
488
-
489
- if (currentCardTarget) {
490
- // 直接用 final 的 text 覆盖 accumulatedText,确保 closeStreaming 用最终内容关闭卡片
491
- // 不能追加,因为 final text 本身就是完整的最终回复
492
- accumulatedText = text;
493
- log.info(`[DingTalk][deliver] 调用 closeStreaming 完成 AI Card`);
494
- await closeStreaming();
495
- deliveredFinalTexts.add(text);
496
- return;
497
- } else {
498
- log.warn(`[DingTalk][deliver] ⚠️ AI Card 创建失败,降级到非流式发送`);
499
- }
500
- }
501
-
502
- // 流式模式但没有 card target:降级到非流式发送
503
- // 或者非流式模式:使用普通消息发送
504
- if (info?.kind === "final") {
505
- log.info(`[DingTalk][deliver] 降级到非流式发送,文本长度=${text.length}`);
506
- try {
507
- for (const chunk of core.channel.text.chunkTextWithMode(
508
- text,
509
- textChunkLimit,
510
- chunkMode
511
- )) {
512
- await sendMessage(
513
- account.config as DingtalkConfig,
514
- sessionWebhook,
515
- chunk,
516
- {
517
- useMarkdown: true,
518
- log: params.runtime.log,
519
- }
520
- );
521
- }
522
- log.info(`[DingTalk][deliver] ✅ 非流式发送成功`);
523
- deliveredFinalTexts.add(text);
524
- } catch (error: any) {
525
- log.error(`[DingTalk][deliver] ❌ 非流式发送失败:${error.message}`);
526
- params.runtime.error?.(
527
- `dingtalk[${account.accountId}]: non-streaming delivery failed: ${String(error)}`
528
- );
529
- // ✅ 发送兜底错误消息
530
- await sendFallbackErrorMessage('sendMessage', error.message);
531
- }
532
- return;
533
- }
534
- },
535
- onError: async (error, info) => {
536
- log.error(`[DingTalk][onError] ${info.kind} reply failed: ${String(error)}`);
537
- params.runtime.error?.(
538
- `dingtalk[${account.accountId}] ${info.kind} reply failed: ${String(error)}`
539
- );
540
- await closeStreaming();
541
- typingCallbacks.onIdle?.();
542
- },
543
- onIdle: async () => {
544
- log.info(`[DingTalk][onIdle] 回复空闲,关闭 AI Card`);
545
- typingCallbacks.onIdle?.();
546
- await closeStreaming();
547
- },
548
- onCleanup: () => {
549
- log.info(`[DingTalk][onCleanup] 清理回调`);
550
- typingCallbacks.onCleanup?.();
551
- },
552
- });
553
-
554
- // 构建完整的 replyOptions:replyOptions 只包含 onReplyStart、onTypingController、onTypingCleanup
555
- // deliver、onError、onIdle、onCleanup 等回调已经在 createReplyDispatcherWithTyping 的参数中定义
556
- return {
557
- dispatcher,
558
- replyOptions: {
559
- ...replyOptions, // ✅ 包含 onReplyStart、onTypingController、onTypingCleanup
560
- onModelSelected,
561
- ...(streamingEnabled && {
562
- onPartialReply: async (payload: ReplyPayload) => {
563
- log.info(`[DingTalk][onPartialReply] 被调用,payload.text=${payload.text ? payload.text.length : 'null'}`);
564
- if (!payload.text) {
565
- log.debug(`[DingTalk][onPartialReply] 空文本,跳过`);
566
- return;
567
- }
568
-
569
- log.debug(`[DingTalk][onPartialReply] 收到部分响应,文本长度=${payload.text.length}`);
570
-
571
- // 异步模式下禁用流式更新
572
- if (asyncMode) {
573
- log.debug(`[DingTalk][onPartialReply] 异步模式,累积响应`);
574
- asyncModeFullResponse = payload.text;
575
- return;
576
- }
577
-
578
- // await startStreaming() 确保 AI Card 创建完成后再更新
579
- // startStreaming 内部会复用已有的 cardCreationPromise,不会重复创建
580
- await startStreaming();
581
-
582
- if (currentCardTarget) {
583
- accumulatedText = payload.text;
584
-
585
- const now = Date.now();
586
- if (now - lastUpdateTime >= updateInterval) {
587
- const { FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN, AUDIO_MARKER_PATTERN } = await import('./services/media/common.ts');
588
- const displayContent = accumulatedText
589
- .replace(FILE_MARKER_PATTERN, '')
590
- .replace(VIDEO_MARKER_PATTERN, '')
591
- .replace(AUDIO_MARKER_PATTERN, '')
592
- .trim();
593
-
594
- log.debug(`[DingTalk][onPartialReply] 更新 AI Card,显示文本长度=${displayContent.length}`);
595
-
596
- try {
597
- await streamAICard(
598
- currentCardTarget as any,
599
- displayContent,
600
- false,
601
- account.config as DingtalkConfig,
602
- log
603
- );
604
- lastUpdateTime = now;
605
- log.debug(`[DingTalk][onPartialReply] ✅ AI Card 更新成功`);
606
- } catch (err: any) {
607
- // 安全检查:确保 code 存在且为字符串
608
- const errorCode = err.response?.data?.code;
609
- if (err.response?.status === 403 && typeof errorCode === 'string' && errorCode.includes('QpsLimit')) {
610
- // QPS 限流,跳过本次更新
611
- log.warn(`[DingTalk][AICard] QPS 限流,跳过本次更新`);
612
- } else {
613
- log.error(`[DingTalk][onPartialReply] ❌ AI Card 更新失败:${err.message}`);
614
- // ✅ 发送兜底错误消息,但不抛出异常,避免中断后续处理
615
- await sendFallbackErrorMessage('sendMessage', err.message);
616
- }
617
- }
618
- } else {
619
- log.debug(`[DingTalk][onPartialReply] 节流控制,跳过本次更新(距离上次更新 ${now - lastUpdateTime}ms)`);
620
- }
621
- } else {
622
- log.warn(`[DingTalk][onPartialReply] ⚠️ AI Card 不存在,跳过更新`);
623
- }
624
- },
625
- }),
626
- },
627
- markDispatchIdle,
628
- getAsyncModeResponse: () => asyncModeFullResponse,
629
- };
630
- }
package/src/runtime.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
2
-
3
- /**
4
- * 自实现的运行时存储工厂,避免依赖特定版本 openclaw 是否导出 createPluginRuntimeStore。
5
- * 旧版 openclaw 没有导出该函数,直接 import 会导致 TypeError,因此在此处内联实现。
6
- */
7
- function createRuntimeStore<T>(errorMessage: string) {
8
- let runtimeValue: T | null = null;
9
-
10
- return {
11
- setRuntime: (next: T): void => {
12
- runtimeValue = next;
13
- },
14
- clearRuntime: (): void => {
15
- runtimeValue = null;
16
- },
17
- tryGetRuntime: (): T | null => {
18
- return runtimeValue;
19
- },
20
- getRuntime: (): T => {
21
- if (runtimeValue === null) {
22
- throw new Error(errorMessage);
23
- }
24
- return runtimeValue;
25
- },
26
- };
27
- }
28
-
29
- const { setRuntime: setDingtalkRuntime, getRuntime: getDingtalkRuntime } =
30
- createRuntimeStore<PluginRuntime>("DingTalk runtime not initialized");
31
-
32
- export { getDingtalkRuntime, setDingtalkRuntime };