openclaw-ringcentral 2026.1.30-beta.1 → 2026.1.30
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/package.json +2 -1
- package/src/accounts.ts +1 -1
- package/src/api.ts +95 -1
- package/src/channel.ts +5 -4
- package/src/monitor.ts +183 -34
- package/src/openclaw.d.ts +347 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-ringcentral",
|
|
3
|
-
"version": "2026.1.30
|
|
3
|
+
"version": "2026.1.30",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "OpenClaw RingCentral Team Messaging channel plugin",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
+
"@rc-ex/ws": "^1.0.0",
|
|
60
61
|
"@ringcentral/sdk": "^5.0.0",
|
|
61
62
|
"@ringcentral/subscriptions": "^5.0.0",
|
|
62
63
|
"isomorphic-ws": "^5.0.0",
|
package/src/accounts.ts
CHANGED
|
@@ -139,7 +139,7 @@ export function resolveRingCentralAccount(params: {
|
|
|
139
139
|
cfg: OpenClawConfig;
|
|
140
140
|
accountId?: string | null;
|
|
141
141
|
}): ResolvedRingCentralAccount {
|
|
142
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
142
|
+
const accountId = normalizeAccountId(params.accountId ?? undefined);
|
|
143
143
|
const baseEnabled =
|
|
144
144
|
(params.cfg.channels?.ringcentral as RingCentralConfig | undefined)?.enabled !== false;
|
|
145
145
|
const merged = mergeRingCentralAccountConfig(params.cfg, accountId);
|
package/src/api.ts
CHANGED
|
@@ -12,6 +12,100 @@ import type {
|
|
|
12
12
|
// Team Messaging API endpoints
|
|
13
13
|
const TM_API_BASE = "/team-messaging/v1";
|
|
14
14
|
|
|
15
|
+
export type RingCentralApiErrorInfo = {
|
|
16
|
+
httpStatus?: number;
|
|
17
|
+
requestId?: string;
|
|
18
|
+
errorCode?: string;
|
|
19
|
+
errorMessage?: string;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
errors?: Array<{ errorCode?: string; message?: string; parameterName?: string }>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function extractRcApiError(err: unknown, accountId?: string): RingCentralApiErrorInfo {
|
|
25
|
+
const info: RingCentralApiErrorInfo = {};
|
|
26
|
+
if (accountId) info.accountId = accountId;
|
|
27
|
+
|
|
28
|
+
if (!err || typeof err !== "object") {
|
|
29
|
+
info.errorMessage = String(err);
|
|
30
|
+
return info;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const e = err as Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
// @ringcentral/sdk wraps errors with response object
|
|
36
|
+
const response = e.response as Record<string, unknown> | undefined;
|
|
37
|
+
if (response) {
|
|
38
|
+
info.httpStatus = typeof response.status === "number" ? response.status : undefined;
|
|
39
|
+
|
|
40
|
+
// Extract request ID from headers
|
|
41
|
+
const headers = response.headers as Record<string, unknown> | undefined;
|
|
42
|
+
if (headers) {
|
|
43
|
+
// headers can be a Headers object or plain object
|
|
44
|
+
if (typeof (headers as any).get === "function") {
|
|
45
|
+
info.requestId = (headers as any).get("x-request-id") ?? (headers as any).get("rcrequestid");
|
|
46
|
+
} else {
|
|
47
|
+
info.requestId = (headers["x-request-id"] ?? headers["rcrequestid"]) as string | undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to extract error body (SDK often attaches parsed JSON to error)
|
|
53
|
+
const body = (e._response as Record<string, unknown> | undefined) ??
|
|
54
|
+
(e.body as Record<string, unknown> | undefined) ??
|
|
55
|
+
(e.data as Record<string, unknown> | undefined);
|
|
56
|
+
if (body && typeof body === "object") {
|
|
57
|
+
info.errorCode = body.errorCode as string | undefined;
|
|
58
|
+
info.errorMessage = body.message as string | undefined;
|
|
59
|
+
if (Array.isArray(body.errors)) {
|
|
60
|
+
info.errors = body.errors;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fallback: parse message if it looks like JSON
|
|
65
|
+
if (!info.errorCode && typeof e.message === "string") {
|
|
66
|
+
const msg = e.message;
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(msg);
|
|
69
|
+
if (parsed && typeof parsed === "object") {
|
|
70
|
+
info.errorCode = parsed.errorCode;
|
|
71
|
+
info.errorMessage = parsed.message ?? info.errorMessage;
|
|
72
|
+
if (Array.isArray(parsed.errors)) {
|
|
73
|
+
info.errors = parsed.errors;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Not JSON, use as-is
|
|
78
|
+
info.errorMessage = info.errorMessage ?? msg;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Extract from standard Error properties
|
|
83
|
+
if (!info.errorMessage && typeof e.message === "string") {
|
|
84
|
+
info.errorMessage = e.message;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return info;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function formatRcApiError(info: RingCentralApiErrorInfo): string {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
|
|
93
|
+
if (info.httpStatus) parts.push(`HTTP ${info.httpStatus}`);
|
|
94
|
+
if (info.errorCode) parts.push(`ErrorCode=${info.errorCode}`);
|
|
95
|
+
if (info.requestId) parts.push(`RequestId=${info.requestId}`);
|
|
96
|
+
if (info.accountId) parts.push(`AccountId=${info.accountId}`);
|
|
97
|
+
if (info.errorMessage) parts.push(`Message="${info.errorMessage}"`);
|
|
98
|
+
|
|
99
|
+
if (info.errors && info.errors.length > 0) {
|
|
100
|
+
const errDetails = info.errors
|
|
101
|
+
.map((e) => `${e.errorCode ?? "?"}: ${e.message ?? "?"}${e.parameterName ? ` (${e.parameterName})` : ""}`)
|
|
102
|
+
.join("; ");
|
|
103
|
+
parts.push(`Details=[${errDetails}]`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts.length > 0 ? parts.join(" | ") : "Unknown error";
|
|
107
|
+
}
|
|
108
|
+
|
|
15
109
|
export async function sendRingCentralMessage(params: {
|
|
16
110
|
account: ResolvedRingCentralAccount;
|
|
17
111
|
chatId: string;
|
|
@@ -132,7 +226,7 @@ export async function uploadRingCentralAttachment(params: {
|
|
|
132
226
|
|
|
133
227
|
// Create FormData for multipart upload
|
|
134
228
|
const formData = new FormData();
|
|
135
|
-
const blob = new Blob([buffer], { type: contentType || "application/octet-stream" });
|
|
229
|
+
const blob = new Blob([new Uint8Array(buffer)], { type: contentType || "application/octet-stream" });
|
|
136
230
|
formData.append("file", blob, filename);
|
|
137
231
|
|
|
138
232
|
const response = await platform.post(
|
package/src/channel.ts
CHANGED
|
@@ -74,7 +74,7 @@ export const ringcentralDock: ChannelDock = {
|
|
|
74
74
|
resolveReplyToMode: ({ cfg }) =>
|
|
75
75
|
(cfg.channels?.ringcentral as RingCentralConfig | undefined)?.replyToMode ?? "off",
|
|
76
76
|
buildToolContext: ({ context, hasRepliedRef }) => ({
|
|
77
|
-
currentChannelId: context.To?.trim() || undefined,
|
|
77
|
+
currentChannelId: (context.To as string | undefined)?.trim() || undefined,
|
|
78
78
|
currentThreadTs: undefined,
|
|
79
79
|
hasRepliedRef,
|
|
80
80
|
}),
|
|
@@ -305,7 +305,7 @@ export const ringcentralPlugin: ChannelPlugin<ResolvedRingCentralAccount> = {
|
|
|
305
305
|
cfg: cfg as OpenClawConfig,
|
|
306
306
|
channelKey: "ringcentral",
|
|
307
307
|
accountId,
|
|
308
|
-
name: input.name,
|
|
308
|
+
name: input.name as string | undefined,
|
|
309
309
|
});
|
|
310
310
|
const next =
|
|
311
311
|
accountId !== DEFAULT_ACCOUNT_ID
|
|
@@ -315,6 +315,7 @@ export const ringcentralPlugin: ChannelPlugin<ResolvedRingCentralAccount> = {
|
|
|
315
315
|
})
|
|
316
316
|
: namedConfig;
|
|
317
317
|
// Build nested credentials block
|
|
318
|
+
const inputServer = input.server as string | undefined;
|
|
318
319
|
const credentialsPatch = input.useEnv
|
|
319
320
|
? {}
|
|
320
321
|
: {
|
|
@@ -322,11 +323,11 @@ export const ringcentralPlugin: ChannelPlugin<ResolvedRingCentralAccount> = {
|
|
|
322
323
|
...(input.clientId ? { clientId: input.clientId } : {}),
|
|
323
324
|
...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
|
|
324
325
|
...(input.jwt ? { jwt: input.jwt } : {}),
|
|
325
|
-
...(
|
|
326
|
+
...(inputServer?.trim() ? { server: inputServer.trim() } : {}),
|
|
326
327
|
},
|
|
327
328
|
};
|
|
328
329
|
// Only include credentials if it has any values
|
|
329
|
-
const hasCredentials = input.clientId || input.clientSecret || input.jwt ||
|
|
330
|
+
const hasCredentials = input.clientId || input.clientSecret || input.jwt || inputServer?.trim();
|
|
330
331
|
const configPatch = input.useEnv || !hasCredentials ? {} : credentialsPatch;
|
|
331
332
|
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
332
333
|
return {
|
package/src/monitor.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Subscriptions } from "@ringcentral/subscriptions";
|
|
2
|
+
import RcWsExtension, { Events as RcWsEvents } from "@rc-ex/ws";
|
|
3
|
+
const WebSocketExtension = RcWsExtension.default ?? RcWsExtension;
|
|
4
|
+
type WebSocketExtension = InstanceType<typeof WebSocketExtension>;
|
|
2
5
|
|
|
3
6
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
4
7
|
import { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk";
|
|
@@ -12,6 +15,8 @@ import {
|
|
|
12
15
|
downloadRingCentralAttachment,
|
|
13
16
|
uploadRingCentralAttachment,
|
|
14
17
|
getRingCentralChat,
|
|
18
|
+
extractRcApiError,
|
|
19
|
+
formatRcApiError,
|
|
15
20
|
} from "./api.js";
|
|
16
21
|
import { getRingCentralRuntime } from "./runtime.js";
|
|
17
22
|
import type {
|
|
@@ -47,6 +52,88 @@ const RECONNECT_MAX_DELAY = 300000; // 5 minutes (increased for rate limiting)
|
|
|
47
52
|
const RECONNECT_MAX_ATTEMPTS = 5; // Reduced attempts
|
|
48
53
|
const RATE_LIMIT_BACKOFF = 60000; // 1 minute backoff on 429
|
|
49
54
|
|
|
55
|
+
// WebSocket singleton per account to avoid hammering /oauth/wstoken.
|
|
56
|
+
// @ringcentral/subscriptions + @rc-ex/ws can swallow initial connect errors and
|
|
57
|
+
// repeated new Subscriptions()/newWsExtension() will trigger new wstoken calls.
|
|
58
|
+
type WsManager = {
|
|
59
|
+
key: string;
|
|
60
|
+
sdk: any;
|
|
61
|
+
subscriptions: Subscriptions;
|
|
62
|
+
rc: any;
|
|
63
|
+
wsExt: WebSocketExtension;
|
|
64
|
+
connectPromise?: Promise<void>;
|
|
65
|
+
lastConnectAt?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const wsManagers = new Map<string, WsManager>();
|
|
69
|
+
|
|
70
|
+
function buildWsManagerKey(account: ResolvedRingCentralAccount): string {
|
|
71
|
+
// Changes in credentials should force a new WS manager.
|
|
72
|
+
return `${account.clientId}:${account.server}:${account.jwt?.slice(0, 20)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function getOrCreateWsManager(
|
|
76
|
+
account: ResolvedRingCentralAccount,
|
|
77
|
+
logger: RingCentralLogger,
|
|
78
|
+
): Promise<WsManager> {
|
|
79
|
+
const key = buildWsManagerKey(account);
|
|
80
|
+
const cached = wsManagers.get(account.accountId);
|
|
81
|
+
if (cached && cached.key === key) return cached;
|
|
82
|
+
|
|
83
|
+
// Replace cache entry on credential change.
|
|
84
|
+
const sdk = await getRingCentralSDK(account);
|
|
85
|
+
const subscriptions = new Subscriptions({ sdk });
|
|
86
|
+
await (subscriptions as any).init?.();
|
|
87
|
+
|
|
88
|
+
const wsExt = new WebSocketExtension({
|
|
89
|
+
debugMode: true,
|
|
90
|
+
autoRecover: { enabled: false },
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const rc = (subscriptions as any).rc;
|
|
94
|
+
if (!rc || typeof rc.installExtension !== "function") {
|
|
95
|
+
throw new Error("Subscriptions.rc.installExtension is unavailable; cannot install WS extension");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logger.debug(`[${account.accountId}] Installing @rc-ex/ws extension (singleton)...`);
|
|
99
|
+
await rc.installExtension(wsExt);
|
|
100
|
+
|
|
101
|
+
const mgr: WsManager = { key, sdk, subscriptions, rc, wsExt };
|
|
102
|
+
wsManagers.set(account.accountId, mgr);
|
|
103
|
+
return mgr;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function ensureWsConnected(
|
|
107
|
+
mgr: WsManager,
|
|
108
|
+
account: ResolvedRingCentralAccount,
|
|
109
|
+
logger: RingCentralLogger,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
// If already connected/open, nothing to do.
|
|
112
|
+
const ws = mgr.wsExt.ws;
|
|
113
|
+
if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (mgr.connectPromise) {
|
|
117
|
+
return mgr.connectPromise;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
mgr.connectPromise = (async () => {
|
|
121
|
+
logger.debug(`[${account.accountId}] Forcing WS connect() (singleton)...`);
|
|
122
|
+
await mgr.wsExt.connect(false);
|
|
123
|
+
mgr.lastConnectAt = Date.now();
|
|
124
|
+
if (!mgr.wsExt.ws) {
|
|
125
|
+
throw new Error("WS connect() returned but wsExt.ws is still undefined");
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await mgr.connectPromise;
|
|
131
|
+
} finally {
|
|
132
|
+
mgr.connectPromise = undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
50
137
|
function trackSentMessageId(messageId: string): void {
|
|
51
138
|
recentlySentMessageIds.add(messageId);
|
|
52
139
|
setTimeout(() => recentlySentMessageIds.delete(messageId), MESSAGE_ID_TTL);
|
|
@@ -195,6 +282,7 @@ async function processMessageWithPipeline(params: {
|
|
|
195
282
|
ownerId?: string;
|
|
196
283
|
}): Promise<void> {
|
|
197
284
|
const { eventBody, account, config, runtime, core, statusSink, ownerId } = params;
|
|
285
|
+
const logger = getLogger(core);
|
|
198
286
|
const mediaMaxMb = account.config.mediaMaxMb ?? 20;
|
|
199
287
|
|
|
200
288
|
const chatId = eventBody.groupId ?? "";
|
|
@@ -225,7 +313,7 @@ async function processMessageWithPipeline(params: {
|
|
|
225
313
|
// This is because the bot uses the JWT user's identity, so we're essentially
|
|
226
314
|
// having a conversation with ourselves (the AI assistant)
|
|
227
315
|
const selfOnly = account.config.selfOnly !== false; // default true
|
|
228
|
-
|
|
316
|
+
logger.debug(`[${account.accountId}] Processing message: senderId=${senderId}, ownerId=${ownerId}, selfOnly=${selfOnly}, chatId=${chatId}`);
|
|
229
317
|
|
|
230
318
|
if (selfOnly && ownerId) {
|
|
231
319
|
if (senderId !== ownerId) {
|
|
@@ -234,7 +322,7 @@ async function processMessageWithPipeline(params: {
|
|
|
234
322
|
}
|
|
235
323
|
}
|
|
236
324
|
|
|
237
|
-
|
|
325
|
+
logger.debug(`[${account.accountId}] Message passed selfOnly check`);
|
|
238
326
|
|
|
239
327
|
// Fetch chat info to determine type
|
|
240
328
|
let chatType = "Group";
|
|
@@ -250,7 +338,7 @@ async function processMessageWithPipeline(params: {
|
|
|
250
338
|
// Personal, PersonalChat, Direct are all DM types
|
|
251
339
|
const isPersonalChat = chatType === "Personal" || chatType === "PersonalChat";
|
|
252
340
|
const isGroup = chatType !== "Direct" && chatType !== "PersonalChat" && chatType !== "Personal";
|
|
253
|
-
|
|
341
|
+
logger.debug(`[${account.accountId}] Chat type: ${chatType}, isGroup: ${isGroup}`);
|
|
254
342
|
|
|
255
343
|
// In selfOnly mode, only allow "Personal" chat (conversation with yourself)
|
|
256
344
|
if (selfOnly && !isPersonalChat) {
|
|
@@ -281,7 +369,6 @@ async function processMessageWithPipeline(params: {
|
|
|
281
369
|
if (!groupAllowlistConfigured) {
|
|
282
370
|
logVerbose(
|
|
283
371
|
core,
|
|
284
|
-
runtime,
|
|
285
372
|
`drop group message (groupPolicy=allowlist, no allowlist, chat=${chatId})`,
|
|
286
373
|
);
|
|
287
374
|
return;
|
|
@@ -452,11 +539,11 @@ async function processMessageWithPipeline(params: {
|
|
|
452
539
|
void core.channel.session
|
|
453
540
|
.recordSessionMetaFromInbound({
|
|
454
541
|
storePath,
|
|
455
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
542
|
+
sessionKey: (ctxPayload.SessionKey as string | undefined) ?? route.sessionKey,
|
|
456
543
|
ctx: ctxPayload,
|
|
457
544
|
})
|
|
458
545
|
.catch((err) => {
|
|
459
|
-
|
|
546
|
+
logger.error(`ringcentral: failed updating session meta: ${String(err)}`);
|
|
460
547
|
});
|
|
461
548
|
|
|
462
549
|
// Typing indicator disabled - respond directly without "thinking" message
|
|
@@ -470,7 +557,6 @@ async function processMessageWithPipeline(params: {
|
|
|
470
557
|
payload,
|
|
471
558
|
account,
|
|
472
559
|
chatId,
|
|
473
|
-
runtime,
|
|
474
560
|
core,
|
|
475
561
|
config,
|
|
476
562
|
statusSink,
|
|
@@ -478,7 +564,7 @@ async function processMessageWithPipeline(params: {
|
|
|
478
564
|
});
|
|
479
565
|
},
|
|
480
566
|
onError: (err, info) => {
|
|
481
|
-
|
|
567
|
+
logger.error(
|
|
482
568
|
`[${account.accountId}] RingCentral ${info.kind} reply failed: ${String(err)}`,
|
|
483
569
|
);
|
|
484
570
|
},
|
|
@@ -510,13 +596,13 @@ async function deliverRingCentralReply(params: {
|
|
|
510
596
|
payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string };
|
|
511
597
|
account: ResolvedRingCentralAccount;
|
|
512
598
|
chatId: string;
|
|
513
|
-
runtime: RingCentralRuntimeEnv;
|
|
514
599
|
core: RingCentralCoreRuntime;
|
|
515
600
|
config: OpenClawConfig;
|
|
516
601
|
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
517
602
|
typingPostId?: string;
|
|
518
603
|
}): Promise<void> {
|
|
519
|
-
const { payload, account, chatId,
|
|
604
|
+
const { payload, account, chatId, core, config, statusSink, typingPostId } = params;
|
|
605
|
+
const logger = getLogger(core);
|
|
520
606
|
const mediaList = payload.mediaUrls?.length
|
|
521
607
|
? payload.mediaUrls
|
|
522
608
|
: payload.mediaUrl
|
|
@@ -533,7 +619,8 @@ async function deliverRingCentralReply(params: {
|
|
|
533
619
|
postId: typingPostId,
|
|
534
620
|
});
|
|
535
621
|
} catch (err) {
|
|
536
|
-
|
|
622
|
+
const errInfo = formatRcApiError(extractRcApiError(err, account.accountId));
|
|
623
|
+
logger.error(`RingCentral typing cleanup failed: ${errInfo}`);
|
|
537
624
|
const fallbackText = payload.text?.trim()
|
|
538
625
|
? payload.text
|
|
539
626
|
: mediaList.length > 1
|
|
@@ -548,7 +635,8 @@ async function deliverRingCentralReply(params: {
|
|
|
548
635
|
});
|
|
549
636
|
suppressCaption = Boolean(payload.text?.trim());
|
|
550
637
|
} catch (updateErr) {
|
|
551
|
-
|
|
638
|
+
const updateErrInfo = formatRcApiError(extractRcApiError(updateErr, account.accountId));
|
|
639
|
+
logger.error(`RingCentral typing update failed: ${updateErrInfo}`);
|
|
552
640
|
}
|
|
553
641
|
}
|
|
554
642
|
}
|
|
@@ -579,7 +667,8 @@ async function deliverRingCentralReply(params: {
|
|
|
579
667
|
if (sendResult?.postId) trackSentMessageId(sendResult.postId);
|
|
580
668
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
581
669
|
} catch (err) {
|
|
582
|
-
|
|
670
|
+
const errInfo = formatRcApiError(extractRcApiError(err, account.accountId));
|
|
671
|
+
logger.error(`RingCentral attachment send failed: ${errInfo}`);
|
|
583
672
|
}
|
|
584
673
|
}
|
|
585
674
|
return;
|
|
@@ -618,7 +707,8 @@ async function deliverRingCentralReply(params: {
|
|
|
618
707
|
}
|
|
619
708
|
statusSink?.({ lastOutboundAt: Date.now() });
|
|
620
709
|
} catch (err) {
|
|
621
|
-
|
|
710
|
+
const errInfo = formatRcApiError(extractRcApiError(err, account.accountId));
|
|
711
|
+
logger.error(`RingCentral message send failed: ${errInfo}`);
|
|
622
712
|
}
|
|
623
713
|
}
|
|
624
714
|
}
|
|
@@ -637,6 +727,9 @@ export async function startRingCentralMonitor(
|
|
|
637
727
|
let isShuttingDown = false;
|
|
638
728
|
let ownerId: string | undefined;
|
|
639
729
|
|
|
730
|
+
// Avoid hammering /oauth/wstoken (auth rate limit is very low, e.g. 5/min).
|
|
731
|
+
let nextAllowedWsConnectAt = 0;
|
|
732
|
+
|
|
640
733
|
// Calculate delay with exponential backoff
|
|
641
734
|
const getReconnectDelay = () => {
|
|
642
735
|
const delay = Math.min(
|
|
@@ -652,18 +745,47 @@ export async function startRingCentralMonitor(
|
|
|
652
745
|
|
|
653
746
|
logger.info(`[${account.accountId}] Starting RingCentral WebSocket subscription...`);
|
|
654
747
|
|
|
748
|
+
if (Date.now() < nextAllowedWsConnectAt) {
|
|
749
|
+
const waitMs = nextAllowedWsConnectAt - Date.now();
|
|
750
|
+
logger.warn(
|
|
751
|
+
`[${account.accountId}] WS connect is rate-limited locally; will retry in ${Math.ceil(waitMs / 1000)}s`,
|
|
752
|
+
);
|
|
753
|
+
scheduleReconnect();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
655
757
|
try {
|
|
656
|
-
// Get
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
//
|
|
660
|
-
|
|
661
|
-
|
|
758
|
+
// Get or create WS manager (singleton per account)
|
|
759
|
+
const mgr = await getOrCreateWsManager(account, logger);
|
|
760
|
+
|
|
761
|
+
// Force connect once per account (with in-flight de-dupe)
|
|
762
|
+
await ensureWsConnected(mgr, account, logger);
|
|
763
|
+
|
|
764
|
+
const subscription = mgr.subscriptions.createSubscription();
|
|
765
|
+
|
|
766
|
+
// IMPORTANT: @ringcentral/subscriptions will create *its own* WS extension instance internally
|
|
767
|
+
// when calling subscription.register(), which can still lead to `wse.ws` undefined and
|
|
768
|
+
// addEventListener crash. We bypass it by using our singleton wsExt directly.
|
|
769
|
+
// We'll keep `subscription` object for event emitter convenience, but registration uses wsExt.
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
// Extra diagnostics: log versions so we can pin a working combo.
|
|
773
|
+
try {
|
|
774
|
+
// @ts-ignore - dynamic import of package.json for version logging
|
|
775
|
+
const sdkVer = (await import("@ringcentral/sdk/package.json", { with: { type: "json" } } as any))?.default?.version;
|
|
776
|
+
// @ts-ignore - dynamic import of package.json for version logging
|
|
777
|
+
const subsVer = (await import("@ringcentral/subscriptions/package.json", { with: { type: "json" } } as any))?.default?.version;
|
|
778
|
+
// @ts-ignore - dynamic import of package.json for version logging
|
|
779
|
+
const rcwsVer = (await import("@rc-ex/ws/package.json", { with: { type: "json" } } as any))?.default?.version;
|
|
780
|
+
logger.debug(`[${account.accountId}] rc sdk versions: @ringcentral/sdk=${sdkVer} @ringcentral/subscriptions=${subsVer} @rc-ex/ws=${rcwsVer}`);
|
|
781
|
+
} catch {
|
|
782
|
+
// ignore
|
|
783
|
+
}
|
|
662
784
|
|
|
663
785
|
// Track current user ID to filter out self messages
|
|
664
786
|
if (!ownerId) {
|
|
665
787
|
try {
|
|
666
|
-
const platform = sdk.platform();
|
|
788
|
+
const platform = mgr.sdk.platform();
|
|
667
789
|
const response = await platform.get("/restapi/v1.0/account/~/extension/~");
|
|
668
790
|
const userInfo = await response.json();
|
|
669
791
|
ownerId = userInfo?.id?.toString();
|
|
@@ -691,30 +813,57 @@ export async function startRingCentralMonitor(
|
|
|
691
813
|
});
|
|
692
814
|
|
|
693
815
|
// Subscribe to Team Messaging events and save WsSubscription for cleanup
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
816
|
+
// Register subscription via singleton wsExt (NOT via @ringcentral/subscriptions.register()).
|
|
817
|
+
// This avoids newWsExtension() being created per call.
|
|
818
|
+
const eventFilters = [
|
|
819
|
+
"/restapi/v1.0/glip/posts",
|
|
820
|
+
"/restapi/v1.0/glip/groups",
|
|
821
|
+
];
|
|
822
|
+
wsSubscription = await mgr.wsExt.subscribe(eventFilters, (event: unknown) => {
|
|
823
|
+
subscription.emit(subscription.events.notification, event);
|
|
824
|
+
});
|
|
700
825
|
|
|
701
826
|
logger.info(`[${account.accountId}] RingCentral WebSocket subscription established`);
|
|
702
827
|
reconnectAttempts = 0; // Reset on success
|
|
703
828
|
|
|
704
829
|
} catch (err) {
|
|
830
|
+
const e = err as any;
|
|
705
831
|
const errStr = String(err);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
// Check for
|
|
709
|
-
const isRateLimited = errStr.includes("429") || errStr.includes("rate") || errStr.includes("Rate");
|
|
832
|
+
const msg = e?.stack ? String(e.stack) : errStr;
|
|
833
|
+
|
|
834
|
+
// Check for auth errors - don't retry on auth failures
|
|
710
835
|
const isAuthError = errStr.includes("401") || errStr.includes("Unauthorized") || errStr.includes("invalid_grant");
|
|
711
|
-
|
|
712
836
|
if (isAuthError) {
|
|
713
837
|
logger.error(`[${account.accountId}] Authentication failed. Please check your credentials.`);
|
|
714
|
-
// Don't retry on auth errors - they won't self-resolve
|
|
715
838
|
return;
|
|
716
839
|
}
|
|
717
|
-
|
|
840
|
+
|
|
841
|
+
// If we hit auth rate limit for /oauth/wstoken, back off according to retry-after.
|
|
842
|
+
const retryAfterHeader =
|
|
843
|
+
typeof e?.response?.headers?.get === "function" ? e.response.headers.get("retry-after") :
|
|
844
|
+
typeof e?.response?.headers?.["retry-after"] === "string" ? e.response.headers["retry-after"] :
|
|
845
|
+
undefined;
|
|
846
|
+
const retryAfterMs =
|
|
847
|
+
typeof e?.retryAfter === "number" ? e.retryAfter :
|
|
848
|
+
(retryAfterHeader ? (parseInt(retryAfterHeader, 10) * 1000) : undefined);
|
|
849
|
+
|
|
850
|
+
const isRateLimited = e?.message === "Request rate exceeded" || e?.response?.status === 429 ||
|
|
851
|
+
errStr.includes("429") || errStr.includes("rate") || errStr.includes("Rate");
|
|
852
|
+
|
|
853
|
+
if (isRateLimited) {
|
|
854
|
+
const backoffMs = Number.isFinite(retryAfterMs) && retryAfterMs! > 0 ? retryAfterMs! : RATE_LIMIT_BACKOFF;
|
|
855
|
+
nextAllowedWsConnectAt = Date.now() + backoffMs;
|
|
856
|
+
logger.error(
|
|
857
|
+
`[${account.accountId}] WS connect failed due to rate limit (wstoken). ` +
|
|
858
|
+
`Backing off for ${Math.ceil(backoffMs / 1000)}s before retrying.`,
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
logger.error(
|
|
863
|
+
`[${account.accountId}] WS subscription failed. ` +
|
|
864
|
+
`Reason=${e?.name ?? 'Error'}: ${e?.message ?? errStr}\n` +
|
|
865
|
+
`Stack:\n${msg}`,
|
|
866
|
+
);
|
|
718
867
|
scheduleReconnect(isRateLimited);
|
|
719
868
|
}
|
|
720
869
|
};
|
package/src/openclaw.d.ts
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
declare module "openclaw/plugin-sdk" {
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
-
// Types
|
|
4
|
+
// Agent Types
|
|
5
|
+
export type AgentConfig = {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Base Types
|
|
5
12
|
export type OpenClawConfig = {
|
|
6
|
-
channels?:
|
|
13
|
+
channels?: {
|
|
14
|
+
defaults?: {
|
|
15
|
+
groupPolicy?: GroupPolicy;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
ringcentral?: {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
accounts?: Record<string, unknown>;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
};
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
};
|
|
25
|
+
agents?: {
|
|
26
|
+
list?: AgentConfig[];
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
};
|
|
29
|
+
commands?: {
|
|
30
|
+
useAccessGroups?: boolean;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
session?: {
|
|
34
|
+
store?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
7
37
|
[key: string]: unknown;
|
|
8
38
|
};
|
|
9
39
|
|
|
@@ -20,47 +50,344 @@ declare module "openclaw/plugin-sdk" {
|
|
|
20
50
|
error: (message: string) => void;
|
|
21
51
|
};
|
|
22
52
|
|
|
53
|
+
// Channel Runtime Types
|
|
54
|
+
export type ChannelRuntime = {
|
|
55
|
+
text: {
|
|
56
|
+
chunkMarkdownText: (text: string, limit: number) => string[];
|
|
57
|
+
chunkMarkdownTextWithMode: (text: string, limit: number, mode: string) => string[];
|
|
58
|
+
resolveChunkMode: (config: OpenClawConfig, channel: string, accountId: string) => string;
|
|
59
|
+
hasControlCommand: (body: string, config: OpenClawConfig) => boolean;
|
|
60
|
+
};
|
|
61
|
+
media: {
|
|
62
|
+
fetchRemoteMedia: (url: string, opts: { maxBytes?: number }) => Promise<{
|
|
63
|
+
buffer: Buffer;
|
|
64
|
+
filename?: string;
|
|
65
|
+
contentType?: string;
|
|
66
|
+
}>;
|
|
67
|
+
saveTempMedia: (opts: { buffer: Buffer; contentType?: string; filename?: string }) => Promise<string>;
|
|
68
|
+
saveMediaBuffer: (buffer: Buffer, contentType: string | undefined, direction: string, maxBytes: number, filename?: string) => Promise<{ path: string; contentType?: string }>;
|
|
69
|
+
};
|
|
70
|
+
commands: {
|
|
71
|
+
shouldComputeCommandAuthorized: (body: string, config: OpenClawConfig) => boolean;
|
|
72
|
+
shouldHandleTextCommands: (opts: { cfg: OpenClawConfig; surface: string }) => boolean;
|
|
73
|
+
isControlCommandMessage: (body: string, config: OpenClawConfig) => boolean;
|
|
74
|
+
resolveCommandAuthorizedFromAuthorizers: (opts: {
|
|
75
|
+
useAccessGroups: boolean;
|
|
76
|
+
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
|
77
|
+
}) => boolean | undefined;
|
|
78
|
+
};
|
|
79
|
+
pairing: {
|
|
80
|
+
readAllowFromStore: (channel: string) => Promise<string[]>;
|
|
81
|
+
};
|
|
82
|
+
routing: {
|
|
83
|
+
resolveAgentRoute: (opts: {
|
|
84
|
+
cfg: OpenClawConfig;
|
|
85
|
+
channel: string;
|
|
86
|
+
accountId: string;
|
|
87
|
+
peer: { kind: string; id: string };
|
|
88
|
+
}) => { agentId: string; sessionKey: string; accountId: string };
|
|
89
|
+
};
|
|
90
|
+
session: {
|
|
91
|
+
resolveStorePath: (store: string | undefined, opts: { agentId: string }) => string;
|
|
92
|
+
readSessionUpdatedAt: (opts: { storePath: string; sessionKey: string }) => number | undefined;
|
|
93
|
+
recordSessionMetaFromInbound: (opts: { storePath: string; sessionKey: string; ctx: Record<string, unknown> }) => Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
reply: {
|
|
96
|
+
resolveEnvelopeFormatOptions: (config: OpenClawConfig) => Record<string, unknown>;
|
|
97
|
+
formatAgentEnvelope: (opts: {
|
|
98
|
+
channel: string;
|
|
99
|
+
from: string;
|
|
100
|
+
timestamp?: number;
|
|
101
|
+
previousTimestamp?: number;
|
|
102
|
+
envelope: Record<string, unknown>;
|
|
103
|
+
body: string;
|
|
104
|
+
}) => string;
|
|
105
|
+
finalizeInboundContext: (payload: Record<string, unknown>) => Record<string, unknown>;
|
|
106
|
+
dispatchReplyWithBufferedBlockDispatcher: (opts: {
|
|
107
|
+
ctx: Record<string, unknown>;
|
|
108
|
+
cfg: OpenClawConfig;
|
|
109
|
+
dispatcherOptions: {
|
|
110
|
+
deliver: (payload: { text?: string; mediaUrl?: string; error?: Error }) => Promise<void>;
|
|
111
|
+
onError: (err: unknown, info: { kind: string }) => void;
|
|
112
|
+
};
|
|
113
|
+
}) => Promise<void>;
|
|
114
|
+
};
|
|
115
|
+
inbound: {
|
|
116
|
+
handleInbound: (opts: {
|
|
117
|
+
channel: string;
|
|
118
|
+
context: Record<string, unknown>;
|
|
119
|
+
replyFn: (response: { message?: string; error?: Error }, info: Record<string, unknown>) => Promise<void>;
|
|
120
|
+
agentRoute?: { agentId: string; sessionKey: string };
|
|
121
|
+
sessionPath?: string;
|
|
122
|
+
groupSystemPrompt?: string;
|
|
123
|
+
abortSignal?: AbortSignal;
|
|
124
|
+
}) => Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
groups: {
|
|
127
|
+
resolveGroupConfig: (opts: {
|
|
128
|
+
channel: string;
|
|
129
|
+
groupId: string;
|
|
130
|
+
accountId: string;
|
|
131
|
+
cfg: OpenClawConfig;
|
|
132
|
+
}) => {
|
|
133
|
+
isAllowed: boolean;
|
|
134
|
+
allowlist: string[];
|
|
135
|
+
users: string[];
|
|
136
|
+
entry?: { requireMention?: boolean; systemPrompt?: string; [key: string]: unknown };
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
23
141
|
export type PluginRuntime = {
|
|
24
142
|
logging: {
|
|
25
143
|
shouldLogVerbose: () => boolean;
|
|
26
144
|
getChildLogger: (bindings: Record<string, unknown>, opts?: { level?: string }) => PluginLogger;
|
|
27
145
|
};
|
|
146
|
+
channel: ChannelRuntime;
|
|
28
147
|
[key: string]: unknown;
|
|
29
148
|
};
|
|
30
149
|
|
|
31
|
-
|
|
32
|
-
|
|
150
|
+
// Policy Types
|
|
151
|
+
export type DmPolicy = "open" | "allowlist" | "pairing" | "disabled";
|
|
152
|
+
export type GroupPolicy = "open" | "allowlist" | "disabled";
|
|
153
|
+
export type MarkdownConfig = {
|
|
154
|
+
enabled?: boolean;
|
|
33
155
|
[key: string]: unknown;
|
|
34
156
|
};
|
|
35
157
|
|
|
158
|
+
// Channel Dock Type
|
|
36
159
|
export type ChannelDock = {
|
|
37
160
|
id: string;
|
|
38
|
-
|
|
161
|
+
capabilities: {
|
|
162
|
+
chatTypes: string[];
|
|
163
|
+
reactions: boolean;
|
|
164
|
+
media: boolean;
|
|
165
|
+
threads: boolean;
|
|
166
|
+
blockStreaming: boolean;
|
|
167
|
+
};
|
|
168
|
+
outbound?: {
|
|
169
|
+
textChunkLimit?: number;
|
|
170
|
+
};
|
|
171
|
+
config?: {
|
|
172
|
+
resolveAllowFrom?: (opts: { cfg: OpenClawConfig; accountId: string }) => string[];
|
|
173
|
+
formatAllowFrom?: (opts: { allowFrom: string[] }) => string[];
|
|
174
|
+
};
|
|
175
|
+
groups?: {
|
|
176
|
+
resolveRequireMention?: (opts: { cfg: OpenClawConfig; accountId: string }) => boolean;
|
|
177
|
+
};
|
|
178
|
+
threading?: {
|
|
179
|
+
resolveReplyToMode?: (opts: { cfg: OpenClawConfig }) => string;
|
|
180
|
+
buildToolContext?: (opts: { context: Record<string, unknown>; hasRepliedRef: { current: boolean } }) => Record<string, unknown>;
|
|
181
|
+
};
|
|
39
182
|
};
|
|
40
183
|
|
|
41
|
-
|
|
42
|
-
export type
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
184
|
+
// Channel Plugin Types
|
|
185
|
+
export type ChannelPluginMeta = {
|
|
186
|
+
id: string;
|
|
187
|
+
label: string;
|
|
188
|
+
selectionLabel?: string;
|
|
189
|
+
docsPath?: string;
|
|
190
|
+
docsLabel?: string;
|
|
191
|
+
blurb?: string;
|
|
192
|
+
order?: number;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export type ChannelPluginCapabilities = {
|
|
196
|
+
chatTypes: string[];
|
|
197
|
+
reactions: boolean;
|
|
198
|
+
threads: boolean;
|
|
199
|
+
media: boolean;
|
|
200
|
+
nativeCommands?: boolean;
|
|
201
|
+
blockStreaming?: boolean;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export type ChannelPluginPairing<TAccount> = {
|
|
205
|
+
idLabel: string;
|
|
206
|
+
normalizeAllowEntry: (entry: string) => string;
|
|
207
|
+
notifyApproval: (opts: { cfg: OpenClawConfig; id: string; account?: TAccount }) => Promise<void>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type ChannelPluginConfig<TAccount> = {
|
|
211
|
+
listAccountIds: (cfg: OpenClawConfig) => string[];
|
|
212
|
+
resolveAccount: (cfg: OpenClawConfig, accountId?: string) => TAccount;
|
|
213
|
+
defaultAccountId: (cfg: OpenClawConfig) => string;
|
|
214
|
+
setAccountEnabled: (opts: { cfg: OpenClawConfig; accountId: string; enabled: boolean }) => OpenClawConfig;
|
|
215
|
+
deleteAccount: (opts: { cfg: OpenClawConfig; accountId: string }) => OpenClawConfig;
|
|
216
|
+
isConfigured: (account: TAccount) => boolean;
|
|
217
|
+
describeAccount: (account: TAccount) => Record<string, unknown>;
|
|
218
|
+
resolveAllowFrom: (opts: { cfg: OpenClawConfig; accountId: string }) => string[];
|
|
219
|
+
formatAllowFrom: (opts: { allowFrom: string[] }) => string[];
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export type ChannelPluginSecurity<TAccount> = {
|
|
223
|
+
resolveDmPolicy: (opts: { cfg: OpenClawConfig; accountId?: string; account: TAccount }) => {
|
|
224
|
+
policy: DmPolicy;
|
|
225
|
+
allowFrom: (string | number)[];
|
|
226
|
+
allowFromPath: string;
|
|
227
|
+
approveHint: string;
|
|
228
|
+
normalizeEntry: (raw: string) => string;
|
|
229
|
+
};
|
|
230
|
+
collectWarnings: (opts: { account: TAccount; cfg: OpenClawConfig }) => string[];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export type ChannelPluginGroups = {
|
|
234
|
+
resolveRequireMention: (opts: { cfg: OpenClawConfig; accountId: string }) => boolean;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export type ChannelPluginThreading = {
|
|
238
|
+
resolveReplyToMode: (opts: { cfg: OpenClawConfig }) => string;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export type ChannelPluginMessaging = {
|
|
242
|
+
normalizeTarget: (target: string) => string | null;
|
|
243
|
+
targetResolver: {
|
|
244
|
+
looksLikeId: (raw: string, normalized: string | null) => boolean;
|
|
245
|
+
hint: string;
|
|
246
|
+
};
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export type DirectoryPeer = { kind: "user"; id: string };
|
|
250
|
+
export type DirectoryGroup = { kind: "group"; id: string };
|
|
251
|
+
|
|
252
|
+
export type ChannelPluginDirectory<TAccount> = {
|
|
253
|
+
self: (opts: { account: TAccount }) => Promise<{ id: string; name?: string } | null>;
|
|
254
|
+
listPeers: (opts: { cfg: OpenClawConfig; accountId: string; query?: string; limit?: number }) => Promise<DirectoryPeer[]>;
|
|
255
|
+
listGroups: (opts: { cfg: OpenClawConfig; accountId: string; query?: string; limit?: number }) => Promise<DirectoryGroup[]>;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export type ResolvedTarget = { input: string; resolved: boolean; id?: string; note?: string };
|
|
259
|
+
|
|
260
|
+
export type ChannelPluginResolver = {
|
|
261
|
+
resolveTargets: (opts: { inputs: string[]; kind: "user" | "group" }) => Promise<ResolvedTarget[]>;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export type ChannelPluginSetup = {
|
|
265
|
+
resolveAccountId: (opts: { accountId?: string }) => string;
|
|
266
|
+
applyAccountName: (opts: { cfg: OpenClawConfig; accountId: string; name?: string }) => OpenClawConfig;
|
|
267
|
+
validateInput: (opts: { accountId: string; input: Record<string, unknown> }) => string | null;
|
|
268
|
+
applyAccountConfig: (opts: { cfg: OpenClawConfig; accountId: string; input: Record<string, unknown> }) => OpenClawConfig;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export type OutboundResult = {
|
|
272
|
+
channel: string;
|
|
273
|
+
messageId: string;
|
|
274
|
+
chatId: string;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type ChannelPluginOutbound<TAccount> = {
|
|
278
|
+
deliveryMode: "direct" | "queued";
|
|
279
|
+
chunker: (text: string, limit: number) => string[];
|
|
280
|
+
chunkerMode?: "markdown" | "plain";
|
|
281
|
+
textChunkLimit: number;
|
|
282
|
+
resolveTarget: (opts: { to?: string; allowFrom?: string[]; mode?: string }) => { ok: true; to: string } | { ok: false; error: Error };
|
|
283
|
+
sendText: (opts: { cfg: OpenClawConfig; to: string; text: string; accountId?: string }) => Promise<OutboundResult>;
|
|
284
|
+
sendMedia: (opts: { cfg: OpenClawConfig; to: string; text?: string; mediaUrl?: string; accountId?: string }) => Promise<OutboundResult>;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export type StatusIssue = {
|
|
288
|
+
channel: string;
|
|
289
|
+
accountId: string;
|
|
290
|
+
kind: string;
|
|
291
|
+
message: string;
|
|
292
|
+
fix?: string;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export type ChannelPluginStatus<TAccount> = {
|
|
296
|
+
defaultRuntime: Record<string, unknown>;
|
|
297
|
+
collectStatusIssues: (accounts: Array<Record<string, unknown>>) => StatusIssue[];
|
|
298
|
+
buildChannelSummary: (opts: { snapshot: Record<string, unknown> }) => Record<string, unknown>;
|
|
299
|
+
probeAccount: (opts: { account: TAccount }) => Promise<{ ok: boolean; error?: string; elapsedMs: number }>;
|
|
300
|
+
buildAccountSnapshot: (opts: { account: TAccount; runtime?: Record<string, unknown>; probe?: Record<string, unknown> }) => Record<string, unknown>;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export type GatewayContext<TAccount> = {
|
|
304
|
+
account: TAccount;
|
|
305
|
+
cfg: OpenClawConfig;
|
|
306
|
+
runtime: { log?: (msg: string) => void; error?: (msg: string) => void; info?: (msg: string) => void };
|
|
307
|
+
abortSignal: AbortSignal;
|
|
308
|
+
setStatus: (patch: Record<string, unknown>) => void;
|
|
309
|
+
log?: PluginLogger;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export type ChannelPluginGateway<TAccount> = {
|
|
313
|
+
startAccount: (ctx: GatewayContext<TAccount>) => Promise<() => void>;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
317
|
+
export type ChannelPlugin<TAccount = any> = {
|
|
318
|
+
id: string;
|
|
319
|
+
meta: ChannelPluginMeta;
|
|
320
|
+
pairing?: ChannelPluginPairing<TAccount>;
|
|
321
|
+
capabilities: ChannelPluginCapabilities;
|
|
322
|
+
streaming?: {
|
|
323
|
+
blockStreamingCoalesceDefaults?: { minChars?: number; idleMs?: number };
|
|
324
|
+
};
|
|
325
|
+
reload?: {
|
|
326
|
+
configPrefixes?: string[];
|
|
327
|
+
};
|
|
328
|
+
configSchema: z.ZodType<unknown>;
|
|
329
|
+
config: ChannelPluginConfig<TAccount>;
|
|
330
|
+
security?: ChannelPluginSecurity<TAccount>;
|
|
331
|
+
groups?: ChannelPluginGroups;
|
|
332
|
+
threading?: ChannelPluginThreading;
|
|
333
|
+
messaging?: ChannelPluginMessaging;
|
|
334
|
+
directory?: ChannelPluginDirectory<TAccount>;
|
|
335
|
+
resolver?: ChannelPluginResolver;
|
|
336
|
+
setup?: ChannelPluginSetup;
|
|
337
|
+
outbound: ChannelPluginOutbound<TAccount>;
|
|
338
|
+
status?: ChannelPluginStatus<TAccount>;
|
|
339
|
+
gateway?: ChannelPluginGateway<TAccount>;
|
|
46
340
|
};
|
|
47
341
|
|
|
48
342
|
// Constants
|
|
49
343
|
export const DEFAULT_ACCOUNT_ID: string;
|
|
50
344
|
|
|
51
345
|
// Functions
|
|
52
|
-
export function normalizeAccountId(accountId: string | undefined): string;
|
|
346
|
+
export function normalizeAccountId(accountId: string | null | undefined): string;
|
|
53
347
|
export function emptyPluginConfigSchema(): z.ZodObject<Record<string, never>>;
|
|
54
|
-
export function resolveMentionGatingWithBypass(opts:
|
|
348
|
+
export function resolveMentionGatingWithBypass(opts: {
|
|
349
|
+
isGroup: boolean;
|
|
350
|
+
requireMention: boolean;
|
|
351
|
+
canDetectMention: boolean;
|
|
352
|
+
wasMentioned: boolean;
|
|
353
|
+
implicitMention: boolean;
|
|
354
|
+
hasAnyMention: boolean;
|
|
355
|
+
allowTextCommands: boolean;
|
|
356
|
+
hasControlCommand: boolean;
|
|
357
|
+
commandAuthorized: boolean;
|
|
358
|
+
}): { shouldSkip: boolean; effectiveWasMentioned?: boolean };
|
|
55
359
|
export function requireOpenAllowFrom(opts: unknown): void;
|
|
56
|
-
export function applyAccountNameToChannelSection(opts:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export function
|
|
63
|
-
export function
|
|
360
|
+
export function applyAccountNameToChannelSection(opts: {
|
|
361
|
+
cfg: OpenClawConfig;
|
|
362
|
+
channelKey: string;
|
|
363
|
+
accountId: string;
|
|
364
|
+
name?: string;
|
|
365
|
+
}): OpenClawConfig;
|
|
366
|
+
export function buildChannelConfigSchema(schema: z.ZodType<unknown>): z.ZodType<unknown>;
|
|
367
|
+
export function deleteAccountFromConfigSection(opts: {
|
|
368
|
+
cfg: OpenClawConfig;
|
|
369
|
+
sectionKey: string;
|
|
370
|
+
accountId: string;
|
|
371
|
+
clearBaseFields?: string[];
|
|
372
|
+
}): OpenClawConfig;
|
|
373
|
+
export function formatPairingApproveHint(channel: string): string;
|
|
374
|
+
export function migrateBaseNameToDefaultAccount(opts: {
|
|
375
|
+
cfg: OpenClawConfig;
|
|
376
|
+
channelKey: string;
|
|
377
|
+
}): OpenClawConfig;
|
|
378
|
+
export function missingTargetError(channel: string, hint: string): Error;
|
|
379
|
+
export function setAccountEnabledInConfigSection(opts: {
|
|
380
|
+
cfg: OpenClawConfig;
|
|
381
|
+
sectionKey: string;
|
|
382
|
+
accountId: string;
|
|
383
|
+
enabled: boolean;
|
|
384
|
+
allowTopLevel?: boolean;
|
|
385
|
+
}): OpenClawConfig;
|
|
386
|
+
export function resolveChannelMediaMaxBytes(opts: {
|
|
387
|
+
cfg: OpenClawConfig;
|
|
388
|
+
resolveChannelLimitMb: (opts: { cfg: OpenClawConfig; accountId: string }) => number | undefined;
|
|
389
|
+
accountId?: string;
|
|
390
|
+
}): number | undefined;
|
|
64
391
|
|
|
65
392
|
// Constants for messages
|
|
66
393
|
export const PAIRING_APPROVED_MESSAGE: string;
|