rol-websocket-channel 1.7.3 → 1.7.4

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.
@@ -51,11 +51,21 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
51
51
  if (auth) {
52
52
  headers.Authorization = auth;
53
53
  }
54
- const response = await fetch(endpoint, {
55
- method: 'POST',
56
- headers,
57
- body: JSON.stringify({ key })
58
- });
54
+ let response;
55
+ try {
56
+ response = await fetch(endpoint, {
57
+ method: 'POST',
58
+ headers,
59
+ body: JSON.stringify({ key })
60
+ });
61
+ }
62
+ catch (error) {
63
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange request failed: ${error instanceof Error ? error.message : String(error)}`, {
64
+ code: 'PAIR_EXCHANGE_REQUEST_FAILED',
65
+ endpoint,
66
+ cause: describeErrorCause(error)
67
+ });
68
+ }
59
69
  const rawText = await response.text();
60
70
  const payload = tryParseJson(rawText);
61
71
  if (!response.ok) {
@@ -66,8 +76,23 @@ async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
66
76
  payload
67
77
  });
68
78
  }
79
+ ensurePairExchangeSucceeded(payload, endpoint, response.status);
69
80
  return normalizePairingPayload(payload, endpoint, existingMqttUrl);
70
81
  }
82
+ function ensurePairExchangeSucceeded(raw, endpoint, status) {
83
+ if (!isRecord(raw) || raw.success !== false) {
84
+ return;
85
+ }
86
+ const serviceMessage = pickString(raw.message) ?? pickString(raw.msg);
87
+ throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed${serviceMessage ? `: ${serviceMessage}` : ''}`, {
88
+ code: 'PAIR_EXCHANGE_FAILED',
89
+ endpoint,
90
+ status,
91
+ serviceCode: raw.code,
92
+ success: false,
93
+ ...(serviceMessage ? { serviceMessage } : {})
94
+ });
95
+ }
71
96
  function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
72
97
  const root = unwrapPayload(raw);
73
98
  const pluginId = pickString(root.pluginId) ?? pickString(root.plugin_id) ?? DEFAULT_PLUGIN_ID;
@@ -341,6 +366,30 @@ function pickRecord(...values) {
341
366
  function isRecord(value) {
342
367
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
343
368
  }
369
+ function describeErrorCause(error) {
370
+ const detail = {};
371
+ if (error instanceof Error) {
372
+ detail.name = error.name;
373
+ detail.message = error.message;
374
+ }
375
+ else {
376
+ detail.message = String(error);
377
+ }
378
+ const cause = error?.cause;
379
+ if (isRecord(cause)) {
380
+ const causeDetail = {};
381
+ for (const key of ['name', 'message', 'code', 'errno', 'syscall', 'hostname', 'address', 'port']) {
382
+ const value = cause[key];
383
+ if (['string', 'number', 'boolean'].includes(typeof value)) {
384
+ causeDetail[key] = value;
385
+ }
386
+ }
387
+ if (Object.keys(causeDetail).length > 0) {
388
+ detail.cause = causeDetail;
389
+ }
390
+ }
391
+ return detail;
392
+ }
344
393
  function throwPairingError(code, message, debug) {
345
394
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
346
395
  code,
@@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile);
9
9
  const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
10
10
  const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
11
11
  const CHANNEL_FALLBACK_VERSION = '1.5.9';
12
+ const MIN_OPENCLAW_PLUGIN_UPDATE_VERSION = '2026.5.6';
12
13
  export const ping = async () => {
13
14
  return {
14
15
  ok: true,
@@ -74,6 +75,18 @@ export const doctorFix = async (_params, context) => {
74
75
  }
75
76
  };
76
77
  export const pluginSelfUpdate = async (_params, context) => {
78
+ const versionCheck = await checkOpenClawVersionForPluginUpdate(context);
79
+ if (!versionCheck.supported) {
80
+ return {
81
+ ok: false,
82
+ action: 'pluginSelfUpdate',
83
+ plugin: 'rol-websocket-channel',
84
+ skipped: true,
85
+ reason: versionCheck.reason,
86
+ message: versionCheck.message,
87
+ restartRecommended: false
88
+ };
89
+ }
77
90
  const result = await runOpenClawCommand(['plugins', 'update', 'rol-websocket-channel'], context, 'pluginSelfUpdate');
78
91
  const output = `${result.stdout}\n${result.stderr}`;
79
92
  if (isPathSourceUpdateSkip(output)) {
@@ -96,6 +109,45 @@ export const pluginSelfUpdate = async (_params, context) => {
96
109
  ...result
97
110
  };
98
111
  };
112
+ async function checkOpenClawVersionForPluginUpdate(context) {
113
+ try {
114
+ const result = await runOpenClawCommand(['--version'], context, 'pluginSelfUpdate.versionCheck');
115
+ const output = `${result.stdout}\n${result.stderr}`.trim();
116
+ const currentVersion = extractOpenClawVersion(output);
117
+ if (!currentVersion) {
118
+ return {
119
+ supported: false,
120
+ reason: 'openclaw-version-unreadable',
121
+ message: `Unable to detect OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
122
+ currentVersion: null,
123
+ output
124
+ };
125
+ }
126
+ if (!isOpenClawVersionAtLeast(currentVersion, MIN_OPENCLAW_PLUGIN_UPDATE_VERSION)) {
127
+ return {
128
+ supported: false,
129
+ reason: 'openclaw-version-too-old',
130
+ message: `OpenClaw ${currentVersion} is too old for rol-websocket-channel self update. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer.`,
131
+ currentVersion,
132
+ output
133
+ };
134
+ }
135
+ return {
136
+ supported: true,
137
+ currentVersion,
138
+ output
139
+ };
140
+ }
141
+ catch (error) {
142
+ return {
143
+ supported: false,
144
+ reason: 'openclaw-version-check-failed',
145
+ message: `Unable to check OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
146
+ currentVersion: null,
147
+ output: error instanceof Error ? error.message : String(error)
148
+ };
149
+ }
150
+ }
99
151
  export const currentVersion = async (_params, context) => {
100
152
  const [channelPackage, registryInfo] = await Promise.all([
101
153
  readJsonFile(path.join(context.projectRoot, 'package.json')),
@@ -117,6 +169,23 @@ export const currentVersion = async (_params, context) => {
117
169
  };
118
170
  };
119
171
  export const logs = async (params, context) => {
172
+ const limit = 10;
173
+ const maxBytes = normalizePositiveInteger(params?.maxBytes, 250000);
174
+ try {
175
+ const result = await runOpenClawCommand(['logs', '--json', '--limit', String(limit), '--max-bytes', String(maxBytes)], context, 'logs');
176
+ return {
177
+ ok: true,
178
+ source: 'openclaw logs',
179
+ nextOffset: null,
180
+ lines: parseOpenClawLogOutput(result.stdout).slice(-limit).reverse()
181
+ };
182
+ }
183
+ catch (officialError) {
184
+ console.error(`[system] openclaw logs failed, falling back to local file scan: ${officialError instanceof Error ? officialError.message : String(officialError)}`);
185
+ return await readLocalLogFiles(context, limit, maxBytes);
186
+ }
187
+ };
188
+ async function readLocalLogFiles(context, limit, maxBytes) {
120
189
  try {
121
190
  // 根据实际情况可能需要调整,这里默认尝试 project root 或 user home 下的 .openclaw/logs
122
191
  // 很多时候全局日志位于 ~/.openclaw/logs/gateway.log 或工程目录下的 .openclaw 文件夹中
@@ -168,8 +237,6 @@ export const logs = async (params, context) => {
168
237
  if (logFiles.length === 0) {
169
238
  return { ok: false, error: `No .log files found in directory: ${logDir}` };
170
239
  }
171
- const limit = 10;
172
- const maxBytes = params?.maxBytes ?? 250000;
173
240
  let offset = undefined;
174
241
  // 获取所有候选日志的详细信息并排序(对应 ls -t)
175
242
  const fileStats = await Promise.all(logFiles.map(async (file) => {
@@ -226,7 +293,31 @@ export const logs = async (params, context) => {
226
293
  error: error instanceof Error ? error.message : String(error)
227
294
  };
228
295
  }
229
- };
296
+ }
297
+ function parseOpenClawLogOutput(output) {
298
+ return output
299
+ .split(/\r?\n/)
300
+ .map((line) => line.trim())
301
+ .filter((line) => line.length > 0)
302
+ .map((line) => {
303
+ try {
304
+ return JSON.parse(line);
305
+ }
306
+ catch {
307
+ return line;
308
+ }
309
+ })
310
+ .filter((entry) => !(isJsonObject(entry) && entry.type === 'meta'));
311
+ }
312
+ function normalizePositiveInteger(value, fallback) {
313
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
314
+ return fallback;
315
+ }
316
+ return Math.floor(value);
317
+ }
318
+ function isJsonObject(value) {
319
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
320
+ }
230
321
  async function runOpenClawCommand(args, context, action) {
231
322
  const command = process.env.OPENCLAW_BIN || 'openclaw';
232
323
  const options = buildExecOptions(context.openclawRoot, context.openclawRoot);
@@ -339,6 +430,28 @@ function normalizeVersion(value) {
339
430
  }
340
431
  return value.trim().replace(/^v/i, '');
341
432
  }
433
+ function isOpenClawVersionAtLeast(current, minimum) {
434
+ const currentVersion = extractOpenClawVersion(current);
435
+ const minimumVersion = extractOpenClawVersion(minimum);
436
+ if (!currentVersion || !minimumVersion) {
437
+ return false;
438
+ }
439
+ const currentParts = currentVersion.split('.').map(Number);
440
+ const minimumParts = minimumVersion.split('.').map(Number);
441
+ for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i += 1) {
442
+ const currentPart = currentParts[i] ?? 0;
443
+ const minimumPart = minimumParts[i] ?? 0;
444
+ if (currentPart > minimumPart)
445
+ return true;
446
+ if (currentPart < minimumPart)
447
+ return false;
448
+ }
449
+ return true;
450
+ }
451
+ function extractOpenClawVersion(value) {
452
+ const match = value.match(/\b(\d{4}\.\d+\.\d+)\b/);
453
+ return match ? match[1] : null;
454
+ }
342
455
  function isPathSourceUpdateSkip(output) {
343
456
  return /Skipping\s+"?rol-websocket-channel"?\s+\(source:\s*path\)/i.test(output);
344
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
@@ -114,11 +114,24 @@ async function exchangePairKey(
114
114
  headers.Authorization = auth;
115
115
  }
116
116
 
117
- const response = await fetch(endpoint, {
118
- method: 'POST',
119
- headers,
120
- body: JSON.stringify({ key })
121
- });
117
+ let response: Response;
118
+ try {
119
+ response = await fetch(endpoint, {
120
+ method: 'POST',
121
+ headers,
122
+ body: JSON.stringify({ key })
123
+ });
124
+ } catch (error) {
125
+ throw new JsonRpcException(
126
+ JSON_RPC_ERRORS.internalError,
127
+ `pair exchange request failed: ${error instanceof Error ? error.message : String(error)}`,
128
+ {
129
+ code: 'PAIR_EXCHANGE_REQUEST_FAILED',
130
+ endpoint,
131
+ cause: describeErrorCause(error)
132
+ }
133
+ );
134
+ }
122
135
 
123
136
  const rawText = await response.text();
124
137
  const payload = tryParseJson(rawText);
@@ -135,9 +148,31 @@ async function exchangePairKey(
135
148
  );
136
149
  }
137
150
 
151
+ ensurePairExchangeSucceeded(payload, endpoint, response.status);
152
+
138
153
  return normalizePairingPayload(payload, endpoint, existingMqttUrl);
139
154
  }
140
155
 
156
+ function ensurePairExchangeSucceeded(raw: unknown, endpoint: string, status: number): void {
157
+ if (!isRecord(raw) || raw.success !== false) {
158
+ return;
159
+ }
160
+
161
+ const serviceMessage = pickString(raw.message) ?? pickString(raw.msg);
162
+ throw new JsonRpcException(
163
+ JSON_RPC_ERRORS.internalError,
164
+ `pair exchange failed${serviceMessage ? `: ${serviceMessage}` : ''}`,
165
+ {
166
+ code: 'PAIR_EXCHANGE_FAILED',
167
+ endpoint,
168
+ status,
169
+ serviceCode: raw.code,
170
+ success: false,
171
+ ...(serviceMessage ? { serviceMessage } : {})
172
+ }
173
+ );
174
+ }
175
+
141
176
  function normalizePairingPayload(
142
177
  raw: unknown,
143
178
  endpoint: string,
@@ -448,6 +483,33 @@ function isRecord(value: unknown): value is Record<string, any> {
448
483
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
449
484
  }
450
485
 
486
+ function describeErrorCause(error: unknown): Record<string, unknown> {
487
+ const detail: Record<string, unknown> = {};
488
+ if (error instanceof Error) {
489
+ detail.name = error.name;
490
+ detail.message = error.message;
491
+ } else {
492
+ detail.message = String(error);
493
+ }
494
+
495
+ const cause = (error as { cause?: unknown } | null | undefined)?.cause;
496
+ if (isRecord(cause)) {
497
+ const causeDetail: Record<string, unknown> = {};
498
+ for (const key of ['name', 'message', 'code', 'errno', 'syscall', 'hostname', 'address', 'port']) {
499
+ const value = cause[key];
500
+ if (['string', 'number', 'boolean'].includes(typeof value)) {
501
+ causeDetail[key] = value;
502
+ }
503
+ }
504
+
505
+ if (Object.keys(causeDetail).length > 0) {
506
+ detail.cause = causeDetail;
507
+ }
508
+ }
509
+
510
+ return detail;
511
+ }
512
+
451
513
  function throwPairingError(code: string, message: string, debug?: PairingPayloadDebug): never {
452
514
  throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, {
453
515
  code,
@@ -11,6 +11,7 @@ const execFileAsync = promisify(execFile);
11
11
  const UPDATE_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
12
12
  const UPDATE_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
13
13
  const CHANNEL_FALLBACK_VERSION = '1.5.9';
14
+ const MIN_OPENCLAW_PLUGIN_UPDATE_VERSION = '2026.5.6';
14
15
 
15
16
  export const ping: MethodHandler = async (): Promise<JsonValue> => {
16
17
  return {
@@ -81,6 +82,19 @@ export const doctorFix: MethodHandler = async (_params, context: MethodContext):
81
82
  };
82
83
 
83
84
  export const pluginSelfUpdate: MethodHandler = async (_params, context: MethodContext): Promise<JsonValue> => {
85
+ const versionCheck = await checkOpenClawVersionForPluginUpdate(context);
86
+ if (!versionCheck.supported) {
87
+ return {
88
+ ok: false,
89
+ action: 'pluginSelfUpdate',
90
+ plugin: 'rol-websocket-channel',
91
+ skipped: true,
92
+ reason: versionCheck.reason,
93
+ message: versionCheck.message,
94
+ restartRecommended: false
95
+ };
96
+ }
97
+
84
98
  const result = await runOpenClawCommand(
85
99
  ['plugins', 'update', 'rol-websocket-channel'],
86
100
  context,
@@ -110,6 +124,61 @@ export const pluginSelfUpdate: MethodHandler = async (_params, context: MethodCo
110
124
  };
111
125
  };
112
126
 
127
+ type PluginUpdateVersionCheck =
128
+ | {
129
+ supported: true;
130
+ currentVersion: string;
131
+ output: string;
132
+ }
133
+ | {
134
+ supported: false;
135
+ reason: string;
136
+ message: string;
137
+ currentVersion: string | null;
138
+ output: string;
139
+ };
140
+
141
+ async function checkOpenClawVersionForPluginUpdate(context: MethodContext): Promise<PluginUpdateVersionCheck> {
142
+ try {
143
+ const result = await runOpenClawCommand(['--version'], context, 'pluginSelfUpdate.versionCheck');
144
+ const output = `${result.stdout}\n${result.stderr}`.trim();
145
+ const currentVersion = extractOpenClawVersion(output);
146
+ if (!currentVersion) {
147
+ return {
148
+ supported: false,
149
+ reason: 'openclaw-version-unreadable',
150
+ message: `Unable to detect OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
151
+ currentVersion: null,
152
+ output
153
+ };
154
+ }
155
+
156
+ if (!isOpenClawVersionAtLeast(currentVersion, MIN_OPENCLAW_PLUGIN_UPDATE_VERSION)) {
157
+ return {
158
+ supported: false,
159
+ reason: 'openclaw-version-too-old',
160
+ message: `OpenClaw ${currentVersion} is too old for rol-websocket-channel self update. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer.`,
161
+ currentVersion,
162
+ output
163
+ };
164
+ }
165
+
166
+ return {
167
+ supported: true,
168
+ currentVersion,
169
+ output
170
+ };
171
+ } catch (error) {
172
+ return {
173
+ supported: false,
174
+ reason: 'openclaw-version-check-failed',
175
+ message: `Unable to check OpenClaw version. Please install or upgrade OpenClaw to ${MIN_OPENCLAW_PLUGIN_UPDATE_VERSION} or newer before updating rol-websocket-channel.`,
176
+ currentVersion: null,
177
+ output: error instanceof Error ? error.message : String(error)
178
+ };
179
+ }
180
+ }
181
+
113
182
  export const currentVersion: MethodHandler = async (_params, context: MethodContext): Promise<JsonValue> => {
114
183
  const [channelPackage, registryInfo] = await Promise.all([
115
184
  readJsonFile<{ name?: string; version?: string }>(path.join(context.projectRoot, 'package.json')),
@@ -140,6 +209,34 @@ export const currentVersion: MethodHandler = async (_params, context: MethodCont
140
209
  };
141
210
 
142
211
  export const logs: MethodHandler = async (params: any, context: MethodContext): Promise<JsonValue> => {
212
+ const limit = 10;
213
+ const maxBytes = normalizePositiveInteger(params?.maxBytes, 250000);
214
+
215
+ try {
216
+ const result = await runOpenClawCommand(
217
+ ['logs', '--json', '--limit', String(limit), '--max-bytes', String(maxBytes)],
218
+ context,
219
+ 'logs'
220
+ );
221
+ return {
222
+ ok: true,
223
+ source: 'openclaw logs',
224
+ nextOffset: null,
225
+ lines: parseOpenClawLogOutput(result.stdout).slice(-limit).reverse()
226
+ };
227
+ } catch (officialError) {
228
+ console.error(
229
+ `[system] openclaw logs failed, falling back to local file scan: ${officialError instanceof Error ? officialError.message : String(officialError)}`
230
+ );
231
+ return await readLocalLogFiles(context, limit, maxBytes);
232
+ }
233
+ };
234
+
235
+ async function readLocalLogFiles(
236
+ context: MethodContext,
237
+ limit: number,
238
+ maxBytes: number
239
+ ): Promise<JsonValue> {
143
240
  try {
144
241
  // 根据实际情况可能需要调整,这里默认尝试 project root 或 user home 下的 .openclaw/logs
145
242
  // 很多时候全局日志位于 ~/.openclaw/logs/gateway.log 或工程目录下的 .openclaw 文件夹中
@@ -201,8 +298,6 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
201
298
  return { ok: false, error: `No .log files found in directory: ${logDir}` };
202
299
  }
203
300
 
204
- const limit = 10;
205
- const maxBytes = params?.maxBytes ?? 250000;
206
301
  let offset: number | undefined = undefined;
207
302
 
208
303
  // 获取所有候选日志的详细信息并排序(对应 ls -t)
@@ -267,7 +362,34 @@ export const logs: MethodHandler = async (params: any, context: MethodContext):
267
362
  error: error instanceof Error ? error.message : String(error)
268
363
  };
269
364
  }
270
- };
365
+ }
366
+
367
+ function parseOpenClawLogOutput(output: string): JsonValue[] {
368
+ return output
369
+ .split(/\r?\n/)
370
+ .map((line) => line.trim())
371
+ .filter((line) => line.length > 0)
372
+ .map((line) => {
373
+ try {
374
+ return JSON.parse(line) as JsonValue;
375
+ } catch {
376
+ return line;
377
+ }
378
+ })
379
+ .filter((entry) => !(isJsonObject(entry) && entry.type === 'meta'));
380
+ }
381
+
382
+ function normalizePositiveInteger(value: unknown, fallback: number): number {
383
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
384
+ return fallback;
385
+ }
386
+
387
+ return Math.floor(value);
388
+ }
389
+
390
+ function isJsonObject(value: JsonValue): value is { [key: string]: JsonValue } {
391
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
392
+ }
271
393
 
272
394
  async function runOpenClawCommand(
273
395
  args: string[],
@@ -432,6 +554,30 @@ function normalizeVersion(value: string | undefined): string {
432
554
  return value.trim().replace(/^v/i, '');
433
555
  }
434
556
 
557
+ function isOpenClawVersionAtLeast(current: string, minimum: string): boolean {
558
+ const currentVersion = extractOpenClawVersion(current);
559
+ const minimumVersion = extractOpenClawVersion(minimum);
560
+ if (!currentVersion || !minimumVersion) {
561
+ return false;
562
+ }
563
+
564
+ const currentParts = currentVersion.split('.').map(Number);
565
+ const minimumParts = minimumVersion.split('.').map(Number);
566
+ for (let i = 0; i < Math.max(currentParts.length, minimumParts.length); i += 1) {
567
+ const currentPart = currentParts[i] ?? 0;
568
+ const minimumPart = minimumParts[i] ?? 0;
569
+ if (currentPart > minimumPart) return true;
570
+ if (currentPart < minimumPart) return false;
571
+ }
572
+
573
+ return true;
574
+ }
575
+
576
+ function extractOpenClawVersion(value: string): string | null {
577
+ const match = value.match(/\b(\d{4}\.\d+\.\d+)\b/);
578
+ return match ? match[1] : null;
579
+ }
580
+
435
581
  function isPathSourceUpdateSkip(output: string): boolean {
436
582
  return /Skipping\s+"?rol-websocket-channel"?\s+\(source:\s*path\)/i.test(output);
437
583
  }