openclaw-ringcentral 2026.1.29-beta2 → 2026.1.30-beta.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/package.json +1 -1
- package/src/monitor.ts +68 -32
- package/src/openclaw.d.ts +10 -5
package/package.json
CHANGED
package/src/monitor.ts
CHANGED
|
@@ -21,19 +21,31 @@ import type {
|
|
|
21
21
|
RingCentralMention,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
|
|
24
|
+
export type RingCentralLogger = {
|
|
25
|
+
debug: (message: string) => void;
|
|
26
|
+
info: (message: string) => void;
|
|
27
|
+
warn: (message: string) => void;
|
|
28
|
+
error: (message: string) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
24
31
|
export type RingCentralRuntimeEnv = {
|
|
25
32
|
log?: (message: string) => void;
|
|
26
33
|
error?: (message: string) => void;
|
|
27
34
|
};
|
|
28
35
|
|
|
36
|
+
function createLogger(core: RingCentralCoreRuntime): RingCentralLogger {
|
|
37
|
+
return core.logging.getChildLogger({ plugin: "ringcentral" });
|
|
38
|
+
}
|
|
39
|
+
|
|
29
40
|
// Track recently sent message IDs to avoid processing bot's own replies
|
|
30
41
|
const recentlySentMessageIds = new Set<string>();
|
|
31
42
|
const MESSAGE_ID_TTL = 60000; // 60 seconds
|
|
32
43
|
|
|
33
44
|
// Reconnection settings
|
|
34
|
-
const RECONNECT_INITIAL_DELAY =
|
|
35
|
-
const RECONNECT_MAX_DELAY =
|
|
36
|
-
const RECONNECT_MAX_ATTEMPTS =
|
|
45
|
+
const RECONNECT_INITIAL_DELAY = 5000; // 5 seconds (increased to avoid 429)
|
|
46
|
+
const RECONNECT_MAX_DELAY = 300000; // 5 minutes (increased for rate limiting)
|
|
47
|
+
const RECONNECT_MAX_ATTEMPTS = 5; // Reduced attempts
|
|
48
|
+
const RATE_LIMIT_BACKOFF = 60000; // 1 minute backoff on 429
|
|
37
49
|
|
|
38
50
|
function trackSentMessageId(messageId: string): void {
|
|
39
51
|
recentlySentMessageIds.add(messageId);
|
|
@@ -54,13 +66,22 @@ export type RingCentralMonitorOptions = {
|
|
|
54
66
|
|
|
55
67
|
type RingCentralCoreRuntime = ReturnType<typeof getRingCentralRuntime>;
|
|
56
68
|
|
|
69
|
+
// Shared logger instance (lazy initialized)
|
|
70
|
+
let sharedLogger: RingCentralLogger | null = null;
|
|
71
|
+
|
|
72
|
+
function getLogger(core: RingCentralCoreRuntime): RingCentralLogger {
|
|
73
|
+
if (!sharedLogger) {
|
|
74
|
+
sharedLogger = createLogger(core);
|
|
75
|
+
}
|
|
76
|
+
return sharedLogger;
|
|
77
|
+
}
|
|
78
|
+
|
|
57
79
|
function logVerbose(
|
|
58
80
|
core: RingCentralCoreRuntime,
|
|
59
|
-
runtime: RingCentralRuntimeEnv,
|
|
60
81
|
message: string,
|
|
61
82
|
) {
|
|
62
83
|
if (core.logging.shouldLogVerbose()) {
|
|
63
|
-
|
|
84
|
+
getLogger(core).debug(message);
|
|
64
85
|
}
|
|
65
86
|
}
|
|
66
87
|
|
|
@@ -190,13 +211,13 @@ async function processMessageWithPipeline(params: {
|
|
|
190
211
|
// Check 1: Skip if this is a message we recently sent
|
|
191
212
|
const messageId = eventBody.id ?? "";
|
|
192
213
|
if (messageId && isOwnSentMessage(messageId)) {
|
|
193
|
-
logVerbose(core,
|
|
214
|
+
logVerbose(core, `skip own sent message: ${messageId}`);
|
|
194
215
|
return;
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
// Check 2: Skip typing/thinking indicators (pattern-based)
|
|
198
219
|
if (rawBody.includes("thinking...") || rawBody.includes("typing...")) {
|
|
199
|
-
logVerbose(core,
|
|
220
|
+
logVerbose(core, "skip typing indicator message");
|
|
200
221
|
return;
|
|
201
222
|
}
|
|
202
223
|
|
|
@@ -208,7 +229,7 @@ async function processMessageWithPipeline(params: {
|
|
|
208
229
|
|
|
209
230
|
if (selfOnly && ownerId) {
|
|
210
231
|
if (senderId !== ownerId) {
|
|
211
|
-
logVerbose(core,
|
|
232
|
+
logVerbose(core, `ignore message from non-owner: ${senderId} (selfOnly mode)`);
|
|
212
233
|
return;
|
|
213
234
|
}
|
|
214
235
|
}
|
|
@@ -233,7 +254,7 @@ async function processMessageWithPipeline(params: {
|
|
|
233
254
|
|
|
234
255
|
// In selfOnly mode, only allow "Personal" chat (conversation with yourself)
|
|
235
256
|
if (selfOnly && !isPersonalChat) {
|
|
236
|
-
logVerbose(core,
|
|
257
|
+
logVerbose(core, `ignore non-personal chat in selfOnly mode: chatType=${chatType}`);
|
|
237
258
|
return;
|
|
238
259
|
}
|
|
239
260
|
|
|
@@ -250,7 +271,7 @@ async function processMessageWithPipeline(params: {
|
|
|
250
271
|
|
|
251
272
|
if (isGroup) {
|
|
252
273
|
if (groupPolicy === "disabled") {
|
|
253
|
-
logVerbose(core,
|
|
274
|
+
logVerbose(core, `drop group message (groupPolicy=disabled, chat=${chatId})`);
|
|
254
275
|
return;
|
|
255
276
|
}
|
|
256
277
|
const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
|
|
@@ -266,19 +287,19 @@ async function processMessageWithPipeline(params: {
|
|
|
266
287
|
return;
|
|
267
288
|
}
|
|
268
289
|
if (!groupAllowed) {
|
|
269
|
-
logVerbose(core,
|
|
290
|
+
logVerbose(core, `drop group message (not allowlisted, chat=${chatId})`);
|
|
270
291
|
return;
|
|
271
292
|
}
|
|
272
293
|
}
|
|
273
294
|
if (groupEntry?.enabled === false || groupEntry?.allow === false) {
|
|
274
|
-
logVerbose(core,
|
|
295
|
+
logVerbose(core, `drop group message (chat disabled, chat=${chatId})`);
|
|
275
296
|
return;
|
|
276
297
|
}
|
|
277
298
|
|
|
278
299
|
if (groupUsers.length > 0) {
|
|
279
300
|
const ok = isSenderAllowed(senderId, groupUsers.map((v) => String(v)));
|
|
280
301
|
if (!ok) {
|
|
281
|
-
logVerbose(core,
|
|
302
|
+
logVerbose(core, `drop group message (sender not allowed, ${senderId})`);
|
|
282
303
|
return;
|
|
283
304
|
}
|
|
284
305
|
}
|
|
@@ -330,7 +351,7 @@ async function processMessageWithPipeline(params: {
|
|
|
330
351
|
// Plugin only handles mention gating; AI decides whether to respond or NO_REPLY
|
|
331
352
|
|
|
332
353
|
if (mentionGate.shouldSkip) {
|
|
333
|
-
logVerbose(core,
|
|
354
|
+
logVerbose(core, `drop group message (mention required, chat=${chatId})`);
|
|
334
355
|
return;
|
|
335
356
|
}
|
|
336
357
|
}
|
|
@@ -341,11 +362,11 @@ async function processMessageWithPipeline(params: {
|
|
|
341
362
|
if (!isGroup && !selfOnly) {
|
|
342
363
|
// Non-selfOnly mode: check dmPolicy and allowFrom
|
|
343
364
|
if (dmPolicy === "disabled") {
|
|
344
|
-
logVerbose(core,
|
|
365
|
+
logVerbose(core, `ignore DM (dmPolicy=disabled)`);
|
|
345
366
|
return;
|
|
346
367
|
}
|
|
347
368
|
if (dmPolicy === "allowlist" && !isSenderAllowed(senderId, effectiveAllowFrom)) {
|
|
348
|
-
logVerbose(core,
|
|
369
|
+
logVerbose(core, `ignore DM from ${senderId} (not in allowFrom)`);
|
|
349
370
|
return;
|
|
350
371
|
}
|
|
351
372
|
}
|
|
@@ -355,7 +376,7 @@ async function processMessageWithPipeline(params: {
|
|
|
355
376
|
core.channel.commands.isControlCommandMessage(rawBody, config) &&
|
|
356
377
|
commandAuthorized !== true
|
|
357
378
|
) {
|
|
358
|
-
logVerbose(core,
|
|
379
|
+
logVerbose(core, `ringcentral: drop control command from ${senderId}`);
|
|
359
380
|
return;
|
|
360
381
|
}
|
|
361
382
|
|
|
@@ -608,6 +629,7 @@ export async function startRingCentralMonitor(
|
|
|
608
629
|
): Promise<() => void> {
|
|
609
630
|
const { account, config, runtime, abortSignal, statusSink } = options;
|
|
610
631
|
const core = getRingCentralRuntime();
|
|
632
|
+
const logger = createLogger(core);
|
|
611
633
|
|
|
612
634
|
let wsSubscription: Awaited<ReturnType<ReturnType<InstanceType<typeof Subscriptions>["createSubscription"]>["register"]>> | null = null;
|
|
613
635
|
let reconnectAttempts = 0;
|
|
@@ -628,7 +650,7 @@ export async function startRingCentralMonitor(
|
|
|
628
650
|
const createSubscription = async (): Promise<void> => {
|
|
629
651
|
if (isShuttingDown || abortSignal.aborted) return;
|
|
630
652
|
|
|
631
|
-
|
|
653
|
+
logger.info(`[${account.accountId}] Starting RingCentral WebSocket subscription...`);
|
|
632
654
|
|
|
633
655
|
try {
|
|
634
656
|
// Get SDK instance
|
|
@@ -645,15 +667,15 @@ export async function startRingCentralMonitor(
|
|
|
645
667
|
const response = await platform.get("/restapi/v1.0/account/~/extension/~");
|
|
646
668
|
const userInfo = await response.json();
|
|
647
669
|
ownerId = userInfo?.id?.toString();
|
|
648
|
-
|
|
670
|
+
logger.info(`[${account.accountId}] Authenticated as extension: ${ownerId}`);
|
|
649
671
|
} catch (err) {
|
|
650
|
-
|
|
672
|
+
logger.error(`[${account.accountId}] Failed to get current user: ${String(err)}`);
|
|
651
673
|
}
|
|
652
674
|
}
|
|
653
675
|
|
|
654
676
|
// Handle notifications
|
|
655
677
|
subscription.on(subscription.events.notification, (event: unknown) => {
|
|
656
|
-
|
|
678
|
+
logger.debug(`WebSocket notification received: ${JSON.stringify(event).slice(0, 500)}`);
|
|
657
679
|
const evt = event as RingCentralWebhookEvent;
|
|
658
680
|
processWebSocketEvent({
|
|
659
681
|
event: evt,
|
|
@@ -664,7 +686,7 @@ export async function startRingCentralMonitor(
|
|
|
664
686
|
statusSink,
|
|
665
687
|
ownerId,
|
|
666
688
|
}).catch((err) => {
|
|
667
|
-
|
|
689
|
+
logger.error(`[${account.accountId}] WebSocket event processing failed: ${String(err)}`);
|
|
668
690
|
});
|
|
669
691
|
});
|
|
670
692
|
|
|
@@ -676,26 +698,40 @@ export async function startRingCentralMonitor(
|
|
|
676
698
|
])
|
|
677
699
|
.register();
|
|
678
700
|
|
|
679
|
-
|
|
701
|
+
logger.info(`[${account.accountId}] RingCentral WebSocket subscription established`);
|
|
680
702
|
reconnectAttempts = 0; // Reset on success
|
|
681
703
|
|
|
682
704
|
} catch (err) {
|
|
683
|
-
|
|
684
|
-
|
|
705
|
+
const errStr = String(err);
|
|
706
|
+
logger.error(`[${account.accountId}] Failed to create WebSocket subscription: ${errStr}`);
|
|
707
|
+
|
|
708
|
+
// Check for rate limiting (429) or auth errors
|
|
709
|
+
const isRateLimited = errStr.includes("429") || errStr.includes("rate") || errStr.includes("Rate");
|
|
710
|
+
const isAuthError = errStr.includes("401") || errStr.includes("Unauthorized") || errStr.includes("invalid_grant");
|
|
711
|
+
|
|
712
|
+
if (isAuthError) {
|
|
713
|
+
logger.error(`[${account.accountId}] Authentication failed. Please check your credentials.`);
|
|
714
|
+
// Don't retry on auth errors - they won't self-resolve
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
scheduleReconnect(isRateLimited);
|
|
685
719
|
}
|
|
686
720
|
};
|
|
687
721
|
|
|
688
722
|
// Schedule reconnection with exponential backoff
|
|
689
|
-
const scheduleReconnect = () => {
|
|
723
|
+
const scheduleReconnect = (isRateLimited = false) => {
|
|
690
724
|
if (isShuttingDown || abortSignal.aborted) return;
|
|
691
725
|
if (reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
|
|
692
|
-
|
|
726
|
+
logger.error(`[${account.accountId}] Max reconnection attempts (${RECONNECT_MAX_ATTEMPTS}) reached. Giving up.`);
|
|
693
727
|
return;
|
|
694
728
|
}
|
|
695
729
|
|
|
696
|
-
|
|
730
|
+
// Use longer delay if rate limited
|
|
731
|
+
const baseDelay = isRateLimited ? RATE_LIMIT_BACKOFF : getReconnectDelay();
|
|
732
|
+
const delay = Math.min(baseDelay, RECONNECT_MAX_DELAY);
|
|
697
733
|
reconnectAttempts++;
|
|
698
|
-
|
|
734
|
+
logger.warn(`[${account.accountId}] Scheduling reconnection attempt ${reconnectAttempts}/${RECONNECT_MAX_ATTEMPTS} in ${delay}ms${isRateLimited ? " (rate limited)" : ""}...`);
|
|
699
735
|
|
|
700
736
|
// Clean up existing WsSubscription
|
|
701
737
|
if (wsSubscription) {
|
|
@@ -706,7 +742,7 @@ export async function startRingCentralMonitor(
|
|
|
706
742
|
reconnectTimeout = setTimeout(() => {
|
|
707
743
|
reconnectTimeout = null;
|
|
708
744
|
createSubscription().catch((err) => {
|
|
709
|
-
|
|
745
|
+
logger.error(`[${account.accountId}] Reconnection failed: ${String(err)}`);
|
|
710
746
|
});
|
|
711
747
|
}, delay);
|
|
712
748
|
};
|
|
@@ -717,7 +753,7 @@ export async function startRingCentralMonitor(
|
|
|
717
753
|
// Handle abort signal
|
|
718
754
|
const cleanup = () => {
|
|
719
755
|
isShuttingDown = true;
|
|
720
|
-
|
|
756
|
+
logger.info(`[${account.accountId}] Stopping RingCentral WebSocket subscription...`);
|
|
721
757
|
|
|
722
758
|
if (reconnectTimeout) {
|
|
723
759
|
clearTimeout(reconnectTimeout);
|
|
@@ -726,7 +762,7 @@ export async function startRingCentralMonitor(
|
|
|
726
762
|
|
|
727
763
|
if (wsSubscription) {
|
|
728
764
|
wsSubscription.revoke().catch((err) => {
|
|
729
|
-
|
|
765
|
+
logger.error(`[${account.accountId}] Failed to revoke subscription: ${String(err)}`);
|
|
730
766
|
});
|
|
731
767
|
wsSubscription = null;
|
|
732
768
|
}
|
package/src/openclaw.d.ts
CHANGED
|
@@ -13,12 +13,17 @@ declare module "openclaw/plugin-sdk" {
|
|
|
13
13
|
[key: string]: unknown;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type PluginLogger = {
|
|
17
|
+
debug: (message: string) => void;
|
|
18
|
+
info: (message: string) => void;
|
|
19
|
+
warn: (message: string) => void;
|
|
20
|
+
error: (message: string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
export type PluginRuntime = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
error(msg: string, ...args: unknown[]): void;
|
|
21
|
-
debug(msg: string, ...args: unknown[]): void;
|
|
24
|
+
logging: {
|
|
25
|
+
shouldLogVerbose: () => boolean;
|
|
26
|
+
getChildLogger: (bindings: Record<string, unknown>, opts?: { level?: string }) => PluginLogger;
|
|
22
27
|
};
|
|
23
28
|
[key: string]: unknown;
|
|
24
29
|
};
|