@soimy/dingtalk 3.0.2 → 3.1.0

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/README.md CHANGED
@@ -439,6 +439,30 @@ openclaw gateway restart
439
439
  1. 检查 clientId 和 clientSecret 是否正确
440
440
  2. 确认网络可以访问钉钉 API
441
441
 
442
+ ### 错误 payload 日志规范(`[ErrorPayload]`)
443
+
444
+ 为便于快速定位 4xx/5xx 参数问题,插件会在 API 错误分支输出统一格式日志:
445
+
446
+ - 通用前缀:`[DingTalk][ErrorPayload][<scope>]`
447
+ - AI Card 前缀:`[DingTalk][AICard][ErrorPayload][<scope>]`
448
+ - 内容格式:`code=<...> message=<...> payload=<...>`(同时保留脱敏后的完整 payload)
449
+
450
+ 常见 scope 示例:
451
+
452
+ - `send.proactiveMessage` / `send.proactiveMedia` / `send.message`
453
+ - `outbound.sendText` / `outbound.sendMedia`
454
+ - `inbound.downloadMedia` / `inbound.cardFinalize`
455
+ - `card.create` / `card.stream` / `card.stream.retryAfterRefresh`
456
+ - `retry.beforeDecision`
457
+
458
+ 排查建议:
459
+
460
+ ```bash
461
+ openclaw logs | grep "\[ErrorPayload\]"
462
+ ```
463
+
464
+ 如果你看到 `code=invalidParameter`,通常优先检查请求 payload 的必填字段(例如 `robotCode`、`userIds`、`msgKey`、`msgParam`)是否完整且格式正确。
465
+
442
466
  ## 开发指南
443
467
 
444
468
  ### 首次设置
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soimy/dingtalk",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "DingTalk (钉钉) channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
@@ -3,6 +3,7 @@ import axios from "axios";
3
3
  import { getAccessToken } from "./auth";
4
4
  import { stripTargetPrefix } from "./config";
5
5
  import { resolveOriginalPeerId } from "./peer-id-registry";
6
+ import { formatDingTalkErrorPayloadLog } from "./utils";
6
7
  import type {
7
8
  AICardInstance,
8
9
  AICardStreamingRequest,
@@ -207,8 +208,12 @@ export async function createAICard(
207
208
  } catch (err: any) {
208
209
  log?.error?.(`[DingTalk][AICard] Create failed: ${err.message}`);
209
210
  if (err.response) {
211
+ const status = err.response.status;
212
+ const statusText = err.response.statusText;
213
+ const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
214
+ log?.error?.(`[DingTalk][AICard] Create error response${statusLabel}`);
210
215
  log?.error?.(
211
- `[DingTalk][AICard] Error response: status=${err.response.status} data=${JSON.stringify(err.response.data)}`,
216
+ formatDingTalkErrorPayloadLog("card.create", err.response.data, "[DingTalk][AICard]"),
212
217
  );
213
218
  }
214
219
  return null;
@@ -322,14 +327,28 @@ export async function streamAICard(
322
327
  return;
323
328
  } catch (retryErr: any) {
324
329
  log?.error?.(`[DingTalk][AICard] Retry after token refresh failed: ${retryErr.message}`);
330
+ if (retryErr.response?.data !== undefined) {
331
+ log?.error?.(
332
+ formatDingTalkErrorPayloadLog(
333
+ "card.stream.retryAfterRefresh",
334
+ retryErr.response.data,
335
+ "[DingTalk][AICard]",
336
+ ),
337
+ );
338
+ }
325
339
  }
326
340
  }
327
341
 
328
342
  card.state = AICardStatus.FAILED;
329
343
  card.lastUpdated = Date.now();
330
344
  log?.error?.(
331
- `[DingTalk][AICard] Streaming update failed: ${err.message}, resp=${JSON.stringify(err.response?.data)}`,
345
+ `[DingTalk][AICard] Streaming update failed: ${err.message}`,
332
346
  );
347
+ if (err.response?.data !== undefined) {
348
+ log?.error?.(
349
+ formatDingTalkErrorPayloadLog("card.stream", err.response.data, "[DingTalk][AICard]"),
350
+ );
351
+ }
333
352
  throw err;
334
353
  }
335
354
  }
package/src/channel.ts CHANGED
@@ -28,7 +28,7 @@ import type {
28
28
  ResolvedAccount,
29
29
  } from "./types";
30
30
  import { ConnectionState } from "./types";
31
- import { cleanupOrphanedTempFiles, getCurrentTimestamp } from "./utils";
31
+ import { cleanupOrphanedTempFiles, formatDingTalkErrorPayloadLog, getCurrentTimestamp } from "./utils";
32
32
 
33
33
  const processingDedupKeys = new Set<string>();
34
34
  const inboundCountersByAccount = new Map<
@@ -187,6 +187,9 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
187
187
  typeof result.error === "string" ? result.error : JSON.stringify(result.error),
188
188
  );
189
189
  } catch (err: any) {
190
+ if (err?.response?.data !== undefined) {
191
+ log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
192
+ }
190
193
  throw new Error(
191
194
  typeof err?.response?.data === "string"
192
195
  ? err.response.data
@@ -261,6 +264,9 @@ export const dingtalkPlugin: DingTalkChannelPlugin = {
261
264
  typeof result.error === "string" ? result.error : JSON.stringify(result.error),
262
265
  );
263
266
  } catch (err: any) {
267
+ if (err?.response?.data !== undefined) {
268
+ log?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
269
+ }
264
270
  throw new Error(
265
271
  typeof err?.response?.data === "string"
266
272
  ? err.response.data
@@ -20,7 +20,7 @@ import { getDingTalkRuntime } from "./runtime";
20
20
  import { sendBySession, sendMessage } from "./send-service";
21
21
  import type { DingTalkConfig, HandleDingTalkMessageParams, MediaFile } from "./types";
22
22
  import { AICardStatus } from "./types";
23
- import { maskSensitiveData } from "./utils";
23
+ import { formatDingTalkErrorPayloadLog, maskSensitiveData } from "./utils";
24
24
 
25
25
  /**
26
26
  * Download DingTalk media file via runtime media service (sandbox-compatible).
@@ -97,7 +97,9 @@ export async function downloadMedia(
97
97
  log.error(
98
98
  `[DingTalk] Failed to download media:${statusLabel}${code} message=${err.message}`,
99
99
  );
100
- if (dataDetail) {
100
+ if (err.response?.data !== undefined) {
101
+ log.error(formatDingTalkErrorPayloadLog("inbound.downloadMedia", err.response.data));
102
+ } else if (dataDetail) {
101
103
  log.error(`[DingTalk] downloadMedia response data: ${dataDetail}`);
102
104
  }
103
105
  } else {
@@ -168,6 +170,9 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
168
170
  );
169
171
  } catch (err: any) {
170
172
  log?.debug?.(`[DingTalk] Failed to send access denied message: ${err.message}`);
173
+ if (err?.response?.data !== undefined) {
174
+ log?.debug?.(formatDingTalkErrorPayloadLog("inbound.accessDeniedReply", err.response.data));
175
+ }
171
176
  }
172
177
 
173
178
  return;
@@ -202,6 +207,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
202
207
  );
203
208
  } catch (err: any) {
204
209
  log?.debug?.(`[DingTalk] Failed to send group access denied message: ${err.message}`);
210
+ if (err?.response?.data !== undefined) {
211
+ log?.debug?.(
212
+ formatDingTalkErrorPayloadLog("inbound.groupAccessDeniedReply", err.response.data),
213
+ );
214
+ }
205
215
  }
206
216
 
207
217
  return;
@@ -355,6 +365,9 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
355
365
  }
356
366
  } catch (err: any) {
357
367
  log?.debug?.(`[DingTalk] Thinking message failed: ${err.message}`);
368
+ if (err?.response?.data !== undefined) {
369
+ log?.debug?.(formatDingTalkErrorPayloadLog("inbound.thinkingMessage", err.response.data));
370
+ }
358
371
  }
359
372
  }
360
373
 
@@ -370,6 +383,11 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
370
383
  return;
371
384
  }
372
385
 
386
+ if (useCardMode && currentAICard && info?.kind === "final") {
387
+ lastCardContent = textToSend;
388
+ return;
389
+ }
390
+
373
391
  // Tool outputs are rendered into card stream as a separate formatted block.
374
392
  if (useCardMode && currentAICard && info?.kind === "tool") {
375
393
  if (isCardInTerminalState(currentAICard.state)) {
@@ -398,6 +416,9 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
398
416
  });
399
417
  } catch (err: any) {
400
418
  log?.error?.(`[DingTalk] Reply failed: ${err.message}`);
419
+ if (err?.response?.data !== undefined) {
420
+ log?.error?.(formatDingTalkErrorPayloadLog("inbound.replyDeliver", err.response.data));
421
+ }
401
422
  throw err;
402
423
  }
403
424
  },
@@ -422,6 +443,9 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
422
443
  await streamAICard(currentAICard, thinkingText, false, log);
423
444
  } catch (err: any) {
424
445
  log?.debug?.(`[DingTalk] Thinking stream update failed: ${err.message}`);
446
+ if (err?.response?.data !== undefined) {
447
+ log?.debug?.(formatDingTalkErrorPayloadLog("inbound.thinkingStream", err.response.data));
448
+ }
425
449
  }
426
450
  },
427
451
  },
@@ -460,6 +484,9 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams)
460
484
  }
461
485
  } catch (err: any) {
462
486
  log?.debug?.(`[DingTalk] AI Card finalization failed: ${err.message}`);
487
+ if (err?.response?.data !== undefined) {
488
+ log?.debug?.(formatDingTalkErrorPayloadLog("inbound.cardFinalize", err.response.data));
489
+ }
463
490
  try {
464
491
  if (currentAICard.state !== AICardStatus.FINISHED) {
465
492
  currentAICard.state = AICardStatus.FAILED;
@@ -11,6 +11,7 @@ import * as path from "path";
11
11
  import axios from "axios";
12
12
  import FormData from "form-data";
13
13
  import type { DingTalkConfig, Logger } from "./types";
14
+ import { formatDingTalkErrorPayloadLog } from "./utils";
14
15
 
15
16
  export type DingTalkMediaType = "image" | "voice" | "video" | "file";
16
17
 
@@ -123,7 +124,11 @@ export async function uploadMedia(
123
124
  } else {
124
125
  log?.error?.(`[DingTalk] Failed to upload media: ${err.message}`);
125
126
  if (axios.isAxiosError(err) && err.response) {
126
- log?.error?.(`[DingTalk] Upload response: ${JSON.stringify(err.response.data)}`);
127
+ const status = err.response.status;
128
+ const statusText = err.response.statusText;
129
+ const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
130
+ log?.error?.(`[DingTalk] Upload response${statusLabel}`);
131
+ log?.error?.(formatDingTalkErrorPayloadLog("media.upload", err.response.data));
127
132
  }
128
133
  }
129
134
  return null;
@@ -13,6 +13,7 @@ import { getLogger } from "./logger-context";
13
13
  import { uploadMedia as uploadMediaUtil } from "./media-utils";
14
14
  import { detectMarkdownAndExtractTitle } from "./message-utils";
15
15
  import { resolveOriginalPeerId } from "./peer-id-registry";
16
+ import { formatDingTalkErrorPayloadLog } from "./utils";
16
17
  import type {
17
18
  AxiosResponse,
18
19
  DingTalkConfig,
@@ -43,8 +44,8 @@ export async function sendProactiveTextOrMarkdown(
43
44
  text: string,
44
45
  options: SendMessageOptions = {},
45
46
  ): Promise<AxiosResponse> {
46
- const token = await getAccessToken(config, options.log);
47
47
  const log = options.log || getLogger();
48
+ const token = await getAccessToken(config, log);
48
49
 
49
50
  // Support group:/user: prefix and restore original case-sensitive conversationId.
50
51
  const { targetId, isExplicitUser } = stripTargetPrefix(target);
@@ -79,13 +80,40 @@ export async function sendProactiveTextOrMarkdown(
79
80
  payload.userIds = [resolvedTarget];
80
81
  }
81
82
 
82
- const result = await axios({
83
- url,
84
- method: "POST",
85
- data: payload,
86
- headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
87
- });
88
- return result.data;
83
+ try {
84
+ const result = await axios({
85
+ url,
86
+ method: "POST",
87
+ data: payload,
88
+ headers: { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json" },
89
+ });
90
+ return result.data;
91
+ } catch (err: unknown) {
92
+ const maybeAxiosError = err as {
93
+ response?: { status?: number; statusText?: string; data?: unknown };
94
+ message?: string;
95
+ };
96
+ if (maybeAxiosError?.response) {
97
+ const status = maybeAxiosError.response.status;
98
+ const statusText = maybeAxiosError.response.statusText;
99
+ const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
100
+ log?.error?.(
101
+ `[DingTalk] Failed to send proactive message:${statusLabel} message=${
102
+ maybeAxiosError.message || String(err)
103
+ }`,
104
+ );
105
+ if (maybeAxiosError.response.data !== undefined) {
106
+ log?.error?.(
107
+ formatDingTalkErrorPayloadLog("send.proactiveMessage", maybeAxiosError.response.data),
108
+ );
109
+ }
110
+ } else if (err instanceof Error) {
111
+ log?.error?.(`[DingTalk] Failed to send proactive message: ${err.message}`);
112
+ } else {
113
+ log?.error?.(`[DingTalk] Failed to send proactive message: ${String(err)}`);
114
+ }
115
+ throw err;
116
+ }
89
117
  }
90
118
 
91
119
  export async function sendProactiveMedia(
@@ -161,7 +189,11 @@ export async function sendProactiveMedia(
161
189
  } catch (err: any) {
162
190
  log?.error?.(`[DingTalk] Failed to send proactive media: ${err.message}`);
163
191
  if (axios.isAxiosError(err) && err.response) {
164
- log?.error?.(`[DingTalk] Response: ${JSON.stringify(err.response.data)}`);
192
+ const status = err.response.status;
193
+ const statusText = err.response.statusText;
194
+ const statusLabel = status ? ` status=${status}${statusText ? ` ${statusText}` : ""}` : "";
195
+ log?.error?.(`[DingTalk] Proactive media response${statusLabel}`);
196
+ log?.error?.(formatDingTalkErrorPayloadLog("send.proactiveMedia", err.response.data));
165
197
  }
166
198
  return { ok: false, error: err.message };
167
199
  }
@@ -276,6 +308,9 @@ export async function sendMessage(
276
308
  return { ok: true, data: result };
277
309
  } catch (err: any) {
278
310
  options.log?.error?.(`[DingTalk] Send message failed: ${err.message}`);
311
+ if (err?.response?.data !== undefined) {
312
+ options.log?.error?.(formatDingTalkErrorPayloadLog("send.message", err.response.data));
313
+ }
279
314
  return { ok: false, error: err.message };
280
315
  }
281
316
  }
package/src/utils.ts CHANGED
@@ -38,6 +38,55 @@ export function maskSensitiveData(data: unknown): any {
38
38
  return masked;
39
39
  }
40
40
 
41
+ export function formatDingTalkErrorPayload(payload: unknown): string {
42
+ if (payload === null || payload === undefined) {
43
+ return "payload=unknown";
44
+ }
45
+
46
+ let code: string | undefined;
47
+ let message: string | undefined;
48
+ if (typeof payload === "object" && !Array.isArray(payload)) {
49
+ const obj = payload as Record<string, unknown>;
50
+ if (typeof obj.code === "string" || typeof obj.code === "number") {
51
+ code = String(obj.code);
52
+ }
53
+ if (typeof obj.message === "string") {
54
+ message = obj.message;
55
+ }
56
+ }
57
+
58
+ let serialized: string;
59
+ try {
60
+ serialized = JSON.stringify(maskSensitiveData(payload));
61
+ } catch {
62
+ if (typeof payload === "string") {
63
+ serialized = payload;
64
+ } else if (typeof payload === "number" || typeof payload === "boolean" || typeof payload === "bigint") {
65
+ serialized = `${payload}`;
66
+ } else {
67
+ serialized = "[unserializable-payload]";
68
+ }
69
+ }
70
+
71
+ const parts: string[] = [];
72
+ if (code) {
73
+ parts.push(`code=${code}`);
74
+ }
75
+ if (message) {
76
+ parts.push(`message=${message}`);
77
+ }
78
+ parts.push(`payload=${serialized}`);
79
+ return parts.join(" ");
80
+ }
81
+
82
+ export function formatDingTalkErrorPayloadLog(
83
+ scope: string,
84
+ payload: unknown,
85
+ prefix: "[DingTalk]" | "[DingTalk][AICard]" = "[DingTalk]",
86
+ ): string {
87
+ return `${prefix}[ErrorPayload][${scope}] ${formatDingTalkErrorPayload(payload)}`;
88
+ }
89
+
41
90
  /**
42
91
  * Cleanup orphaned temp files from dingtalk media
43
92
  * Run at startup to clean up files from crashed processes
@@ -98,6 +147,10 @@ export async function retryWithBackoff<T>(
98
147
  const isRetryable =
99
148
  statusCode === 401 || statusCode === 429 || (statusCode && statusCode >= 500);
100
149
 
150
+ if (err.response?.data !== undefined) {
151
+ log?.debug?.(formatDingTalkErrorPayloadLog("retry.beforeDecision", err.response.data));
152
+ }
153
+
101
154
  if (!isRetryable || attempt === maxRetries) {
102
155
  throw err;
103
156
  }