rol-websocket-channel 1.5.3 → 1.5.5

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.
@@ -0,0 +1,6 @@
1
+ # SDK Channel TODO
2
+
3
+ 1. Migrate the plugin entry to the current SDK style with `defineChannelPluginEntry({ channel, cli, hooks, manifest })`.
4
+ 2. Move `sender` message normalization into a dedicated inbound normalizer while keeping admin MQTT command types out of the chat inbound path.
5
+ 3. Align CLI metadata with the SDK entry format while preserving `admin-bridge` and `rol-websocket-channel` command compatibility.
6
+ 4. Keep `package.json`, `openclaw.plugin.json`, and ClawHub release versions in sync for update/install flows.
package/dist/index.js CHANGED
@@ -9,6 +9,26 @@ import * as ConnectionManager from "./src/mqtt/connection-manager.js";
9
9
  // ============================================
10
10
  import { getContext, initializeContext } from "./src/shared/context.js";
11
11
  let pluginRuntime = null;
12
+ function isRecord(value) {
13
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
14
+ }
15
+ function pickNonEmptyString(...values) {
16
+ for (const value of values) {
17
+ if (typeof value !== "string")
18
+ continue;
19
+ const trimmed = value.trim();
20
+ if (trimmed)
21
+ return trimmed;
22
+ }
23
+ return undefined;
24
+ }
25
+ function getChannelConfig(cfg) {
26
+ const entry = cfg.channels?.["rol-websocket-channel"];
27
+ if (!isRecord(entry))
28
+ return null;
29
+ const nested = isRecord(entry.config) ? entry.config : {};
30
+ return { entry, nested };
31
+ }
12
32
  export function getPluginRuntime() {
13
33
  return pluginRuntime;
14
34
  }
@@ -99,7 +119,7 @@ const WebSocketChannel = {
99
119
  enum: ["pairing", "allowlist", "open", "disabled"],
100
120
  },
101
121
  },
102
- required: ["mqttUrl"],
122
+ required: ["mqttUrl", "mqttTopic"],
103
123
  },
104
124
  },
105
125
  },
@@ -139,12 +159,12 @@ const WebSocketChannel = {
139
159
  },
140
160
  "config.mqttUrl": {
141
161
  label: "MQTT Broker URL",
142
- placeholder: "ws://192.168.1.152:8083/mqtt",
162
+ placeholder: "ws://mqtt.example.com:8083/mqtt",
143
163
  help: "MQTT broker WebSocket URL",
144
164
  },
145
165
  "config.mqttTopic": {
146
166
  label: "MQTT Topic",
147
- placeholder: "announcement/tester",
167
+ placeholder: "announcement/{userId}/{agentId}/#",
148
168
  help: "MQTT topic to subscribe/publish",
149
169
  },
150
170
  "config.groupPolicy": {
@@ -155,26 +175,37 @@ const WebSocketChannel = {
155
175
  },
156
176
  config: {
157
177
  listAccountIds: (cfg) => {
158
- const channelCfg = cfg.channels?.["rol-websocket-channel"];
159
- if (!channelCfg || !channelCfg.config || !channelCfg.config.mqttUrl) {
178
+ const channelCfg = getChannelConfig(cfg);
179
+ const mqttUrl = channelCfg
180
+ ? pickNonEmptyString(channelCfg.nested.mqttUrl, channelCfg.entry.mqttUrl)
181
+ : undefined;
182
+ if (!mqttUrl) {
160
183
  return [];
161
184
  }
162
185
  return ["default"];
163
186
  },
164
187
  resolveAccount: (cfg, accountId) => {
165
- const channelCfg = cfg.channels?.["rol-websocket-channel"];
166
- if (!channelCfg || !channelCfg.config) {
188
+ const channelCfg = getChannelConfig(cfg);
189
+ if (!channelCfg) {
190
+ return undefined;
191
+ }
192
+ const config = channelCfg.nested;
193
+ const mqttUrl = pickNonEmptyString(config.mqttUrl, channelCfg.entry.mqttUrl);
194
+ if (!mqttUrl) {
195
+ return undefined;
196
+ }
197
+ const mqttTopic = pickNonEmptyString(config.mqttTopic, channelCfg.entry.mqttTopic);
198
+ if (!mqttTopic) {
167
199
  return undefined;
168
200
  }
169
- const config = channelCfg.config;
170
201
  return {
171
202
  accountId: "default",
172
- mqttUrl: config.mqttUrl || "ws://192.168.1.152:8083/mqtt",
173
- mqttTopic: config.mqttTopic || "announcement/tester",
203
+ mqttUrl,
204
+ mqttTopic,
174
205
  enabled: config.enabled !== false,
175
- dmPolicy: channelCfg.dmPolicy || config.groupPolicy || "open",
176
- allowFrom: Array.isArray(channelCfg.allowFrom)
177
- ? channelCfg.allowFrom
206
+ dmPolicy: channelCfg.entry.dmPolicy || config.groupPolicy || "open",
207
+ allowFrom: Array.isArray(channelCfg.entry.allowFrom)
208
+ ? channelCfg.entry.allowFrom
178
209
  : [],
179
210
  groupPolicy: config.groupPolicy || "open",
180
211
  };
@@ -262,8 +293,31 @@ const WebSocketChannel = {
262
293
  console.error(`[MQTT] startAccount(${account.accountId}): Runtime API not available`);
263
294
  throw new Error("Runtime API not available");
264
295
  }
265
- const mqttUrl = account.mqttUrl || "ws://192.168.1.152:8083/mqtt";
266
- const mqttTopic = account.mqttTopic || "announcement/tester";
296
+ const mqttUrl = pickNonEmptyString(account.mqttUrl);
297
+ if (!mqttUrl) {
298
+ const message = "MQTT broker URL is not configured";
299
+ console.error(`[MQTT] startAccount(${account.accountId}): ${message}`);
300
+ ctx.setStatus({
301
+ accountId: account.accountId,
302
+ running: false,
303
+ connected: false,
304
+ lastError: message,
305
+ });
306
+ throw new Error(message);
307
+ }
308
+ const mqttTopic = pickNonEmptyString(account.mqttTopic);
309
+ if (!mqttTopic) {
310
+ const message = "MQTT topic is not configured";
311
+ console.error(`[MQTT] startAccount(${account.accountId}): ${message}`);
312
+ ctx.setStatus({
313
+ accountId: account.accountId,
314
+ mqttUrl,
315
+ running: false,
316
+ connected: false,
317
+ lastError: message,
318
+ });
319
+ throw new Error(message);
320
+ }
267
321
  console.log(`[MQTT] startAccount(${account.accountId}): url=${mqttUrl}, topic=${mqttTopic}`);
268
322
  ctx.setStatus({
269
323
  accountId: account.accountId,
@@ -105,7 +105,16 @@ function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
105
105
  ?? pickString(root.mqttTopic)
106
106
  ?? pickString(root.mqtt_topic)
107
107
  ?? rawValue
108
- ?? 'announcement/tester';
108
+ ?? null;
109
+ if (!mqttTopic) {
110
+ throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttTopic is missing from pairing payload', {
111
+ endpoint,
112
+ rootKeys: Object.keys(root),
113
+ channelKeys: Object.keys(channelValue),
114
+ channelConfigKeys: Object.keys(channelConfigValue),
115
+ hasRawValue: Boolean(rawValue)
116
+ });
117
+ }
109
118
  const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
110
119
  ?? pickString(channelValue.group_policy)
111
120
  ?? pickString(channelValue.dmPolicy)
@@ -181,7 +190,7 @@ function applyPairingConfig(config, key, payload) {
181
190
  ...existingChannelConfig,
182
191
  enabled: true,
183
192
  mqttUrl: payload.channel.mqttUrl,
184
- mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
193
+ mqttTopic: payload.channel.mqttTopic,
185
194
  groupPolicy: payload.channel.groupPolicy
186
195
  }
187
196
  };
package/index.ts CHANGED
@@ -34,6 +34,29 @@ import { getContext, initializeContext } from "./src/shared/context.js";
34
34
 
35
35
  let pluginRuntime: any = null;
36
36
 
37
+ function isRecord(value: unknown): value is Record<string, any> {
38
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
39
+ }
40
+
41
+ function pickNonEmptyString(...values: unknown[]): string | undefined {
42
+ for (const value of values) {
43
+ if (typeof value !== "string") continue;
44
+ const trimmed = value.trim();
45
+ if (trimmed) return trimmed;
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ function getChannelConfig(cfg: OpenClawConfig): {
51
+ entry: Record<string, any>;
52
+ nested: Record<string, any>;
53
+ } | null {
54
+ const entry = cfg.channels?.["rol-websocket-channel"];
55
+ if (!isRecord(entry)) return null;
56
+ const nested = isRecord(entry.config) ? entry.config : {};
57
+ return { entry, nested };
58
+ }
59
+
37
60
  export function getPluginRuntime(): any {
38
61
  return pluginRuntime;
39
62
  }
@@ -135,7 +158,7 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
135
158
  enum: ["pairing", "allowlist", "open", "disabled"],
136
159
  },
137
160
  },
138
- required: ["mqttUrl"],
161
+ required: ["mqttUrl", "mqttTopic"],
139
162
  },
140
163
  },
141
164
  },
@@ -175,12 +198,12 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
175
198
  },
176
199
  "config.mqttUrl": {
177
200
  label: "MQTT Broker URL",
178
- placeholder: "ws://192.168.1.152:8083/mqtt",
201
+ placeholder: "ws://mqtt.example.com:8083/mqtt",
179
202
  help: "MQTT broker WebSocket URL",
180
203
  },
181
204
  "config.mqttTopic": {
182
205
  label: "MQTT Topic",
183
- placeholder: "announcement/tester",
206
+ placeholder: "announcement/{userId}/{agentId}/#",
184
207
  help: "MQTT topic to subscribe/publish",
185
208
  },
186
209
  "config.groupPolicy": {
@@ -192,29 +215,43 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
192
215
 
193
216
  config: {
194
217
  listAccountIds: (cfg: OpenClawConfig) => {
195
- const channelCfg = cfg.channels?.["rol-websocket-channel"];
196
- if (!channelCfg || !channelCfg.config || !channelCfg.config.mqttUrl) {
218
+ const channelCfg = getChannelConfig(cfg);
219
+ const mqttUrl = channelCfg
220
+ ? pickNonEmptyString(channelCfg.nested.mqttUrl, channelCfg.entry.mqttUrl)
221
+ : undefined;
222
+ if (!mqttUrl) {
197
223
  return [];
198
224
  }
199
225
  return ["default"];
200
226
  },
201
227
 
202
228
  resolveAccount: (cfg: OpenClawConfig, accountId: string) => {
203
- const channelCfg = cfg.channels?.["rol-websocket-channel"];
204
- if (!channelCfg || !channelCfg.config) {
229
+ const channelCfg = getChannelConfig(cfg);
230
+ if (!channelCfg) {
205
231
  return undefined;
206
232
  }
207
233
 
208
- const config = channelCfg.config as any;
234
+ const config = channelCfg.nested;
235
+ const mqttUrl = pickNonEmptyString(config.mqttUrl, channelCfg.entry.mqttUrl);
236
+ if (!mqttUrl) {
237
+ return undefined;
238
+ }
239
+ const mqttTopic = pickNonEmptyString(
240
+ config.mqttTopic,
241
+ channelCfg.entry.mqttTopic,
242
+ );
243
+ if (!mqttTopic) {
244
+ return undefined;
245
+ }
209
246
 
210
247
  return {
211
248
  accountId: "default",
212
- mqttUrl: config.mqttUrl || "ws://192.168.1.152:8083/mqtt",
213
- mqttTopic: config.mqttTopic || "announcement/tester",
249
+ mqttUrl,
250
+ mqttTopic,
214
251
  enabled: config.enabled !== false,
215
- dmPolicy: channelCfg.dmPolicy || config.groupPolicy || "open",
216
- allowFrom: Array.isArray(channelCfg.allowFrom)
217
- ? channelCfg.allowFrom
252
+ dmPolicy: channelCfg.entry.dmPolicy || config.groupPolicy || "open",
253
+ allowFrom: Array.isArray(channelCfg.entry.allowFrom)
254
+ ? channelCfg.entry.allowFrom
218
255
  : [],
219
256
  groupPolicy: config.groupPolicy || "open",
220
257
  };
@@ -322,8 +359,35 @@ const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
322
359
  throw new Error("Runtime API not available");
323
360
  }
324
361
 
325
- const mqttUrl = account.mqttUrl || "ws://192.168.1.152:8083/mqtt";
326
- const mqttTopic = account.mqttTopic || "announcement/tester";
362
+ const mqttUrl = pickNonEmptyString(account.mqttUrl);
363
+ if (!mqttUrl) {
364
+ const message = "MQTT broker URL is not configured";
365
+ console.error(
366
+ `[MQTT] startAccount(${account.accountId}): ${message}`,
367
+ );
368
+ ctx.setStatus({
369
+ accountId: account.accountId,
370
+ running: false,
371
+ connected: false,
372
+ lastError: message,
373
+ });
374
+ throw new Error(message);
375
+ }
376
+ const mqttTopic = pickNonEmptyString(account.mqttTopic);
377
+ if (!mqttTopic) {
378
+ const message = "MQTT topic is not configured";
379
+ console.error(
380
+ `[MQTT] startAccount(${account.accountId}): ${message}`,
381
+ );
382
+ ctx.setStatus({
383
+ accountId: account.accountId,
384
+ mqttUrl,
385
+ running: false,
386
+ connected: false,
387
+ lastError: message,
388
+ });
389
+ throw new Error(message);
390
+ }
327
391
  console.log(
328
392
  `[MQTT] startAccount(${account.accountId}): url=${mqttUrl}, topic=${mqttTopic}`,
329
393
  );
@@ -69,12 +69,12 @@
69
69
  },
70
70
  "mqttUrl": {
71
71
  "label": "MQTT Broker URL",
72
- "placeholder": "ws://192.168.1.152:8083/mqtt",
73
- "help": "MQTT broker WebSocket URL (e.g., ws://192.168.1.152:8083/mqtt)"
72
+ "placeholder": "ws://mqtt.example.com:8083/mqtt",
73
+ "help": "MQTT broker WebSocket URL"
74
74
  },
75
75
  "mqttTopic": {
76
76
  "label": "MQTT Topic",
77
- "placeholder": "announcement/tester",
77
+ "placeholder": "announcement/{userId}/{agentId}/#",
78
78
  "help": "MQTT topic to subscribe/publish"
79
79
  },
80
80
  "groupPolicy": {
@@ -216,7 +216,7 @@
216
216
  "enum": ["pairing", "allowlist", "open", "disabled"]
217
217
  }
218
218
  },
219
- "required": ["mqttUrl"]
219
+ "required": ["mqttUrl", "mqttTopic"]
220
220
  }
221
221
  }
222
222
  }
@@ -294,12 +294,12 @@
294
294
  },
295
295
  "channels.rol-websocket-channel.config.mqttUrl": {
296
296
  "label": "MQTT Broker URL",
297
- "placeholder": "ws://192.168.1.152:8083/mqtt",
298
- "help": "MQTT broker WebSocket URL (e.g., ws://192.168.1.152:8083/mqtt)"
297
+ "placeholder": "ws://mqtt.example.com:8083/mqtt",
298
+ "help": "MQTT broker WebSocket URL"
299
299
  },
300
300
  "channels.rol-websocket-channel.config.mqttTopic": {
301
301
  "label": "MQTT Topic",
302
- "placeholder": "announcement/tester",
302
+ "placeholder": "announcement/{userId}/{agentId}/#",
303
303
  "help": "MQTT topic to subscribe/publish"
304
304
  },
305
305
  "channels.rol-websocket-channel.config.groupPolicy": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -41,7 +41,7 @@ interface PairingPayload {
41
41
  };
42
42
  channel: {
43
43
  mqttUrl: string;
44
- mqttTopic?: string;
44
+ mqttTopic: string;
45
45
  groupPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';
46
46
  };
47
47
  }
@@ -51,7 +51,8 @@ interface PairingPayloadDebug {
51
51
  rootKeys: string[];
52
52
  channelKeys: string[];
53
53
  channelConfigKeys: string[];
54
- hasExistingMqttUrl: boolean;
54
+ hasExistingMqttUrl?: boolean;
55
+ hasRawValue?: boolean;
55
56
  }
56
57
 
57
58
  export async function pairWithKey(
@@ -181,7 +182,16 @@ function normalizePairingPayload(
181
182
  ?? pickString(root.mqttTopic)
182
183
  ?? pickString(root.mqtt_topic)
183
184
  ?? rawValue
184
- ?? 'announcement/tester';
185
+ ?? null;
186
+ if (!mqttTopic) {
187
+ throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttTopic is missing from pairing payload', {
188
+ endpoint,
189
+ rootKeys: Object.keys(root),
190
+ channelKeys: Object.keys(channelValue),
191
+ channelConfigKeys: Object.keys(channelConfigValue),
192
+ hasRawValue: Boolean(rawValue)
193
+ });
194
+ }
185
195
 
186
196
  const groupPolicy = normalizeGroupPolicy(
187
197
  pickString(channelValue.groupPolicy)
@@ -266,7 +276,7 @@ function applyPairingConfig(config: OpenClawConfig, key: string, payload: Pairin
266
276
  ...existingChannelConfig,
267
277
  enabled: true,
268
278
  mqttUrl: payload.channel.mqttUrl,
269
- mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
279
+ mqttTopic: payload.channel.mqttTopic,
270
280
  groupPolicy: payload.channel.groupPolicy
271
281
  }
272
282
  };