@youdu/openclaw-youdu 2026.4.28 → 2026.5.7

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.
@@ -0,0 +1,250 @@
1
+ import { monitorYouDuProvider } from './monitor.js';
2
+ import { resolveYouDuAccount } from './utils.js';
3
+ import { TEXT_CHUNK_LIMIT, CHANNEL_ID, DEFAULT_DM_POLICY } from './const.js';
4
+ import { youduSetupAdapter, youduSetupWizard } from './onboarding.js';
5
+ import { DEFAULT_ACCOUNT_ID, formatPairingApproveHint } from './openclaw-compat.js';
6
+ import { getYouDuRuntime } from './runtime.js';
7
+
8
+ /*
9
+ * Licensed under the MIT License.
10
+ */
11
+ const meta = {
12
+ id: CHANNEL_ID,
13
+ label: '有度',
14
+ selectionLabel: '有度',
15
+ detailLabel: '有度机器人',
16
+ docsPath: `/channels/${CHANNEL_ID}`,
17
+ docsLabel: CHANNEL_ID,
18
+ blurb: '有度机器人接入插件',
19
+ systemImage: 'message.fill',
20
+ };
21
+ const youduPlugin = {
22
+ id: CHANNEL_ID,
23
+ meta: {
24
+ ...meta,
25
+ quickstartAllowFrom: true,
26
+ },
27
+ pairing: {
28
+ idLabel: 'youduUserId',
29
+ // 格式化用户输入的ID/配对码,去除前缀、空格等
30
+ normalizeAllowEntry: (entry) => entry.replace(new RegExp(`^(${CHANNEL_ID}|user):`, 'i'), '').trim(),
31
+ // 配对成功后的回调:可以给用户发配对成功通知
32
+ notifyApproval: async ({ cfg, id }) => { },
33
+ },
34
+ setupWizard: youduSetupWizard,
35
+ setup: youduSetupAdapter,
36
+ capabilities: {
37
+ chatTypes: ['direct', 'group'],
38
+ reactions: false,
39
+ threads: false,
40
+ media: true,
41
+ nativeCommands: false,
42
+ blockStreaming: true,
43
+ },
44
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
45
+ config: {
46
+ listAccountIds: () => [DEFAULT_ACCOUNT_ID],
47
+ resolveAccount: (cfg) => resolveYouDuAccount(cfg),
48
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
49
+ setAccountEnabled: ({ cfg, enabled }) => {
50
+ const youduConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
51
+ return {
52
+ ...cfg,
53
+ channels: {
54
+ ...cfg.channels,
55
+ [CHANNEL_ID]: {
56
+ ...youduConfig,
57
+ enabled,
58
+ },
59
+ },
60
+ };
61
+ },
62
+ deleteAccount: ({ cfg }) => {
63
+ const youduConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
64
+ const { botId, secret, ...rest } = youduConfig;
65
+ return {
66
+ ...cfg,
67
+ channels: {
68
+ ...cfg.channels,
69
+ [CHANNEL_ID]: rest,
70
+ },
71
+ };
72
+ },
73
+ isConfigured: (account) => Boolean(account.botId?.trim() && account.secret?.trim() && account.url?.trim()),
74
+ describeAccount: (account) => ({
75
+ accountId: account.accountId,
76
+ name: account.name,
77
+ enabled: account.enabled,
78
+ configured: Boolean(account.botId?.trim() && account.secret?.trim() && account.url?.trim()),
79
+ botId: account.botId,
80
+ url: account.url,
81
+ }),
82
+ // 读取允许的用户白名单(配对成功的用户会被SDK自动写入这里)
83
+ resolveAllowFrom: ({ cfg }) => {
84
+ const account = resolveYouDuAccount(cfg);
85
+ return (account.config.allowFrom ?? []).map((entry) => String(entry));
86
+ },
87
+ // 格式化用户输入的白名单条目,和配对逻辑保持一致
88
+ formatAllowFrom: ({ allowFrom }) => allowFrom
89
+ .map((entry) => String(entry).trim())
90
+ .filter(Boolean),
91
+ },
92
+ security: {
93
+ // 私信权限策略核心:控制谁能给机器人发私信
94
+ resolveDmPolicy: ({ account }) => {
95
+ const basePath = `channels.${CHANNEL_ID}.`;
96
+ return {
97
+ // 默认改为 pairing 模式
98
+ policy: account.config.dmPolicy ?? DEFAULT_DM_POLICY,
99
+ // 白名单列表
100
+ allowFrom: account.config.allowFrom ?? [],
101
+ policyPath: `${basePath}dmPolicy`,
102
+ allowFromPath: basePath,
103
+ // 配对提示语:用户未配对时,机器人自动回复的提示
104
+ approveHint: formatPairingApproveHint(CHANNEL_ID),
105
+ // 格式化用户ID,和pairing模块保持一致
106
+ normalizeEntry: (raw) => raw.replace(new RegExp(`^${CHANNEL_ID}:`, 'i'), '').trim(),
107
+ };
108
+ },
109
+ // 配置风险告警:检测不安全的配置并提示
110
+ collectWarnings: ({ account, cfg }) => {
111
+ const warnings = [];
112
+ const dmPolicy = account.config.dmPolicy ?? 'open';
113
+ if (dmPolicy === 'open') {
114
+ const hasWildcard = (account.config.allowFrom ?? []).some((entry) => String(entry).trim() === '*');
115
+ if (!hasWildcard) {
116
+ warnings.push(`- 有度私信:dmPolicy="open" 但 allowFrom 未包含 "*"。任何人都可以发消息,但允许列表为空可能导致意外行为。`);
117
+ }
118
+ }
119
+ const groupPolicy = account.config.groupPolicy ?? 'open';
120
+ if (groupPolicy === 'open') {
121
+ warnings.push(`- 有度群组:groupPolicy="open" 允许所有群组中的成员触发。`);
122
+ }
123
+ return warnings;
124
+ },
125
+ },
126
+ messaging: {
127
+ normalizeTarget: (target) => {
128
+ const trimmed = target.trim();
129
+ if (!trimmed)
130
+ return undefined;
131
+ return trimmed;
132
+ },
133
+ targetResolver: {
134
+ looksLikeId: (id) => {
135
+ const trimmed = id?.trim();
136
+ return Boolean(trimmed);
137
+ },
138
+ hint: '<userId|groupId>',
139
+ },
140
+ },
141
+ directory: {
142
+ self: async () => null,
143
+ listPeers: async () => [],
144
+ listGroups: async () => [],
145
+ },
146
+ outbound: {
147
+ deliveryMode: "gateway",
148
+ chunker: (text, limit) => getYouDuRuntime().channel.text.chunkMarkdownText(text, limit),
149
+ textChunkLimit: TEXT_CHUNK_LIMIT,
150
+ },
151
+ status: {
152
+ defaultRuntime: {
153
+ accountId: DEFAULT_ACCOUNT_ID,
154
+ running: false,
155
+ lastStartAt: null,
156
+ lastStopAt: null,
157
+ lastError: null,
158
+ },
159
+ collectStatusIssues: (accounts) => {
160
+ return accounts.flatMap((entry) => {
161
+ const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
162
+ const enabled = entry.enabled !== false;
163
+ const configured = entry.configured === true;
164
+ if (!enabled) {
165
+ return [];
166
+ }
167
+ const issues = [];
168
+ if (!configured) {
169
+ issues.push({
170
+ channel: CHANNEL_ID,
171
+ accountId,
172
+ kind: 'config',
173
+ message: '有度机器人 ID 或 Secret 或 Url 未配置',
174
+ fix: `Run: openclaw channels add ${CHANNEL_ID} --bot-id <id> --secret <secret> --url <url>`,
175
+ });
176
+ }
177
+ return issues;
178
+ });
179
+ },
180
+ buildChannelSummary: ({ snapshot }) => ({
181
+ configured: snapshot.configured ?? false,
182
+ running: snapshot.running ?? false,
183
+ lastStartAt: snapshot.lastStartAt ?? null,
184
+ lastStopAt: snapshot.lastStopAt ?? null,
185
+ lastError: snapshot.lastError ?? null,
186
+ }),
187
+ probeAccount: async () => {
188
+ return { ok: true, status: 200 };
189
+ },
190
+ buildAccountSnapshot: ({ account, runtime }) => {
191
+ const configured = Boolean(account.botId?.trim() &&
192
+ account.secret?.trim() &&
193
+ account.url?.trim());
194
+ return {
195
+ accountId: account.accountId,
196
+ name: account.name,
197
+ enabled: account.enabled,
198
+ configured,
199
+ running: runtime?.running ?? false,
200
+ lastStartAt: runtime?.lastStartAt ?? null,
201
+ lastStopAt: runtime?.lastStopAt ?? null,
202
+ lastError: runtime?.lastError ?? null,
203
+ };
204
+ },
205
+ },
206
+ gateway: {
207
+ startAccount: async (ctx) => {
208
+ const account = ctx.account;
209
+ return monitorYouDuProvider({
210
+ account,
211
+ config: ctx.cfg,
212
+ runtime: ctx.runtime,
213
+ abortSignal: ctx.abortSignal,
214
+ });
215
+ },
216
+ logoutAccount: async ({ cfg }) => {
217
+ const nextCfg = { ...cfg };
218
+ const youduConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
219
+ const nextYoudu = { ...youduConfig };
220
+ let cleared = false;
221
+ let changed = false;
222
+ if (nextYoudu.botId || nextYoudu.secret) {
223
+ delete nextYoudu.botId;
224
+ delete nextYoudu.secret;
225
+ cleared = true;
226
+ changed = true;
227
+ }
228
+ if (changed) {
229
+ if (Object.keys(nextYoudu).length > 0) {
230
+ nextCfg.channels = { ...nextCfg.channels, [CHANNEL_ID]: nextYoudu };
231
+ }
232
+ else {
233
+ const nextChannels = { ...nextCfg.channels };
234
+ delete nextChannels[CHANNEL_ID];
235
+ if (Object.keys(nextChannels).length > 0) {
236
+ nextCfg.channels = nextChannels;
237
+ }
238
+ else {
239
+ delete nextCfg.channels;
240
+ }
241
+ }
242
+ }
243
+ const resolved = resolveYouDuAccount(changed ? nextCfg : cfg);
244
+ const loggedOut = !resolved.botId && !resolved.secret;
245
+ return { cleared, envToken: false, loggedOut };
246
+ },
247
+ },
248
+ };
249
+
250
+ export { youduPlugin };
@@ -1,4 +1,3 @@
1
- import * as https from "node:https";
2
1
  /** 渠道 ID */
3
2
  export declare const CHANNEL_ID: "youdu";
4
3
  export declare const DEFAULT_DM_POLICY = "pairing";
@@ -8,7 +7,6 @@ export declare const PORT = 7443;
8
7
  export declare const WS_URL = "/cgi/bot/wss";
9
8
  export declare const DOWNLOAD_MEDIA_URL = "/cgi/bot/file/download";
10
9
  export declare const UPLOAD_MEDIA_URL = "/cgi/bot/file/upload";
11
- export declare const agent: https.Agent;
12
10
  /** 文本分块大小上限 */
13
11
  export declare const TEXT_CHUNK_LIMIT = 4000;
14
12
  /** WebSocket 心跳间隔(毫秒) */
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Licensed under the MIT License.
3
+ */
4
+ /** 渠道 ID */
5
+ const CHANNEL_ID = 'youdu';
6
+ const DEFAULT_DM_POLICY = 'pairing';
7
+ const WEBSOCKET_SCHEME = 'wss://';
8
+ const HTTP_SCHEME = 'https://';
9
+ const PORT = 7443;
10
+ const WS_URL = "/cgi/bot/wss";
11
+ const DOWNLOAD_MEDIA_URL = "/cgi/bot/file/download";
12
+ const UPLOAD_MEDIA_URL = "/cgi/bot/file/upload";
13
+ /** 文本分块大小上限 */
14
+ const TEXT_CHUNK_LIMIT = 4000;
15
+ /** WebSocket 心跳间隔(毫秒) */
16
+ const WS_HEARTBEAT_INTERVAL_MS = 30000;
17
+ /** WebSocket 最大重连次数 */
18
+ const WS_MAX_RECONNECT_ATTEMPTS = 10;
19
+ /** 消息处理总超时时间(毫秒) */
20
+ const MESSAGE_PROCESS_TIMEOUT_MS = 5 * 60 * 1000;
21
+ /** "思考中"流式消息占位内容 */
22
+ const THINKING_MESSAGE = '<think></think>';
23
+ /** 消息状态清理间隔(毫秒) */
24
+ const MESSAGE_STATE_CLEANUP_INTERVAL_MS = 60000;
25
+ /** 仅包含图片时的消息占位符 */
26
+ const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
27
+ /** 仅包含文件时的消息占位符 */
28
+ const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
29
+ /** 最大的媒体传输大小 */
30
+ const DEFAULT_MEDIA_MAX_SIZE = 12 * 1024 * 1024; // 12MB
31
+
32
+ export { CHANNEL_ID, DEFAULT_DM_POLICY, DEFAULT_MEDIA_MAX_SIZE, DOWNLOAD_MEDIA_URL, HTTP_SCHEME, MEDIA_DOCUMENT_PLACEHOLDER, MEDIA_IMAGE_PLACEHOLDER, MESSAGE_PROCESS_TIMEOUT_MS, MESSAGE_STATE_CLEANUP_INTERVAL_MS, PORT, TEXT_CHUNK_LIMIT, THINKING_MESSAGE, UPLOAD_MEDIA_URL, WEBSOCKET_SCHEME, WS_HEARTBEAT_INTERVAL_MS, WS_MAX_RECONNECT_ATTEMPTS, WS_URL };
@@ -0,0 +1,78 @@
1
+ import { getYouDuRuntime } from './runtime.js';
2
+ import { DEFAULT_DM_POLICY, CHANNEL_ID } from './const.js';
3
+ import { isSenderAllowed } from './group-policy.js';
4
+ import { sendYouDuReply } from './message-sender.js';
5
+
6
+ /*
7
+ * Licensed under the MIT License.
8
+ */
9
+ // ============================================================================
10
+ // 公开 API
11
+ // ============================================================================
12
+ /**
13
+ * 检查 DM Policy 访问控制
14
+ * @returns 检查结果,包含是否允许继续处理
15
+ */
16
+ async function checkDmPolicy(params) {
17
+ const { senderId, isGroup, account, wsClient, frame, runtime } = params;
18
+ const core = getYouDuRuntime();
19
+ // 群聊消息不检查 DM Policy
20
+ if (isGroup) {
21
+ return { allowed: true };
22
+ }
23
+ const dmPolicy = account.config.dmPolicy ?? DEFAULT_DM_POLICY;
24
+ const configAllowFrom = (account.config.allowFrom ?? []).map((v) => String(v));
25
+ // 如果 dmPolicy 是 disabled,直接拒绝
26
+ if (dmPolicy === "disabled") {
27
+ runtime.log?.(`[youdu] Blocked DM from ${senderId} (dmPolicy=disabled)`);
28
+ return { allowed: false };
29
+ }
30
+ // 如果是 open 模式,允许所有人
31
+ if (dmPolicy === "open") {
32
+ return { allowed: true };
33
+ }
34
+ const storeAllowFrom = await core.channel.pairing
35
+ .readAllowFromStore({ channel: CHANNEL_ID, accountId: account.accountId })
36
+ .catch(() => []);
37
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom];
38
+ const senderAllowedResult = isSenderAllowed(senderId, effectiveAllowFrom);
39
+ if (senderAllowedResult) {
40
+ return { allowed: true };
41
+ }
42
+ // 处理未授权用户
43
+ if (dmPolicy === "pairing") {
44
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
45
+ channel: CHANNEL_ID,
46
+ id: senderId,
47
+ accountId: account.accountId,
48
+ meta: { name: senderId },
49
+ });
50
+ if (created) {
51
+ runtime.log?.(`[youdu] Pairing request created for sender=${senderId}`);
52
+ try {
53
+ await sendYouDuReply({
54
+ wsClient,
55
+ frame,
56
+ text: core.channel.pairing.buildPairingReply({
57
+ channel: CHANNEL_ID,
58
+ idLine: `您的有度用户ID: ${senderId}`,
59
+ code,
60
+ }),
61
+ runtime,
62
+ });
63
+ }
64
+ catch (err) {
65
+ runtime.error?.(`[youdu] Failed to send pairing reply to ${senderId}: ${String(err)}`);
66
+ }
67
+ }
68
+ else {
69
+ runtime.log?.(`[youdu] Pairing request already exists for sender=${senderId}`);
70
+ }
71
+ return { allowed: false, pairingSent: created };
72
+ }
73
+ // allowlist 模式:直接拒绝未授权用户
74
+ runtime.log?.(`[youdu] Blocked unauthorized sender ${senderId} (dmPolicy=${dmPolicy})`);
75
+ return { allowed: false };
76
+ }
77
+
78
+ export { checkDmPolicy };
@@ -0,0 +1,113 @@
1
+ import { CHANNEL_ID } from './const.js';
2
+
3
+ /*
4
+ * Licensed under the MIT License.
5
+ */
6
+ // ============================================================================
7
+ // 内部辅助函数
8
+ // ============================================================================
9
+ function resolveWeComGroupConfig(params) {
10
+ const groups = params.cfg?.groups ?? {};
11
+ const wildcard = groups["*"];
12
+ const groupId = params.groupId?.trim();
13
+ if (!groupId) {
14
+ return undefined;
15
+ }
16
+ const direct = groups[groupId];
17
+ if (direct) {
18
+ return direct;
19
+ }
20
+ const lowered = groupId.toLowerCase();
21
+ const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
22
+ if (matchKey) {
23
+ return groups[matchKey];
24
+ }
25
+ return wildcard;
26
+ }
27
+ /**
28
+ * 检查群组是否在允许列表中
29
+ */
30
+ function isYouDuGroupAllowed(params) {
31
+ const { groupPolicy } = params;
32
+ if (groupPolicy === "disabled") {
33
+ return false;
34
+ }
35
+ if (groupPolicy === "open") {
36
+ return true;
37
+ }
38
+ // allowlist 模式:检查群组是否在允许列表中
39
+ const normalizedAllowFrom = params.allowFrom.map((entry) => String(entry).replace(new RegExp(`^${CHANNEL_ID}:`, "i"), "").trim());
40
+ if (normalizedAllowFrom.includes("*")) {
41
+ return true;
42
+ }
43
+ const normalizedGroupId = params.groupId.trim();
44
+ return normalizedAllowFrom.some((entry) => entry === normalizedGroupId || entry.toLowerCase() === normalizedGroupId.toLowerCase());
45
+ }
46
+ /**
47
+ * 检查群组内发送者是否在允许列表中
48
+ */
49
+ function isGroupSenderAllowed(params) {
50
+ const { senderId, groupId, youduConfig } = params;
51
+ const groupConfig = resolveWeComGroupConfig({
52
+ cfg: youduConfig,
53
+ groupId,
54
+ });
55
+ const perGroupSenderAllowFrom = (groupConfig?.allowFrom ?? []).map((v) => String(v));
56
+ if (perGroupSenderAllowFrom.length === 0) {
57
+ return true;
58
+ }
59
+ if (perGroupSenderAllowFrom.includes("*")) {
60
+ return true;
61
+ }
62
+ return perGroupSenderAllowFrom.some((entry) => {
63
+ const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, "i"), "").trim();
64
+ return normalized === senderId || normalized === `user:${senderId}`;
65
+ });
66
+ }
67
+ // ============================================================================
68
+ // 公开 API
69
+ // ============================================================================
70
+ /**
71
+ * 检查群组策略访问控制
72
+ * @returns 检查结果,包含是否允许继续处理
73
+ */
74
+ function checkGroupPolicy(params) {
75
+ const { chatId, senderId, account, config, runtime } = params;
76
+ const youduConfig = (config.channels?.[CHANNEL_ID] ?? {});
77
+ const defaultGroupPolicy = config.channels?.[CHANNEL_ID]?.groupPolicy;
78
+ const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
79
+ const groupAllowFrom = youduConfig.groupAllowFrom ?? [];
80
+ const groupAllowed = isYouDuGroupAllowed({
81
+ groupPolicy,
82
+ allowFrom: groupAllowFrom,
83
+ groupId: chatId,
84
+ });
85
+ if (!groupAllowed) {
86
+ runtime.log?.(`[youdu] Group ${chatId} not allowed (groupPolicy=${groupPolicy})`);
87
+ return { allowed: false };
88
+ }
89
+ const senderAllowed = isGroupSenderAllowed({
90
+ senderId,
91
+ groupId: chatId,
92
+ youduConfig,
93
+ });
94
+ if (!senderAllowed) {
95
+ runtime.log?.(`[youdu] Sender ${senderId} not in group ${chatId} sender allowlist`);
96
+ return { allowed: false };
97
+ }
98
+ return { allowed: true };
99
+ }
100
+ /**
101
+ * 检查发送者是否在允许列表中(通用)
102
+ */
103
+ function isSenderAllowed(senderId, allowFrom) {
104
+ if (allowFrom.includes("*")) {
105
+ return true;
106
+ }
107
+ return allowFrom.some((entry) => {
108
+ const normalized = entry.replace(new RegExp(`^${CHANNEL_ID}:`, "i"), "").trim();
109
+ return normalized === senderId || normalized === `user:${senderId}`;
110
+ });
111
+ }
112
+
113
+ export { checkGroupPolicy, isSenderAllowed };
@@ -1,5 +1,5 @@
1
1
  import type { IncomingHttpHeaders } from "node:http";
2
- import type { ResolvedYouDuAccount as ResolvedYouduAccount, YouDuConfig as YouduConfig } from './utils.js';
2
+ import type { ResolvedYouDuAccount, YouDuConfig } from './utils.js';
3
3
  import { WSClient, WsFrame } from "./sdk/ws-client";
4
4
  import { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
5
5
  export declare namespace Youdu {
@@ -23,7 +23,7 @@ export declare namespace Youdu {
23
23
  }
24
24
  interface UploadAndSendMediaOptions {
25
25
  wsClient: WSClient;
26
- account: ResolvedYouduAccount;
26
+ account: ResolvedYouDuAccount;
27
27
  mediaUrl: string;
28
28
  frame: WsFrame;
29
29
  mediaLocalRoots?: readonly string[];
@@ -35,6 +35,7 @@ export declare namespace Youdu {
35
35
  body: Buffer;
36
36
  }
37
37
  }
38
+ export declare const YouDuRequestTimeoutMs = 30000;
38
39
  export declare function inferInboundMessageText(body: {
39
40
  text?: {
40
41
  content?: string;
@@ -58,7 +59,7 @@ export declare function extractInboundMedia(body: {
58
59
  }): Youdu.InboundMediaAttachment[];
59
60
  export declare function uploadAndSendMedia(options: Youdu.UploadAndSendMediaOptions): Promise<void>;
60
61
  export declare function uploadOutboundMedia(params: {
61
- account: ResolvedYouduAccount;
62
+ account: ResolvedYouDuAccount;
62
63
  mediaUrl: string;
63
64
  mediaLocalRoots?: readonly string[];
64
65
  preferredKind?: Youdu.InboundMediaAttachment['kind'];
@@ -66,9 +67,9 @@ export declare function uploadOutboundMedia(params: {
66
67
  }): Promise<Youdu.InboundMediaAttachment>;
67
68
  export declare function buildMediaMessageBody(media: Youdu.InboundMediaAttachment): Record<string, unknown>;
68
69
  export declare function downloadInboundMedia(params: {
69
- account: ResolvedYouduAccount;
70
+ account: ResolvedYouDuAccount;
70
71
  attachments: Youdu.InboundMediaAttachment[];
71
72
  messageId: string;
72
- config: YouduConfig;
73
+ config: YouDuConfig;
73
74
  token?: string;
74
75
  }): Promise<Array<Youdu.DownloadedInboundMedia>>;