@soimy/dingtalk 3.5.3 → 3.6.1
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 +4 -1
- package/index.ts +7 -0
- package/openclaw.plugin.json +153 -5
- package/package.json +1 -1
- package/src/auth.ts +5 -2
- package/src/card/card-markdown-image-reroute.ts +106 -0
- package/src/card/card-run-registry.ts +54 -1
- package/src/card/card-stop-handler.ts +10 -20
- package/src/card/card-template.ts +14 -3
- package/src/card/statusline-renderer.ts +94 -0
- package/src/card-draft-controller.ts +245 -52
- package/src/card-service.ts +408 -23
- package/src/channel.ts +24 -1083
- package/src/config-schema.ts +21 -1
- package/src/config.ts +139 -66
- package/src/device-registration.ts +245 -0
- package/src/gateway/channel-gateway.ts +637 -0
- package/src/inbound-handler.ts +1276 -975
- package/src/media-utils.ts +6 -0
- package/src/message-context-store.ts +183 -85
- package/src/message-utils.ts +124 -16
- package/src/messaging/btw-deliver.ts +85 -0
- package/src/messaging/channel-actions.ts +174 -0
- package/src/messaging/channel-outbound.ts +158 -0
- package/src/onboarding.ts +333 -235
- package/src/path-utils.ts +49 -0
- package/src/platform/channel-status.ts +81 -0
- package/src/reply-strategy-card.ts +373 -64
- package/src/reply-strategy-markdown.ts +1 -1
- package/src/reply-strategy-types.ts +93 -0
- package/src/reply-strategy-with-reaction.ts +1 -1
- package/src/reply-strategy.ts +14 -72
- package/src/run-usage-store.ts +59 -0
- package/src/secret-input.ts +216 -0
- package/src/send-service.ts +115 -3
- package/src/session-state.ts +62 -0
- package/src/targeting/agent-name-matcher.ts +28 -0
- package/src/targeting/agent-routing.ts +30 -5
- package/src/types.ts +48 -157
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sendMessage } from "../send-service";
|
|
2
|
+
import type { DingTalkConfig, Logger } from "../types";
|
|
3
|
+
|
|
4
|
+
const MAX_QUESTION_LENGTH = 80;
|
|
5
|
+
const LEADING_MENTIONS_RE = /^(?:@\S+\s+)*/u;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Strip leading `@mention` tokens from inbound text. Used by both the abort and
|
|
9
|
+
* BTW bypass branches in `inbound-handler.ts` so that command detection works
|
|
10
|
+
* uniformly in DM and group chats.
|
|
11
|
+
*/
|
|
12
|
+
export function stripLeadingMentions(text: string): string {
|
|
13
|
+
return text.replace(LEADING_MENTIONS_RE, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildBtwBlockquote(senderName: string, rawQuestion: string): string {
|
|
17
|
+
const stripped = stripLeadingMentions(rawQuestion);
|
|
18
|
+
// Iterate by Unicode code points (not UTF-16 code units) so emoji /
|
|
19
|
+
// surrogate pairs aren't sliced in half at the truncation boundary.
|
|
20
|
+
const codePoints = Array.from(stripped);
|
|
21
|
+
const truncated =
|
|
22
|
+
codePoints.length > MAX_QUESTION_LENGTH
|
|
23
|
+
? `${codePoints.slice(0, MAX_QUESTION_LENGTH).join("")}…`
|
|
24
|
+
: stripped;
|
|
25
|
+
const senderPrefix = senderName ? `${senderName}: ` : "";
|
|
26
|
+
return `> ${senderPrefix}${truncated}\n\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DeliverBtwReplyArgs {
|
|
30
|
+
config: DingTalkConfig;
|
|
31
|
+
sessionWebhook: string | undefined;
|
|
32
|
+
conversationId: string;
|
|
33
|
+
to: string;
|
|
34
|
+
senderName: string;
|
|
35
|
+
rawQuestion: string;
|
|
36
|
+
replyText: string;
|
|
37
|
+
log: Logger | undefined;
|
|
38
|
+
accountId?: string;
|
|
39
|
+
storePath?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Deliver a BTW reply through the unified `sendMessage` entry point.
|
|
44
|
+
*
|
|
45
|
+
* BTW is a special inbound trigger, but the *outbound* reply is still a regular
|
|
46
|
+
* markdown/text message and must inherit the standard send-service semantics:
|
|
47
|
+
* persistence into the message context store, delivery metadata tracking, and
|
|
48
|
+
* the single `{ ok, error, ... }` contract. We pass `forceMarkdown: true` so
|
|
49
|
+
* that `sendMessage` skips the card branch even when the channel is configured
|
|
50
|
+
* for card mode — BTW must never create or touch an AI Card (see CLAUDE.md
|
|
51
|
+
* anti-pattern: "Do not create multiple active AI Cards for the same
|
|
52
|
+
* `accountId:conversationId`").
|
|
53
|
+
*
|
|
54
|
+
* When `sessionWebhook` is present `sendMessage` internally dispatches via
|
|
55
|
+
* `sendBySession`; otherwise it falls back to the proactive text/markdown API.
|
|
56
|
+
* Either way the caller sees the same return shape, and failures propagate as
|
|
57
|
+
* `{ ok: false }` instead of being silently swallowed.
|
|
58
|
+
*/
|
|
59
|
+
export async function deliverBtwReply(
|
|
60
|
+
args: DeliverBtwReplyArgs,
|
|
61
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
62
|
+
const blockquote = buildBtwBlockquote(args.senderName, args.rawQuestion);
|
|
63
|
+
const fullText = `${blockquote}${args.replyText}`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await sendMessage(args.config, args.to, fullText, {
|
|
67
|
+
log: args.log,
|
|
68
|
+
accountId: args.accountId,
|
|
69
|
+
storePath: args.storePath,
|
|
70
|
+
conversationId: args.conversationId,
|
|
71
|
+
sessionWebhook: args.sessionWebhook,
|
|
72
|
+
forceMarkdown: true,
|
|
73
|
+
});
|
|
74
|
+
if (!result.ok) {
|
|
75
|
+
args.log?.warn?.(
|
|
76
|
+
`[DingTalk] BTW reply delivery returned not-ok: ${result.error ?? "unknown"}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return { ok: result.ok, error: result.error };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
82
|
+
args.log?.warn?.(`[DingTalk] BTW reply delivery threw: ${error}`);
|
|
83
|
+
return { ok: false, error };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { jsonResult } from "openclaw/plugin-sdk/channel-actions";
|
|
2
|
+
import type { ChannelMessageActionAdapter } from "openclaw/plugin-sdk/channel-contract";
|
|
3
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
|
+
import { readStringParam } from "openclaw/plugin-sdk/param-readers";
|
|
5
|
+
import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
|
|
6
|
+
import { getConfig, stripTargetPrefix } from "../config";
|
|
7
|
+
import { getLogger } from "../logger-context";
|
|
8
|
+
import { resolveOriginalPeerId } from "../peer-id-registry";
|
|
9
|
+
import { hasConfiguredSecretInput } from "../secret-input";
|
|
10
|
+
import { sendMedia, sendMessage } from "../send-service";
|
|
11
|
+
import { parseBooleanLike } from "../utils";
|
|
12
|
+
|
|
13
|
+
function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
14
|
+
return parseBooleanLike(params[key]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readSharedAudioAsVoiceParam(params: Record<string, unknown>): boolean {
|
|
18
|
+
const sharedValue = readBooleanLikeParam(params, "audioAsVoice");
|
|
19
|
+
if (sharedValue !== undefined) {
|
|
20
|
+
return sharedValue;
|
|
21
|
+
}
|
|
22
|
+
return readBooleanLikeParam(params, "asVoice") === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveConversationIdFromSessionKey(sessionKey?: string | null): string | undefined {
|
|
26
|
+
const trimmed = sessionKey?.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const markers = [":group:", ":channel:", ":direct:"] as const;
|
|
32
|
+
const marker = markers.find((candidate) => trimmed.includes(candidate));
|
|
33
|
+
if (!marker) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const suffix = trimmed.slice(trimmed.indexOf(marker) + marker.length);
|
|
38
|
+
const threadMarker = ":topic:";
|
|
39
|
+
const threadIndex = suffix.indexOf(threadMarker);
|
|
40
|
+
const conversationId = (threadIndex >= 0 ? suffix.slice(0, threadIndex) : suffix).trim();
|
|
41
|
+
return conversationId || undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function inferCardOwnerIdFromTarget(target: string): string | undefined {
|
|
45
|
+
const trimmed = target.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("cid")) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function describeDingTalkMessageTool(cfg: OpenClawConfig) {
|
|
53
|
+
const config = getConfig(cfg);
|
|
54
|
+
const configured = Boolean(config.clientId && hasConfiguredSecretInput(config.clientSecret));
|
|
55
|
+
if (!configured && !(config.accounts && Object.keys(config.accounts).length > 0)) {
|
|
56
|
+
return { actions: [], capabilities: [], schema: null };
|
|
57
|
+
}
|
|
58
|
+
const hasCardMode =
|
|
59
|
+
config.messageType === "card" ||
|
|
60
|
+
(config.accounts && Object.values(config.accounts).some((account) => account?.messageType === "card"));
|
|
61
|
+
return {
|
|
62
|
+
actions: ["send"] as const,
|
|
63
|
+
capabilities: hasCardMode ? (["cards"] as const) : [],
|
|
64
|
+
schema: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createDingTalkMessageActions(): ChannelMessageActionAdapter {
|
|
69
|
+
return {
|
|
70
|
+
describeMessageTool: ({ cfg }) => describeDingTalkMessageTool(cfg),
|
|
71
|
+
supportsAction: ({ action }) => action === "send",
|
|
72
|
+
extractToolSend: ({ args }) => extractToolSend(args, "sendMessage"),
|
|
73
|
+
handleAction: async ({ action, params, cfg, accountId, dryRun, mediaLocalRoots, sessionKey }) => {
|
|
74
|
+
if (action !== "send") {
|
|
75
|
+
throw new Error(`Action ${action} is not supported for provider dingtalk.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const to = readStringParam(params, "to", { required: true });
|
|
79
|
+
const mediaInput =
|
|
80
|
+
readStringParam(params, "media", { trim: false }) ??
|
|
81
|
+
readStringParam(params, "path", { trim: false }) ??
|
|
82
|
+
readStringParam(params, "filePath", { trim: false }) ??
|
|
83
|
+
readStringParam(params, "mediaUrl", { trim: false });
|
|
84
|
+
|
|
85
|
+
const hasMedia = Boolean(mediaInput && mediaInput.trim());
|
|
86
|
+
const caption = readStringParam(params, "caption", { allowEmpty: true }) ?? "";
|
|
87
|
+
let message =
|
|
88
|
+
readStringParam(params, "message", {
|
|
89
|
+
required: !hasMedia,
|
|
90
|
+
allowEmpty: true,
|
|
91
|
+
}) ?? "";
|
|
92
|
+
|
|
93
|
+
if (!message.trim() && caption.trim()) {
|
|
94
|
+
message = caption;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const asVoice = readSharedAudioAsVoiceParam(params);
|
|
98
|
+
const requestedMediaType = readStringParam(params, "mediaType") as
|
|
99
|
+
| "image"
|
|
100
|
+
| "voice"
|
|
101
|
+
| "video"
|
|
102
|
+
| "file"
|
|
103
|
+
| undefined;
|
|
104
|
+
|
|
105
|
+
const target = resolveOriginalPeerId(stripTargetPrefix(to).targetId);
|
|
106
|
+
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
return jsonResult({
|
|
109
|
+
ok: true,
|
|
110
|
+
dryRun: true,
|
|
111
|
+
to: target,
|
|
112
|
+
hasMedia,
|
|
113
|
+
asVoice,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const log = getLogger();
|
|
118
|
+
const config = getConfig(cfg, accountId ?? undefined);
|
|
119
|
+
|
|
120
|
+
if (hasMedia && mediaInput) {
|
|
121
|
+
const conversationId = resolveConversationIdFromSessionKey(sessionKey) ?? target;
|
|
122
|
+
const expectedCardOwnerId =
|
|
123
|
+
readStringParam(params, "expectedCardOwnerId") ?? inferCardOwnerIdFromTarget(target);
|
|
124
|
+
const result = await sendMedia(config, target, mediaInput, {
|
|
125
|
+
log,
|
|
126
|
+
accountId: accountId ?? undefined,
|
|
127
|
+
conversationId,
|
|
128
|
+
mediaType: requestedMediaType ?? undefined,
|
|
129
|
+
audioAsVoice: asVoice,
|
|
130
|
+
mediaLocalRoots: mediaLocalRoots ? [...mediaLocalRoots] : undefined,
|
|
131
|
+
expectedCardOwnerId: expectedCardOwnerId ?? undefined,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
throw new Error(result.error || "send media failed");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return jsonResult({
|
|
139
|
+
ok: true,
|
|
140
|
+
to: target,
|
|
141
|
+
messageId: result.messageId ?? null,
|
|
142
|
+
result: result.data ?? null,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (asVoice) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
"DingTalk send with asVoice requires media/path/filePath/mediaUrl pointing to an audio file.",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!message.trim()) {
|
|
153
|
+
throw new Error("send requires message when media is not provided");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await sendMessage(config, target, message, {
|
|
157
|
+
log,
|
|
158
|
+
accountId: accountId ?? undefined,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!result.ok) {
|
|
162
|
+
throw new Error(result.error || "send message failed");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const data = result.data as any;
|
|
166
|
+
return jsonResult({
|
|
167
|
+
ok: true,
|
|
168
|
+
to: target,
|
|
169
|
+
messageId: data?.processQueryKey || data?.messageId || null,
|
|
170
|
+
result: data ?? null,
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getConfig } from "../config";
|
|
3
|
+
import { getLogger } from "../logger-context";
|
|
4
|
+
import { getDingTalkRuntime } from "../runtime";
|
|
5
|
+
import { sendMedia, sendMessage } from "../send-service";
|
|
6
|
+
import { normalizeResolvedDingTalkTarget } from "../targeting/target-directory-adapter";
|
|
7
|
+
import type { DingTalkChannelPlugin } from "../types";
|
|
8
|
+
import { formatDingTalkErrorPayloadLog, parseBooleanLike } from "../utils";
|
|
9
|
+
|
|
10
|
+
function readBooleanLikeParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
11
|
+
return parseBooleanLike(params[key]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readSharedAudioAsVoiceParam(params: Record<string, unknown>): boolean {
|
|
15
|
+
const sharedValue = readBooleanLikeParam(params, "audioAsVoice");
|
|
16
|
+
if (sharedValue !== undefined) {
|
|
17
|
+
return sharedValue;
|
|
18
|
+
}
|
|
19
|
+
return readBooleanLikeParam(params, "asVoice") === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createDingTalkOutbound(): NonNullable<DingTalkChannelPlugin["outbound"]> {
|
|
23
|
+
return {
|
|
24
|
+
deliveryMode: "direct",
|
|
25
|
+
resolveTarget: ({ to }: any) => {
|
|
26
|
+
const trimmed = to?.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false as const,
|
|
30
|
+
error: new Error("DingTalk message requires --to <conversationId>"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { ok: true as const, to: normalizeResolvedDingTalkTarget(trimmed) };
|
|
34
|
+
},
|
|
35
|
+
sendText: async ({ cfg, to, text, accountId, log }: any) => {
|
|
36
|
+
const config = getConfig(cfg, accountId);
|
|
37
|
+
const runtime = getDingTalkRuntime();
|
|
38
|
+
const storePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
|
|
39
|
+
agentId: accountId,
|
|
40
|
+
});
|
|
41
|
+
const effectiveLog = getLogger(accountId) || log;
|
|
42
|
+
try {
|
|
43
|
+
const result = await sendMessage(config, to, text, {
|
|
44
|
+
log: effectiveLog,
|
|
45
|
+
accountId,
|
|
46
|
+
storePath,
|
|
47
|
+
conversationId: to,
|
|
48
|
+
});
|
|
49
|
+
effectiveLog?.debug?.(`[DingTalk] sendText: "${text}" result: ${JSON.stringify(result)}`);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
throw new Error(result.error || "sendText failed");
|
|
52
|
+
}
|
|
53
|
+
const data = result.data as any;
|
|
54
|
+
const messageId = String(data?.processQueryKey || data?.messageId || randomUUID());
|
|
55
|
+
const meta =
|
|
56
|
+
result.data || result.tracking
|
|
57
|
+
? {
|
|
58
|
+
...(result.data ? { data: result.data as unknown as Record<string, unknown> } : {}),
|
|
59
|
+
...(result.tracking ? { tracking: result.tracking } : {}),
|
|
60
|
+
}
|
|
61
|
+
: undefined;
|
|
62
|
+
return {
|
|
63
|
+
channel: "dingtalk",
|
|
64
|
+
messageId,
|
|
65
|
+
meta,
|
|
66
|
+
};
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
if (err?.response?.data !== undefined) {
|
|
69
|
+
effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendText", err.response.data));
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
typeof err?.response?.data === "string"
|
|
73
|
+
? err.response.data
|
|
74
|
+
: err?.message || "sendText failed",
|
|
75
|
+
{ cause: err },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
sendMedia: async ({
|
|
80
|
+
cfg,
|
|
81
|
+
to,
|
|
82
|
+
mediaPath,
|
|
83
|
+
filePath,
|
|
84
|
+
mediaUrl,
|
|
85
|
+
mediaType: providedMediaType,
|
|
86
|
+
audioAsVoice,
|
|
87
|
+
asVoice,
|
|
88
|
+
accountId,
|
|
89
|
+
mediaLocalRoots,
|
|
90
|
+
log,
|
|
91
|
+
expectedCardOwnerId,
|
|
92
|
+
}: any) => {
|
|
93
|
+
const config = getConfig(cfg, accountId);
|
|
94
|
+
const runtime = getDingTalkRuntime();
|
|
95
|
+
const storePath = runtime.channel.session.resolveStorePath(cfg.session?.store, {
|
|
96
|
+
agentId: accountId,
|
|
97
|
+
});
|
|
98
|
+
const effectiveLog = getLogger(accountId) || log;
|
|
99
|
+
if (!config.clientId) {
|
|
100
|
+
throw new Error("DingTalk not configured");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rawMediaPath = mediaPath || filePath || mediaUrl;
|
|
104
|
+
if (!rawMediaPath) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`mediaPath, filePath, or mediaUrl is required. Received: ${JSON.stringify({
|
|
107
|
+
to,
|
|
108
|
+
mediaPath,
|
|
109
|
+
filePath,
|
|
110
|
+
mediaUrl,
|
|
111
|
+
})}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const requestedMediaType = typeof providedMediaType === "string"
|
|
116
|
+
? (providedMediaType as "image" | "voice" | "video" | "file")
|
|
117
|
+
: undefined;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await sendMedia(config, to, rawMediaPath, {
|
|
121
|
+
log: effectiveLog,
|
|
122
|
+
accountId,
|
|
123
|
+
storePath,
|
|
124
|
+
conversationId: to,
|
|
125
|
+
mediaType: requestedMediaType,
|
|
126
|
+
audioAsVoice: readSharedAudioAsVoiceParam({ audioAsVoice, asVoice }),
|
|
127
|
+
mediaLocalRoots,
|
|
128
|
+
expectedCardOwnerId,
|
|
129
|
+
});
|
|
130
|
+
effectiveLog?.debug?.(`[DingTalk] sendMedia result: ${JSON.stringify(result)}`);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
throw new Error(result.error || "sendMedia failed");
|
|
133
|
+
}
|
|
134
|
+
const data = result.data;
|
|
135
|
+
const messageId = String(
|
|
136
|
+
result.messageId || data?.processQueryKey || data?.messageId || randomUUID(),
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
channel: "dingtalk",
|
|
140
|
+
messageId,
|
|
141
|
+
meta: result.data
|
|
142
|
+
? { data: result.data as unknown as Record<string, unknown> }
|
|
143
|
+
: undefined,
|
|
144
|
+
};
|
|
145
|
+
} catch (err: any) {
|
|
146
|
+
if (err?.response?.data !== undefined) {
|
|
147
|
+
effectiveLog?.error?.(formatDingTalkErrorPayloadLog("outbound.sendMedia", err.response.data));
|
|
148
|
+
}
|
|
149
|
+
throw new Error(
|
|
150
|
+
typeof err?.response?.data === "string"
|
|
151
|
+
? err.response.data
|
|
152
|
+
: err?.message || "sendMedia failed",
|
|
153
|
+
{ cause: err },
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|