openclaw-ringcentral 2026.1.29-beta1
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/LICENSE +21 -0
- package/README.md +186 -0
- package/index.ts +18 -0
- package/openclaw.plugin.json +200 -0
- package/package.json +72 -0
- package/src/accounts.test.ts +311 -0
- package/src/accounts.ts +167 -0
- package/src/api.ts +241 -0
- package/src/auth.ts +92 -0
- package/src/channel.ts +545 -0
- package/src/config-schema.ts +78 -0
- package/src/markdown.test.ts +168 -0
- package/src/markdown.ts +158 -0
- package/src/monitor.test.ts +47 -0
- package/src/monitor.ts +742 -0
- package/src/openclaw.d.ts +68 -0
- package/src/runtime.ts +14 -0
- package/src/targets.test.ts +118 -0
- package/src/targets.ts +70 -0
- package/src/types.ts +174 -0
package/src/channel.ts
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyAccountNameToChannelSection,
|
|
3
|
+
buildChannelConfigSchema,
|
|
4
|
+
DEFAULT_ACCOUNT_ID,
|
|
5
|
+
deleteAccountFromConfigSection,
|
|
6
|
+
formatPairingApproveHint,
|
|
7
|
+
migrateBaseNameToDefaultAccount,
|
|
8
|
+
missingTargetError,
|
|
9
|
+
normalizeAccountId,
|
|
10
|
+
PAIRING_APPROVED_MESSAGE,
|
|
11
|
+
resolveChannelMediaMaxBytes,
|
|
12
|
+
setAccountEnabledInConfigSection,
|
|
13
|
+
type ChannelDock,
|
|
14
|
+
type ChannelPlugin,
|
|
15
|
+
type OpenClawConfig,
|
|
16
|
+
} from "openclaw/plugin-sdk";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
listRingCentralAccountIds,
|
|
20
|
+
resolveDefaultRingCentralAccountId,
|
|
21
|
+
resolveRingCentralAccount,
|
|
22
|
+
type ResolvedRingCentralAccount,
|
|
23
|
+
} from "./accounts.js";
|
|
24
|
+
import { RingCentralConfigSchema } from "./config-schema.js";
|
|
25
|
+
import {
|
|
26
|
+
sendRingCentralMessage,
|
|
27
|
+
uploadRingCentralAttachment,
|
|
28
|
+
probeRingCentral,
|
|
29
|
+
} from "./api.js";
|
|
30
|
+
import { getRingCentralRuntime } from "./runtime.js";
|
|
31
|
+
import { startRingCentralMonitor } from "./monitor.js";
|
|
32
|
+
import {
|
|
33
|
+
normalizeRingCentralTarget,
|
|
34
|
+
isRingCentralChatTarget,
|
|
35
|
+
parseRingCentralTarget,
|
|
36
|
+
} from "./targets.js";
|
|
37
|
+
import type { RingCentralConfig } from "./types.js";
|
|
38
|
+
|
|
39
|
+
const formatAllowFromEntry = (entry: string) =>
|
|
40
|
+
(entry ?? "")
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/^(ringcentral|rc):/i, "")
|
|
43
|
+
.replace(/^user:/i, "")
|
|
44
|
+
.toLowerCase();
|
|
45
|
+
|
|
46
|
+
export const ringcentralDock: ChannelDock = {
|
|
47
|
+
id: "ringcentral",
|
|
48
|
+
capabilities: {
|
|
49
|
+
chatTypes: ["direct", "group", "thread"],
|
|
50
|
+
reactions: false,
|
|
51
|
+
media: true,
|
|
52
|
+
threads: false,
|
|
53
|
+
blockStreaming: true,
|
|
54
|
+
},
|
|
55
|
+
outbound: { textChunkLimit: 4000 },
|
|
56
|
+
config: {
|
|
57
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
58
|
+
(resolveRingCentralAccount({ cfg: cfg as OpenClawConfig, accountId }).config.dm?.allowFrom ??
|
|
59
|
+
[]
|
|
60
|
+
).map((entry) => String(entry)),
|
|
61
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
62
|
+
allowFrom
|
|
63
|
+
.map((entry) => String(entry))
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map(formatAllowFromEntry),
|
|
66
|
+
},
|
|
67
|
+
groups: {
|
|
68
|
+
resolveRequireMention: ({ cfg, accountId }) => {
|
|
69
|
+
const account = resolveRingCentralAccount({ cfg: cfg as OpenClawConfig, accountId });
|
|
70
|
+
return account.config.requireMention ?? true;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
threading: {
|
|
74
|
+
resolveReplyToMode: ({ cfg }) =>
|
|
75
|
+
(cfg.channels?.ringcentral as RingCentralConfig | undefined)?.replyToMode ?? "off",
|
|
76
|
+
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
77
|
+
currentChannelId: context.To?.trim() || undefined,
|
|
78
|
+
currentThreadTs: undefined,
|
|
79
|
+
hasRepliedRef,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const ringcentralPlugin: ChannelPlugin<ResolvedRingCentralAccount> = {
|
|
85
|
+
id: "ringcentral",
|
|
86
|
+
meta: {
|
|
87
|
+
id: "ringcentral",
|
|
88
|
+
label: "RingCentral",
|
|
89
|
+
selectionLabel: "RingCentral Team Messaging",
|
|
90
|
+
docsPath: "/channels/ringcentral",
|
|
91
|
+
docsLabel: "ringcentral",
|
|
92
|
+
blurb: "RingCentral Team Messaging via REST API and WebSocket.",
|
|
93
|
+
order: 56,
|
|
94
|
+
},
|
|
95
|
+
pairing: {
|
|
96
|
+
idLabel: "ringcentralUserId",
|
|
97
|
+
normalizeAllowEntry: (entry) => formatAllowFromEntry(entry),
|
|
98
|
+
notifyApproval: async ({ cfg, id }) => {
|
|
99
|
+
const account = resolveRingCentralAccount({ cfg: cfg as OpenClawConfig });
|
|
100
|
+
if (account.credentialSource === "none") return;
|
|
101
|
+
const target = normalizeRingCentralTarget(id) ?? id;
|
|
102
|
+
// For DM approval, we need to find/create a direct chat
|
|
103
|
+
// This is a simplified version - in production you'd need to resolve the chat ID
|
|
104
|
+
try {
|
|
105
|
+
await sendRingCentralMessage({
|
|
106
|
+
account,
|
|
107
|
+
chatId: target,
|
|
108
|
+
text: PAIRING_APPROVED_MESSAGE,
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
// Approval notification failed, but pairing still succeeds
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
capabilities: {
|
|
116
|
+
chatTypes: ["direct", "group"],
|
|
117
|
+
reactions: false,
|
|
118
|
+
threads: false,
|
|
119
|
+
media: true,
|
|
120
|
+
nativeCommands: false,
|
|
121
|
+
blockStreaming: true,
|
|
122
|
+
},
|
|
123
|
+
streaming: {
|
|
124
|
+
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
|
125
|
+
},
|
|
126
|
+
reload: { configPrefixes: ["channels.ringcentral"] },
|
|
127
|
+
configSchema: buildChannelConfigSchema(RingCentralConfigSchema),
|
|
128
|
+
config: {
|
|
129
|
+
listAccountIds: (cfg) => listRingCentralAccountIds(cfg as OpenClawConfig),
|
|
130
|
+
resolveAccount: (cfg, accountId) =>
|
|
131
|
+
resolveRingCentralAccount({ cfg: cfg as OpenClawConfig, accountId }),
|
|
132
|
+
defaultAccountId: (cfg) => resolveDefaultRingCentralAccountId(cfg as OpenClawConfig),
|
|
133
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) =>
|
|
134
|
+
setAccountEnabledInConfigSection({
|
|
135
|
+
cfg: cfg as OpenClawConfig,
|
|
136
|
+
sectionKey: "ringcentral",
|
|
137
|
+
accountId,
|
|
138
|
+
enabled,
|
|
139
|
+
allowTopLevel: true,
|
|
140
|
+
}),
|
|
141
|
+
deleteAccount: ({ cfg, accountId }) =>
|
|
142
|
+
deleteAccountFromConfigSection({
|
|
143
|
+
cfg: cfg as OpenClawConfig,
|
|
144
|
+
sectionKey: "ringcentral",
|
|
145
|
+
accountId,
|
|
146
|
+
clearBaseFields: [
|
|
147
|
+
"credentials",
|
|
148
|
+
"name",
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
isConfigured: (account) => account.credentialSource !== "none",
|
|
152
|
+
describeAccount: (account) => ({
|
|
153
|
+
accountId: account.accountId,
|
|
154
|
+
name: account.name,
|
|
155
|
+
enabled: account.enabled,
|
|
156
|
+
configured: account.credentialSource !== "none",
|
|
157
|
+
credentialSource: account.credentialSource,
|
|
158
|
+
server: account.server,
|
|
159
|
+
}),
|
|
160
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
161
|
+
(resolveRingCentralAccount({
|
|
162
|
+
cfg: cfg as OpenClawConfig,
|
|
163
|
+
accountId,
|
|
164
|
+
}).config.dm?.allowFrom ?? []
|
|
165
|
+
).map((entry) => String(entry)),
|
|
166
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
167
|
+
allowFrom
|
|
168
|
+
.map((entry) => String(entry))
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.map(formatAllowFromEntry),
|
|
171
|
+
},
|
|
172
|
+
security: {
|
|
173
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
174
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
175
|
+
const useAccountPath = Boolean(
|
|
176
|
+
(cfg as OpenClawConfig).channels?.ringcentral?.accounts?.[resolvedAccountId],
|
|
177
|
+
);
|
|
178
|
+
const allowFromPath = useAccountPath
|
|
179
|
+
? `channels.ringcentral.accounts.${resolvedAccountId}.dm.`
|
|
180
|
+
: "channels.ringcentral.dm.";
|
|
181
|
+
return {
|
|
182
|
+
policy: account.config.dm?.policy ?? "pairing",
|
|
183
|
+
allowFrom: account.config.dm?.allowFrom ?? [],
|
|
184
|
+
allowFromPath,
|
|
185
|
+
approveHint: formatPairingApproveHint("ringcentral"),
|
|
186
|
+
normalizeEntry: (raw) => formatAllowFromEntry(raw),
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
collectWarnings: ({ account, cfg }) => {
|
|
190
|
+
const warnings: string[] = [];
|
|
191
|
+
const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
|
|
192
|
+
const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
193
|
+
if (groupPolicy === "open") {
|
|
194
|
+
warnings.push(
|
|
195
|
+
`- RingCentral chats: groupPolicy="open" allows any chat to trigger (mention-gated). Set channels.ringcentral.groupPolicy="allowlist" and configure channels.ringcentral.groups.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (account.config.dm?.policy === "open") {
|
|
199
|
+
warnings.push(
|
|
200
|
+
`- RingCentral DMs are open to anyone. Set channels.ringcentral.dm.policy="pairing" or "allowlist".`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return warnings;
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
groups: {
|
|
207
|
+
resolveRequireMention: ({ cfg, accountId }) => {
|
|
208
|
+
const account = resolveRingCentralAccount({ cfg: cfg as OpenClawConfig, accountId });
|
|
209
|
+
return account.config.requireMention ?? true;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
threading: {
|
|
213
|
+
resolveReplyToMode: ({ cfg }) =>
|
|
214
|
+
(cfg.channels?.ringcentral as RingCentralConfig | undefined)?.replyToMode ?? "off",
|
|
215
|
+
},
|
|
216
|
+
messaging: {
|
|
217
|
+
normalizeTarget: normalizeRingCentralTarget,
|
|
218
|
+
targetResolver: {
|
|
219
|
+
looksLikeId: (raw, normalized) => {
|
|
220
|
+
const value = normalized ?? raw.trim();
|
|
221
|
+
return isRingCentralChatTarget(value);
|
|
222
|
+
},
|
|
223
|
+
hint: "<chatId>",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
directory: {
|
|
227
|
+
self: async () => null,
|
|
228
|
+
listPeers: async ({ cfg, accountId, query, limit }) => {
|
|
229
|
+
const account = resolveRingCentralAccount({
|
|
230
|
+
cfg: cfg as OpenClawConfig,
|
|
231
|
+
accountId,
|
|
232
|
+
});
|
|
233
|
+
const q = query?.trim().toLowerCase() || "";
|
|
234
|
+
const allowFrom = account.config.dm?.allowFrom ?? [];
|
|
235
|
+
const peers = Array.from(
|
|
236
|
+
new Set(
|
|
237
|
+
allowFrom
|
|
238
|
+
.map((entry) => String(entry).trim())
|
|
239
|
+
.filter((entry) => Boolean(entry) && entry !== "*")
|
|
240
|
+
.map((entry) => normalizeRingCentralTarget(entry) ?? entry),
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
|
244
|
+
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
245
|
+
.map((id) => ({ kind: "user", id }) as const);
|
|
246
|
+
return peers;
|
|
247
|
+
},
|
|
248
|
+
listGroups: async ({ cfg, accountId, query, limit }) => {
|
|
249
|
+
const account = resolveRingCentralAccount({
|
|
250
|
+
cfg: cfg as OpenClawConfig,
|
|
251
|
+
accountId,
|
|
252
|
+
});
|
|
253
|
+
const groups = account.config.groups ?? {};
|
|
254
|
+
const q = query?.trim().toLowerCase() || "";
|
|
255
|
+
const entries = Object.keys(groups)
|
|
256
|
+
.filter((key) => key && key !== "*")
|
|
257
|
+
.filter((key) => (q ? key.toLowerCase().includes(q) : true))
|
|
258
|
+
.slice(0, limit && limit > 0 ? limit : undefined)
|
|
259
|
+
.map((id) => ({ kind: "group", id }) as const);
|
|
260
|
+
return entries;
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
resolver: {
|
|
264
|
+
resolveTargets: async ({ inputs, kind }) => {
|
|
265
|
+
const resolved = inputs.map((input) => {
|
|
266
|
+
const parsed = parseRingCentralTarget(input);
|
|
267
|
+
if (parsed.type === "unknown" || !parsed.id) {
|
|
268
|
+
return { input, resolved: false, note: "invalid target format" };
|
|
269
|
+
}
|
|
270
|
+
if (kind === "user" && parsed.type === "user") {
|
|
271
|
+
return { input, resolved: true, id: parsed.id };
|
|
272
|
+
}
|
|
273
|
+
if (kind === "group" && parsed.type === "chat") {
|
|
274
|
+
return { input, resolved: true, id: parsed.id };
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
input,
|
|
278
|
+
resolved: false,
|
|
279
|
+
note: "use rc:chat:<id> or rc:user:<id>",
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
return resolved;
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
setup: {
|
|
286
|
+
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
287
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
288
|
+
applyAccountNameToChannelSection({
|
|
289
|
+
cfg: cfg as OpenClawConfig,
|
|
290
|
+
channelKey: "ringcentral",
|
|
291
|
+
accountId,
|
|
292
|
+
name,
|
|
293
|
+
}),
|
|
294
|
+
validateInput: ({ accountId, input }) => {
|
|
295
|
+
if (input.useEnv && accountId !== DEFAULT_ACCOUNT_ID) {
|
|
296
|
+
return "RINGCENTRAL_* env vars can only be used for the default account.";
|
|
297
|
+
}
|
|
298
|
+
if (!input.useEnv && (!input.clientId || !input.clientSecret || !input.jwt)) {
|
|
299
|
+
return "RingCentral requires --client-id, --client-secret, and --jwt (or use --use-env).";
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
},
|
|
303
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
304
|
+
const namedConfig = applyAccountNameToChannelSection({
|
|
305
|
+
cfg: cfg as OpenClawConfig,
|
|
306
|
+
channelKey: "ringcentral",
|
|
307
|
+
accountId,
|
|
308
|
+
name: input.name,
|
|
309
|
+
});
|
|
310
|
+
const next =
|
|
311
|
+
accountId !== DEFAULT_ACCOUNT_ID
|
|
312
|
+
? migrateBaseNameToDefaultAccount({
|
|
313
|
+
cfg: namedConfig as OpenClawConfig,
|
|
314
|
+
channelKey: "ringcentral",
|
|
315
|
+
})
|
|
316
|
+
: namedConfig;
|
|
317
|
+
// Build nested credentials block
|
|
318
|
+
const credentialsPatch = input.useEnv
|
|
319
|
+
? {}
|
|
320
|
+
: {
|
|
321
|
+
credentials: {
|
|
322
|
+
...(input.clientId ? { clientId: input.clientId } : {}),
|
|
323
|
+
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
324
|
+
...(input.jwt ? { jwt: input.jwt } : {}),
|
|
325
|
+
...(input.server?.trim() ? { server: input.server.trim() } : {}),
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
// Only include credentials if it has any values
|
|
329
|
+
const hasCredentials = input.clientId || input.clientSecret || input.jwt || input.server?.trim();
|
|
330
|
+
const configPatch = input.useEnv || !hasCredentials ? {} : credentialsPatch;
|
|
331
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
332
|
+
return {
|
|
333
|
+
...next,
|
|
334
|
+
channels: {
|
|
335
|
+
...next.channels,
|
|
336
|
+
ringcentral: {
|
|
337
|
+
...(next.channels?.ringcentral ?? {}),
|
|
338
|
+
enabled: true,
|
|
339
|
+
...configPatch,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
} as OpenClawConfig;
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...next,
|
|
346
|
+
channels: {
|
|
347
|
+
...next.channels,
|
|
348
|
+
ringcentral: {
|
|
349
|
+
...(next.channels?.ringcentral ?? {}),
|
|
350
|
+
enabled: true,
|
|
351
|
+
accounts: {
|
|
352
|
+
...(next.channels?.ringcentral?.accounts ?? {}),
|
|
353
|
+
[accountId]: {
|
|
354
|
+
...(next.channels?.ringcentral?.accounts?.[accountId] ?? {}),
|
|
355
|
+
enabled: true,
|
|
356
|
+
...configPatch,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
} as OpenClawConfig;
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
outbound: {
|
|
365
|
+
deliveryMode: "direct",
|
|
366
|
+
chunker: (text, limit) =>
|
|
367
|
+
getRingCentralRuntime().channel.text.chunkMarkdownText(text, limit),
|
|
368
|
+
chunkerMode: "markdown",
|
|
369
|
+
textChunkLimit: 4000,
|
|
370
|
+
resolveTarget: ({ to, allowFrom, mode }) => {
|
|
371
|
+
const trimmed = to?.trim() ?? "";
|
|
372
|
+
const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
373
|
+
const allowList = allowListRaw
|
|
374
|
+
.filter((entry) => entry !== "*")
|
|
375
|
+
.map((entry) => normalizeRingCentralTarget(entry))
|
|
376
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
377
|
+
|
|
378
|
+
if (trimmed) {
|
|
379
|
+
const normalized = normalizeRingCentralTarget(trimmed);
|
|
380
|
+
if (!normalized) {
|
|
381
|
+
if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
|
|
382
|
+
return { ok: true, to: allowList[0] };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
error: missingTargetError(
|
|
387
|
+
"RingCentral",
|
|
388
|
+
"<chatId> or channels.ringcentral.dm.allowFrom[0]",
|
|
389
|
+
),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return { ok: true, to: normalized };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (allowList.length > 0) {
|
|
396
|
+
return { ok: true, to: allowList[0] };
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: missingTargetError(
|
|
401
|
+
"RingCentral",
|
|
402
|
+
"<chatId> or channels.ringcentral.dm.allowFrom[0]",
|
|
403
|
+
),
|
|
404
|
+
};
|
|
405
|
+
},
|
|
406
|
+
sendText: async ({ cfg, to, text, accountId }) => {
|
|
407
|
+
const account = resolveRingCentralAccount({
|
|
408
|
+
cfg: cfg as OpenClawConfig,
|
|
409
|
+
accountId,
|
|
410
|
+
});
|
|
411
|
+
const result = await sendRingCentralMessage({
|
|
412
|
+
account,
|
|
413
|
+
chatId: to,
|
|
414
|
+
text,
|
|
415
|
+
});
|
|
416
|
+
return {
|
|
417
|
+
channel: "ringcentral",
|
|
418
|
+
messageId: result?.postId ?? "",
|
|
419
|
+
chatId: to,
|
|
420
|
+
};
|
|
421
|
+
},
|
|
422
|
+
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
423
|
+
if (!mediaUrl) {
|
|
424
|
+
throw new Error("RingCentral mediaUrl is required.");
|
|
425
|
+
}
|
|
426
|
+
const account = resolveRingCentralAccount({
|
|
427
|
+
cfg: cfg as OpenClawConfig,
|
|
428
|
+
accountId,
|
|
429
|
+
});
|
|
430
|
+
const runtime = getRingCentralRuntime();
|
|
431
|
+
const maxBytes = resolveChannelMediaMaxBytes({
|
|
432
|
+
cfg: cfg as OpenClawConfig,
|
|
433
|
+
resolveChannelLimitMb: ({ cfg: c, accountId: aid }) =>
|
|
434
|
+
(c.channels?.ringcentral as { accounts?: Record<string, { mediaMaxMb?: number }>; mediaMaxMb?: number } | undefined)
|
|
435
|
+
?.accounts?.[aid]?.mediaMaxMb ??
|
|
436
|
+
(c.channels?.ringcentral as { mediaMaxMb?: number } | undefined)?.mediaMaxMb,
|
|
437
|
+
accountId,
|
|
438
|
+
});
|
|
439
|
+
const loaded = await runtime.channel.media.fetchRemoteMedia(mediaUrl, {
|
|
440
|
+
maxBytes: maxBytes ?? (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
441
|
+
});
|
|
442
|
+
const upload = await uploadRingCentralAttachment({
|
|
443
|
+
account,
|
|
444
|
+
chatId: to,
|
|
445
|
+
filename: loaded.filename ?? "attachment",
|
|
446
|
+
buffer: loaded.buffer,
|
|
447
|
+
contentType: loaded.contentType,
|
|
448
|
+
});
|
|
449
|
+
const result = await sendRingCentralMessage({
|
|
450
|
+
account,
|
|
451
|
+
chatId: to,
|
|
452
|
+
text,
|
|
453
|
+
attachments: upload.attachmentId ? [{ id: upload.attachmentId }] : undefined,
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
channel: "ringcentral",
|
|
457
|
+
messageId: result?.postId ?? "",
|
|
458
|
+
chatId: to,
|
|
459
|
+
};
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
status: {
|
|
463
|
+
defaultRuntime: {
|
|
464
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
465
|
+
running: false,
|
|
466
|
+
lastStartAt: null,
|
|
467
|
+
lastStopAt: null,
|
|
468
|
+
lastError: null,
|
|
469
|
+
},
|
|
470
|
+
collectStatusIssues: (accounts) =>
|
|
471
|
+
accounts.flatMap((entry) => {
|
|
472
|
+
const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
473
|
+
const enabled = entry.enabled !== false;
|
|
474
|
+
const configured = entry.configured === true;
|
|
475
|
+
if (!enabled || !configured) return [];
|
|
476
|
+
const issues = [];
|
|
477
|
+
if (!entry.clientId) {
|
|
478
|
+
issues.push({
|
|
479
|
+
channel: "ringcentral",
|
|
480
|
+
accountId,
|
|
481
|
+
kind: "config",
|
|
482
|
+
message: "RingCentral clientId is missing.",
|
|
483
|
+
fix: "Set channels.ringcentral.clientId or use rc-credentials.json.",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return issues;
|
|
487
|
+
}),
|
|
488
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
489
|
+
configured: snapshot.configured ?? false,
|
|
490
|
+
credentialSource: snapshot.credentialSource ?? "none",
|
|
491
|
+
server: snapshot.server ?? null,
|
|
492
|
+
running: snapshot.running ?? false,
|
|
493
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
494
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
495
|
+
lastError: snapshot.lastError ?? null,
|
|
496
|
+
probe: snapshot.probe,
|
|
497
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
498
|
+
}),
|
|
499
|
+
probeAccount: async ({ account }) => probeRingCentral(account),
|
|
500
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
501
|
+
accountId: account.accountId,
|
|
502
|
+
name: account.name,
|
|
503
|
+
enabled: account.enabled,
|
|
504
|
+
configured: account.credentialSource !== "none",
|
|
505
|
+
credentialSource: account.credentialSource,
|
|
506
|
+
server: account.server,
|
|
507
|
+
clientId: account.clientId ? `${account.clientId.slice(0, 8)}...` : undefined,
|
|
508
|
+
running: runtime?.running ?? false,
|
|
509
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
510
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
511
|
+
lastError: runtime?.lastError ?? null,
|
|
512
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
513
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
514
|
+
dmPolicy: account.config.dm?.policy ?? "allowlist",
|
|
515
|
+
probe,
|
|
516
|
+
}),
|
|
517
|
+
},
|
|
518
|
+
gateway: {
|
|
519
|
+
startAccount: async (ctx) => {
|
|
520
|
+
const account = ctx.account;
|
|
521
|
+
ctx.log?.info(`[${account.accountId}] starting RingCentral WebSocket`);
|
|
522
|
+
ctx.setStatus({
|
|
523
|
+
accountId: account.accountId,
|
|
524
|
+
running: true,
|
|
525
|
+
lastStartAt: Date.now(),
|
|
526
|
+
server: account.server,
|
|
527
|
+
});
|
|
528
|
+
const unregister = await startRingCentralMonitor({
|
|
529
|
+
account,
|
|
530
|
+
config: ctx.cfg as OpenClawConfig,
|
|
531
|
+
runtime: ctx.runtime,
|
|
532
|
+
abortSignal: ctx.abortSignal,
|
|
533
|
+
statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
|
|
534
|
+
});
|
|
535
|
+
return () => {
|
|
536
|
+
unregister?.();
|
|
537
|
+
ctx.setStatus({
|
|
538
|
+
accountId: account.accountId,
|
|
539
|
+
running: false,
|
|
540
|
+
lastStopAt: Date.now(),
|
|
541
|
+
});
|
|
542
|
+
};
|
|
543
|
+
},
|
|
544
|
+
},
|
|
545
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BlockStreamingCoalesceSchema,
|
|
5
|
+
DmPolicySchema,
|
|
6
|
+
GroupPolicySchema,
|
|
7
|
+
MarkdownConfigSchema,
|
|
8
|
+
requireOpenAllowFrom,
|
|
9
|
+
} from "openclaw/plugin-sdk";
|
|
10
|
+
|
|
11
|
+
const RingCentralGroupConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
requireMention: z.boolean().optional(),
|
|
14
|
+
enabled: z.boolean().optional(),
|
|
15
|
+
users: z.array(z.union([z.string(), z.number()])).optional(),
|
|
16
|
+
systemPrompt: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
|
|
20
|
+
const RingCentralCredentialsSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
clientId: z.string().optional(),
|
|
23
|
+
clientSecret: z.string().optional(),
|
|
24
|
+
jwt: z.string().optional(),
|
|
25
|
+
server: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
const RingCentralAccountSchemaBase = z
|
|
30
|
+
.object({
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
enabled: z.boolean().optional(),
|
|
33
|
+
credentials: RingCentralCredentialsSchema.optional(),
|
|
34
|
+
markdown: MarkdownConfigSchema,
|
|
35
|
+
dmPolicy: DmPolicySchema.optional().default("allowlist"),
|
|
36
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
37
|
+
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
38
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
39
|
+
groups: z.record(z.string(), RingCentralGroupConfigSchema.optional()).optional(),
|
|
40
|
+
requireMention: z.boolean().optional(),
|
|
41
|
+
mediaMaxMb: z.number().int().positive().optional(),
|
|
42
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
43
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
44
|
+
blockStreaming: z.boolean().optional(),
|
|
45
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
46
|
+
allowBots: z.boolean().optional(),
|
|
47
|
+
botExtensionId: z.string().optional(),
|
|
48
|
+
selfOnly: z.boolean().optional(),
|
|
49
|
+
useAdaptiveCards: z.boolean().optional(), // Use Adaptive Cards for code blocks (default: false)
|
|
50
|
+
})
|
|
51
|
+
.strict();
|
|
52
|
+
|
|
53
|
+
const RingCentralAccountSchema = RingCentralAccountSchemaBase.superRefine((value, ctx) => {
|
|
54
|
+
requireOpenAllowFrom({
|
|
55
|
+
policy: value.dmPolicy,
|
|
56
|
+
allowFrom: value.allowFrom,
|
|
57
|
+
ctx,
|
|
58
|
+
path: ["allowFrom"],
|
|
59
|
+
message:
|
|
60
|
+
'channels.ringcentral.dmPolicy="open" requires channels.ringcentral.allowFrom to include "*"',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export const RingCentralConfigSchema = RingCentralAccountSchemaBase.extend({
|
|
65
|
+
accounts: z.record(z.string(), RingCentralAccountSchema.optional()).optional(),
|
|
66
|
+
defaultAccount: z.string().optional(),
|
|
67
|
+
}).superRefine((value, ctx) => {
|
|
68
|
+
requireOpenAllowFrom({
|
|
69
|
+
policy: value.dmPolicy,
|
|
70
|
+
allowFrom: value.allowFrom,
|
|
71
|
+
ctx,
|
|
72
|
+
path: ["allowFrom"],
|
|
73
|
+
message:
|
|
74
|
+
'channels.ringcentral.dmPolicy="open" requires channels.ringcentral.allowFrom to include "*"',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
export type { RingCentralAccountConfig, RingCentralConfig } from "./types.js";
|