@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 +24 -0
- package/package.json +1 -1
- package/src/card-service.ts +21 -2
- package/src/channel.ts +7 -1
- package/src/inbound-handler.ts +29 -2
- package/src/media-utils.ts +6 -1
- package/src/send-service.ts +44 -9
- package/src/utils.ts +53 -0
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
package/src/card-service.ts
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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
|
package/src/inbound-handler.ts
CHANGED
|
@@ -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 (
|
|
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;
|
package/src/media-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/send-service.ts
CHANGED
|
@@ -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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
}
|