@openclaw/bluebubbles 2026.3.2 → 2026.3.8-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/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/bluebubbles";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/bluebubbles";
3
3
  import { bluebubblesPlugin } from "./src/channel.js";
4
4
  import { setBlueBubblesRuntime } from "./src/runtime.js";
5
5
 
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@openclaw/bluebubbles",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.8-beta.1",
4
4
  "description": "OpenClaw BlueBubbles channel plugin",
5
5
  "type": "module",
6
+ "dependencies": {
7
+ "zod": "^4.3.6"
8
+ },
6
9
  "openclaw": {
7
10
  "extensions": [
8
11
  "./index.ts"
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
2
2
  import { resolveBlueBubblesAccount } from "./accounts.js";
3
3
  import { normalizeResolvedSecretInputString } from "./secret-input.js";
4
4
 
package/src/accounts.ts CHANGED
@@ -1,9 +1,5 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
- import {
3
- DEFAULT_ACCOUNT_ID,
4
- normalizeAccountId,
5
- normalizeOptionalAccountId,
6
- } from "openclaw/plugin-sdk/account-id";
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import { createAccountListHelpers, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
7
3
  import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
8
4
  import { normalizeBlueBubblesServerUrl, type BlueBubblesAccountConfig } from "./types.js";
9
5
 
@@ -16,36 +12,11 @@ export type ResolvedBlueBubblesAccount = {
16
12
  baseUrl?: string;
17
13
  };
18
14
 
19
- function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
20
- const accounts = cfg.channels?.bluebubbles?.accounts;
21
- if (!accounts || typeof accounts !== "object") {
22
- return [];
23
- }
24
- return Object.keys(accounts).filter(Boolean);
25
- }
26
-
27
- export function listBlueBubblesAccountIds(cfg: OpenClawConfig): string[] {
28
- const ids = listConfiguredAccountIds(cfg);
29
- if (ids.length === 0) {
30
- return [DEFAULT_ACCOUNT_ID];
31
- }
32
- return ids.toSorted((a, b) => a.localeCompare(b));
33
- }
34
-
35
- export function resolveDefaultBlueBubblesAccountId(cfg: OpenClawConfig): string {
36
- const preferred = normalizeOptionalAccountId(cfg.channels?.bluebubbles?.defaultAccount);
37
- if (
38
- preferred &&
39
- listBlueBubblesAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
40
- ) {
41
- return preferred;
42
- }
43
- const ids = listBlueBubblesAccountIds(cfg);
44
- if (ids.includes(DEFAULT_ACCOUNT_ID)) {
45
- return DEFAULT_ACCOUNT_ID;
46
- }
47
- return ids[0] ?? DEFAULT_ACCOUNT_ID;
48
- }
15
+ const {
16
+ listAccountIds: listBlueBubblesAccountIds,
17
+ resolveDefaultAccountId: resolveDefaultBlueBubblesAccountId,
18
+ } = createAccountListHelpers("bluebubbles");
19
+ export { listBlueBubblesAccountIds, resolveDefaultBlueBubblesAccountId };
49
20
 
50
21
  function resolveAccountConfig(
51
22
  cfg: OpenClawConfig,
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
2
2
  import { describe, expect, it, vi, beforeEach } from "vitest";
3
3
  import { bluebubblesMessageActions } from "./actions.js";
4
4
  import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
package/src/actions.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  readStringParam,
11
11
  type ChannelMessageActionAdapter,
12
12
  type ChannelMessageActionName,
13
- } from "openclaw/plugin-sdk";
13
+ } from "openclaw/plugin-sdk/bluebubbles";
14
14
  import { resolveBlueBubblesAccount } from "./accounts.js";
15
15
  import { sendBlueBubblesAttachment } from "./attachments.js";
16
16
  import {
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
2
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import "./test-mocks.js";
4
4
  import { downloadBlueBubblesAttachment, sendBlueBubblesAttachment } from "./attachments.js";
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
4
4
  import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
5
5
  import { postMultipartFormData } from "./multipart.js";
6
6
  import {
package/src/channel.ts CHANGED
@@ -1,19 +1,29 @@
1
- import type { ChannelAccountSnapshot, ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type {
2
+ ChannelAccountSnapshot,
3
+ ChannelPlugin,
4
+ OpenClawConfig,
5
+ } from "openclaw/plugin-sdk/bluebubbles";
2
6
  import {
3
7
  applyAccountNameToChannelSection,
4
8
  buildChannelConfigSchema,
9
+ buildComputedAccountStatusSnapshot,
5
10
  buildProbeChannelStatusSummary,
6
11
  collectBlueBubblesStatusIssues,
7
12
  DEFAULT_ACCOUNT_ID,
8
13
  deleteAccountFromConfigSection,
9
- formatPairingApproveHint,
10
14
  migrateBaseNameToDefaultAccount,
11
15
  normalizeAccountId,
12
16
  PAIRING_APPROVED_MESSAGE,
13
17
  resolveBlueBubblesGroupRequireMention,
14
18
  resolveBlueBubblesGroupToolPolicy,
15
19
  setAccountEnabledInConfigSection,
16
- } from "openclaw/plugin-sdk";
20
+ } from "openclaw/plugin-sdk/bluebubbles";
21
+ import {
22
+ buildAccountScopedDmSecurityPolicy,
23
+ collectOpenGroupPolicyRestrictSendersWarnings,
24
+ formatNormalizedAllowFromEntries,
25
+ mapAllowFromEntries,
26
+ } from "openclaw/plugin-sdk/compat";
17
27
  import {
18
28
  listBlueBubblesAccountIds,
19
29
  type ResolvedBlueBubblesAccount,
@@ -21,6 +31,7 @@ import {
21
31
  resolveDefaultBlueBubblesAccountId,
22
32
  } from "./accounts.js";
23
33
  import { bluebubblesMessageActions } from "./actions.js";
34
+ import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
24
35
  import { BlueBubblesConfigSchema } from "./config-schema.js";
25
36
  import { sendBlueBubblesMedia } from "./media-send.js";
26
37
  import { resolveBlueBubblesMessageId } from "./monitor.js";
@@ -105,41 +116,37 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
105
116
  baseUrl: account.baseUrl,
106
117
  }),
107
118
  resolveAllowFrom: ({ cfg, accountId }) =>
108
- (resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom ?? []).map((entry) =>
109
- String(entry),
110
- ),
119
+ mapAllowFromEntries(resolveBlueBubblesAccount({ cfg: cfg, accountId }).config.allowFrom),
111
120
  formatAllowFrom: ({ allowFrom }) =>
112
- allowFrom
113
- .map((entry) => String(entry).trim())
114
- .filter(Boolean)
115
- .map((entry) => entry.replace(/^bluebubbles:/i, ""))
116
- .map((entry) => normalizeBlueBubblesHandle(entry)),
121
+ formatNormalizedAllowFromEntries({
122
+ allowFrom,
123
+ normalizeEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
124
+ }),
117
125
  },
118
126
  actions: bluebubblesMessageActions,
119
127
  security: {
120
128
  resolveDmPolicy: ({ cfg, accountId, account }) => {
121
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
122
- const useAccountPath = Boolean(cfg.channels?.bluebubbles?.accounts?.[resolvedAccountId]);
123
- const basePath = useAccountPath
124
- ? `channels.bluebubbles.accounts.${resolvedAccountId}.`
125
- : "channels.bluebubbles.";
126
- return {
127
- policy: account.config.dmPolicy ?? "pairing",
129
+ return buildAccountScopedDmSecurityPolicy({
130
+ cfg,
131
+ channelKey: "bluebubbles",
132
+ accountId,
133
+ fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
134
+ policy: account.config.dmPolicy,
128
135
  allowFrom: account.config.allowFrom ?? [],
129
- policyPath: `${basePath}dmPolicy`,
130
- allowFromPath: basePath,
131
- approveHint: formatPairingApproveHint("bluebubbles"),
136
+ policyPathSuffix: "dmPolicy",
132
137
  normalizeEntry: (raw) => normalizeBlueBubblesHandle(raw.replace(/^bluebubbles:/i, "")),
133
- };
138
+ });
134
139
  },
135
140
  collectWarnings: ({ account }) => {
136
141
  const groupPolicy = account.config.groupPolicy ?? "allowlist";
137
- if (groupPolicy !== "open") {
138
- return [];
139
- }
140
- return [
141
- `- BlueBubbles groups: groupPolicy="open" allows any member to trigger the bot. Set channels.bluebubbles.groupPolicy="allowlist" + channels.bluebubbles.groupAllowFrom to restrict senders.`,
142
- ];
142
+ return collectOpenGroupPolicyRestrictSendersWarnings({
143
+ groupPolicy,
144
+ surface: "BlueBubbles groups",
145
+ openScope: "any member",
146
+ groupPolicyPath: "channels.bluebubbles.groupPolicy",
147
+ groupAllowFromPath: "channels.bluebubbles.groupAllowFrom",
148
+ mentionGated: false,
149
+ });
143
150
  },
144
151
  },
145
152
  messaging: {
@@ -250,41 +257,16 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
250
257
  channelKey: "bluebubbles",
251
258
  })
252
259
  : namedConfig;
253
- if (accountId === DEFAULT_ACCOUNT_ID) {
254
- return {
255
- ...next,
256
- channels: {
257
- ...next.channels,
258
- bluebubbles: {
259
- ...next.channels?.bluebubbles,
260
- enabled: true,
261
- ...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
262
- ...(input.password ? { password: input.password } : {}),
263
- ...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
264
- },
265
- },
266
- } as OpenClawConfig;
267
- }
268
- return {
269
- ...next,
270
- channels: {
271
- ...next.channels,
272
- bluebubbles: {
273
- ...next.channels?.bluebubbles,
274
- enabled: true,
275
- accounts: {
276
- ...next.channels?.bluebubbles?.accounts,
277
- [accountId]: {
278
- ...next.channels?.bluebubbles?.accounts?.[accountId],
279
- enabled: true,
280
- ...(input.httpUrl ? { serverUrl: input.httpUrl } : {}),
281
- ...(input.password ? { password: input.password } : {}),
282
- ...(input.webhookPath ? { webhookPath: input.webhookPath } : {}),
283
- },
284
- },
285
- },
260
+ return applyBlueBubblesConnectionConfig({
261
+ cfg: next,
262
+ accountId,
263
+ patch: {
264
+ serverUrl: input.httpUrl,
265
+ password: input.password,
266
+ webhookPath: input.webhookPath,
286
267
  },
287
- } as OpenClawConfig;
268
+ onlyDefinedFields: true,
269
+ });
288
270
  },
289
271
  },
290
272
  pairing: {
@@ -368,20 +350,18 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
368
350
  buildAccountSnapshot: ({ account, runtime, probe }) => {
369
351
  const running = runtime?.running ?? false;
370
352
  const probeOk = (probe as BlueBubblesProbe | undefined)?.ok;
371
- return {
353
+ const base = buildComputedAccountStatusSnapshot({
372
354
  accountId: account.accountId,
373
355
  name: account.name,
374
356
  enabled: account.enabled,
375
357
  configured: account.configured,
358
+ runtime,
359
+ probe,
360
+ });
361
+ return {
362
+ ...base,
376
363
  baseUrl: account.baseUrl,
377
- running,
378
364
  connected: probeOk ?? running,
379
- lastStartAt: runtime?.lastStartAt ?? null,
380
- lastStopAt: runtime?.lastStopAt ?? null,
381
- lastError: runtime?.lastError ?? null,
382
- probe,
383
- lastInboundAt: runtime?.lastInboundAt ?? null,
384
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
385
365
  };
386
366
  },
387
367
  },
package/src/chat.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import path from "node:path";
3
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
3
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
4
4
  import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
5
5
  import { postMultipartFormData } from "./multipart.js";
6
6
  import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
@@ -30,6 +30,39 @@ function resolvePartIndex(partIndex: number | undefined): number {
30
30
  return typeof partIndex === "number" ? partIndex : 0;
31
31
  }
32
32
 
33
+ async function sendBlueBubblesChatEndpointRequest(params: {
34
+ chatGuid: string;
35
+ opts: BlueBubblesChatOpts;
36
+ endpoint: "read" | "typing";
37
+ method: "POST" | "DELETE";
38
+ action: "read" | "typing";
39
+ }): Promise<void> {
40
+ const trimmed = params.chatGuid.trim();
41
+ if (!trimmed) {
42
+ return;
43
+ }
44
+ const { baseUrl, password, accountId } = resolveAccount(params.opts);
45
+ if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
46
+ return;
47
+ }
48
+ const url = buildBlueBubblesApiUrl({
49
+ baseUrl,
50
+ path: `/api/v1/chat/${encodeURIComponent(trimmed)}/${params.endpoint}`,
51
+ password,
52
+ });
53
+ const res = await blueBubblesFetchWithTimeout(
54
+ url,
55
+ { method: params.method },
56
+ params.opts.timeoutMs,
57
+ );
58
+ if (!res.ok) {
59
+ const errorText = await res.text().catch(() => "");
60
+ throw new Error(
61
+ `BlueBubbles ${params.action} failed (${res.status}): ${errorText || "unknown"}`,
62
+ );
63
+ }
64
+ }
65
+
33
66
  async function sendPrivateApiJsonRequest(params: {
34
67
  opts: BlueBubblesChatOpts;
35
68
  feature: string;
@@ -65,24 +98,13 @@ export async function markBlueBubblesChatRead(
65
98
  chatGuid: string,
66
99
  opts: BlueBubblesChatOpts = {},
67
100
  ): Promise<void> {
68
- const trimmed = chatGuid.trim();
69
- if (!trimmed) {
70
- return;
71
- }
72
- const { baseUrl, password, accountId } = resolveAccount(opts);
73
- if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
74
- return;
75
- }
76
- const url = buildBlueBubblesApiUrl({
77
- baseUrl,
78
- path: `/api/v1/chat/${encodeURIComponent(trimmed)}/read`,
79
- password,
101
+ await sendBlueBubblesChatEndpointRequest({
102
+ chatGuid,
103
+ opts,
104
+ endpoint: "read",
105
+ method: "POST",
106
+ action: "read",
80
107
  });
81
- const res = await blueBubblesFetchWithTimeout(url, { method: "POST" }, opts.timeoutMs);
82
- if (!res.ok) {
83
- const errorText = await res.text().catch(() => "");
84
- throw new Error(`BlueBubbles read failed (${res.status}): ${errorText || "unknown"}`);
85
- }
86
108
  }
87
109
 
88
110
  export async function sendBlueBubblesTyping(
@@ -90,28 +112,13 @@ export async function sendBlueBubblesTyping(
90
112
  typing: boolean,
91
113
  opts: BlueBubblesChatOpts = {},
92
114
  ): Promise<void> {
93
- const trimmed = chatGuid.trim();
94
- if (!trimmed) {
95
- return;
96
- }
97
- const { baseUrl, password, accountId } = resolveAccount(opts);
98
- if (getCachedBlueBubblesPrivateApiStatus(accountId) === false) {
99
- return;
100
- }
101
- const url = buildBlueBubblesApiUrl({
102
- baseUrl,
103
- path: `/api/v1/chat/${encodeURIComponent(trimmed)}/typing`,
104
- password,
115
+ await sendBlueBubblesChatEndpointRequest({
116
+ chatGuid,
117
+ opts,
118
+ endpoint: "typing",
119
+ method: typing ? "POST" : "DELETE",
120
+ action: "typing",
105
121
  });
106
- const res = await blueBubblesFetchWithTimeout(
107
- url,
108
- { method: typing ? "POST" : "DELETE" },
109
- opts.timeoutMs,
110
- );
111
- if (!res.ok) {
112
- const errorText = await res.text().catch(() => "");
113
- throw new Error(`BlueBubbles typing failed (${res.status}): ${errorText || "unknown"}`);
114
- }
115
122
  }
116
123
 
117
124
  /**
@@ -0,0 +1,77 @@
1
+ import { DEFAULT_ACCOUNT_ID, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
2
+
3
+ type BlueBubblesConfigPatch = {
4
+ serverUrl?: string;
5
+ password?: unknown;
6
+ webhookPath?: string;
7
+ };
8
+
9
+ type AccountEnabledMode = boolean | "preserve-or-true";
10
+
11
+ function normalizePatch(
12
+ patch: BlueBubblesConfigPatch,
13
+ onlyDefinedFields: boolean,
14
+ ): BlueBubblesConfigPatch {
15
+ if (!onlyDefinedFields) {
16
+ return patch;
17
+ }
18
+ const next: BlueBubblesConfigPatch = {};
19
+ if (patch.serverUrl !== undefined) {
20
+ next.serverUrl = patch.serverUrl;
21
+ }
22
+ if (patch.password !== undefined) {
23
+ next.password = patch.password;
24
+ }
25
+ if (patch.webhookPath !== undefined) {
26
+ next.webhookPath = patch.webhookPath;
27
+ }
28
+ return next;
29
+ }
30
+
31
+ export function applyBlueBubblesConnectionConfig(params: {
32
+ cfg: OpenClawConfig;
33
+ accountId: string;
34
+ patch: BlueBubblesConfigPatch;
35
+ onlyDefinedFields?: boolean;
36
+ accountEnabled?: AccountEnabledMode;
37
+ }): OpenClawConfig {
38
+ const patch = normalizePatch(params.patch, params.onlyDefinedFields === true);
39
+ if (params.accountId === DEFAULT_ACCOUNT_ID) {
40
+ return {
41
+ ...params.cfg,
42
+ channels: {
43
+ ...params.cfg.channels,
44
+ bluebubbles: {
45
+ ...params.cfg.channels?.bluebubbles,
46
+ enabled: true,
47
+ ...patch,
48
+ },
49
+ },
50
+ };
51
+ }
52
+
53
+ const currentAccount = params.cfg.channels?.bluebubbles?.accounts?.[params.accountId];
54
+ const enabled =
55
+ params.accountEnabled === "preserve-or-true"
56
+ ? (currentAccount?.enabled ?? true)
57
+ : (params.accountEnabled ?? true);
58
+
59
+ return {
60
+ ...params.cfg,
61
+ channels: {
62
+ ...params.cfg.channels,
63
+ bluebubbles: {
64
+ ...params.cfg.channels?.bluebubbles,
65
+ enabled: true,
66
+ accounts: {
67
+ ...params.cfg.channels?.bluebubbles?.accounts,
68
+ [params.accountId]: {
69
+ ...currentAccount,
70
+ enabled,
71
+ ...patch,
72
+ },
73
+ },
74
+ },
75
+ },
76
+ };
77
+ }
@@ -5,7 +5,7 @@ describe("BlueBubblesConfigSchema", () => {
5
5
  it("accepts account config when serverUrl and password are both set", () => {
6
6
  const parsed = BlueBubblesConfigSchema.safeParse({
7
7
  serverUrl: "http://localhost:1234",
8
- password: "secret",
8
+ password: "secret", // pragma: allowlist secret
9
9
  });
10
10
  expect(parsed.success).toBe(true);
11
11
  });
@@ -1,9 +1,11 @@
1
- import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
1
+ import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/bluebubbles";
2
+ import {
3
+ AllowFromEntrySchema,
4
+ buildCatchallMultiAccountChannelSchema,
5
+ } from "openclaw/plugin-sdk/compat";
2
6
  import { z } from "zod";
3
7
  import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
4
8
 
5
- const allowFromEntry = z.union([z.string(), z.number()]);
6
-
7
9
  const bluebubblesActionSchema = z
8
10
  .object({
9
11
  reactions: z.boolean().default(true),
@@ -34,8 +36,8 @@ const bluebubblesAccountSchema = z
34
36
  password: buildSecretInputSchema().optional(),
35
37
  webhookPath: z.string().optional(),
36
38
  dmPolicy: z.enum(["pairing", "allowlist", "open", "disabled"]).optional(),
37
- allowFrom: z.array(allowFromEntry).optional(),
38
- groupAllowFrom: z.array(allowFromEntry).optional(),
39
+ allowFrom: z.array(AllowFromEntrySchema).optional(),
40
+ groupAllowFrom: z.array(AllowFromEntrySchema).optional(),
39
41
  groupPolicy: z.enum(["open", "disabled", "allowlist"]).optional(),
40
42
  historyLimit: z.number().int().min(0).optional(),
41
43
  dmHistoryLimit: z.number().int().min(0).optional(),
@@ -60,8 +62,8 @@ const bluebubblesAccountSchema = z
60
62
  }
61
63
  });
62
64
 
63
- export const BlueBubblesConfigSchema = bluebubblesAccountSchema.extend({
64
- accounts: z.object({}).catchall(bluebubblesAccountSchema).optional(),
65
- defaultAccount: z.string().optional(),
65
+ export const BlueBubblesConfigSchema = buildCatchallMultiAccountChannelSchema(
66
+ bluebubblesAccountSchema,
67
+ ).extend({
66
68
  actions: bluebubblesActionSchema,
67
69
  });
package/src/history.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
2
2
  import { resolveBlueBubblesServerAccount } from "./account-resolve.js";
3
3
  import { blueBubblesFetchWithTimeout, buildBlueBubblesApiUrl } from "./types.js";
4
4
 
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
5
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/bluebubbles";
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { sendBlueBubblesMedia } from "./media-send.js";
8
8
  import { setBlueBubblesRuntime } from "./runtime.js";
package/src/media-send.ts CHANGED
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk";
6
+ import { resolveChannelMediaMaxBytes, type OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
7
7
  import { resolveBlueBubblesAccount } from "./accounts.js";
8
8
  import { sendBlueBubblesAttachment } from "./attachments.js";
9
9
  import { resolveBlueBubblesMessageId } from "./monitor.js";
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/bluebubbles";
2
2
  import type { NormalizedWebhookMessage } from "./monitor-normalize.js";
3
3
  import type { BlueBubblesCoreRuntime, WebhookTarget } from "./monitor-shared.js";
4
4
 
@@ -1,3 +1,4 @@
1
+ import { parseFiniteNumber } from "openclaw/plugin-sdk/bluebubbles";
1
2
  import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";
2
3
  import type { BlueBubblesAttachment } from "./types.js";
3
4
 
@@ -35,17 +36,7 @@ function readNumberLike(record: Record<string, unknown> | null, key: string): nu
35
36
  if (!record) {
36
37
  return undefined;
37
38
  }
38
- const value = record[key];
39
- if (typeof value === "number" && Number.isFinite(value)) {
40
- return value;
41
- }
42
- if (typeof value === "string") {
43
- const parsed = Number.parseFloat(value);
44
- if (Number.isFinite(parsed)) {
45
- return parsed;
46
- }
47
- }
48
- return undefined;
39
+ return parseFiniteNumber(record[key]);
49
40
  }
50
41
 
51
42
  function extractAttachments(message: Record<string, unknown>): BlueBubblesAttachment[] {