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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-ringcentral",
3
- "version": "2026.1.29-beta2",
3
+ "version": "2026.1.30-beta.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "OpenClaw RingCentral Team Messaging channel plugin",
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 = 1000; // 1 second
35
- const RECONNECT_MAX_DELAY = 60000; // 60 seconds
36
- const RECONNECT_MAX_ATTEMPTS = 10;
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
- runtime.log?.(`[ringcentral] ${message}`);
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, runtime, `skip own sent message: ${messageId}`);
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, runtime, "skip typing indicator message");
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, runtime, `ignore message from non-owner: ${senderId} (selfOnly mode)`);
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, runtime, `ignore non-personal chat in selfOnly mode: chatType=${chatType}`);
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, runtime, `drop group message (groupPolicy=disabled, chat=${chatId})`);
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, runtime, `drop group message (not allowlisted, chat=${chatId})`);
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, runtime, `drop group message (chat disabled, chat=${chatId})`);
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, runtime, `drop group message (sender not allowed, ${senderId})`);
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, runtime, `drop group message (mention required, chat=${chatId})`);
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, runtime, `ignore DM (dmPolicy=disabled)`);
365
+ logVerbose(core, `ignore DM (dmPolicy=disabled)`);
345
366
  return;
346
367
  }
347
368
  if (dmPolicy === "allowlist" && !isSenderAllowed(senderId, effectiveAllowFrom)) {
348
- logVerbose(core, runtime, `ignore DM from ${senderId} (not in allowFrom)`);
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, runtime, `ringcentral: drop control command from ${senderId}`);
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
- runtime.log?.(`[${account.accountId}] Starting RingCentral WebSocket subscription...`);
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
- runtime.log?.(`[${account.accountId}] Authenticated as extension: ${ownerId}`);
670
+ logger.info(`[${account.accountId}] Authenticated as extension: ${ownerId}`);
649
671
  } catch (err) {
650
- runtime.error?.(`[${account.accountId}] Failed to get current user: ${String(err)}`);
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
- logVerbose(core, runtime, `WebSocket notification received: ${JSON.stringify(event).slice(0, 500)}`);
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
- runtime.error?.(`[${account.accountId}] WebSocket event processing failed: ${String(err)}`);
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
- runtime.log?.(`[${account.accountId}] RingCentral WebSocket subscription established`);
701
+ logger.info(`[${account.accountId}] RingCentral WebSocket subscription established`);
680
702
  reconnectAttempts = 0; // Reset on success
681
703
 
682
704
  } catch (err) {
683
- runtime.error?.(`[${account.accountId}] Failed to create WebSocket subscription: ${String(err)}`);
684
- scheduleReconnect();
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
- runtime.error?.(`[${account.accountId}] Max reconnection attempts (${RECONNECT_MAX_ATTEMPTS}) reached. Giving up.`);
726
+ logger.error(`[${account.accountId}] Max reconnection attempts (${RECONNECT_MAX_ATTEMPTS}) reached. Giving up.`);
693
727
  return;
694
728
  }
695
729
 
696
- const delay = getReconnectDelay();
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
- runtime.log?.(`[${account.accountId}] Scheduling reconnection attempt ${reconnectAttempts}/${RECONNECT_MAX_ATTEMPTS} in ${delay}ms...`);
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
- runtime.error?.(`[${account.accountId}] Reconnection failed: ${String(err)}`);
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
- runtime.log?.(`[${account.accountId}] Stopping RingCentral WebSocket subscription...`);
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
- runtime.error?.(`[${account.accountId}] Failed to revoke subscription: ${String(err)}`);
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
- log: {
18
- info(msg: string, ...args: unknown[]): void;
19
- warn(msg: string, ...args: unknown[]): void;
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
  };