rol-websocket-channel 1.0.5 → 1.0.7

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
@@ -441,7 +441,7 @@ async function handleIncomingMessage(
441
441
  };
442
442
 
443
443
  const targetAgentId: string | null = innerData.agentId ?? innerData.agent_id ?? null;
444
- const targetSessionId: string | null = innerData.sessionId ?? innerData.session_id ?? null;
444
+ const targetSessionId: string | null = innerData.sessionKey ?? innerData.session_key ?? null;
445
445
 
446
446
  log?.info(
447
447
  `[rol-websocket-channel] 📨 Received: "${normalizedMessage.text}" from ${normalizedMessage.senderId}` +
@@ -684,6 +684,68 @@ function registerAdminBridgeCli(api: any) {
684
684
  }
685
685
  },
686
686
  );
687
+
688
+ const mem9 = root
689
+ .command("mem9")
690
+ .description("Mem9 installer and reconnect utilities");
691
+
692
+ mem9
693
+ .command("install")
694
+ .description("Install mem9 plugin, create cloud key, write config, and restart gateway")
695
+ .action(async () => {
696
+ try {
697
+ const { installMem9 } = await import("./src/admin/methods/mem9.js");
698
+ const result = await installMem9(getContext());
699
+ process.stdout.write(
700
+ JSON.stringify({ ok: true, result }, null, 2) + "\n",
701
+ );
702
+ } catch (error) {
703
+ process.exitCode = 1;
704
+ process.stderr.write(
705
+ JSON.stringify(
706
+ {
707
+ ok: false,
708
+ error: {
709
+ message:
710
+ error instanceof Error ? error.message : String(error),
711
+ code: (error as any)?.data?.code,
712
+ },
713
+ },
714
+ null,
715
+ 2,
716
+ ) + "\n",
717
+ );
718
+ }
719
+ });
720
+
721
+ mem9
722
+ .command("reconnect <key>")
723
+ .description("Replace mem9 apiKey, update config, and restart gateway")
724
+ .action(async (key: string) => {
725
+ try {
726
+ const { reconnectMem9 } = await import("./src/admin/methods/mem9.js");
727
+ const result = await reconnectMem9(key, getContext());
728
+ process.stdout.write(
729
+ JSON.stringify({ ok: true, result }, null, 2) + "\n",
730
+ );
731
+ } catch (error) {
732
+ process.exitCode = 1;
733
+ process.stderr.write(
734
+ JSON.stringify(
735
+ {
736
+ ok: false,
737
+ error: {
738
+ message:
739
+ error instanceof Error ? error.message : String(error),
740
+ code: (error as any)?.data?.code,
741
+ },
742
+ },
743
+ null,
744
+ 2,
745
+ ) + "\n",
746
+ );
747
+ }
748
+ });
687
749
  },
688
750
  {
689
751
  descriptors: [
@@ -35,6 +35,7 @@ import {
35
35
  createMemoryBackupRecord,
36
36
  importMemoryZip,
37
37
  } from './src/admin/methods/memory.js';
38
+ import { getMem9Config, installMem9, reconnectMem9 } from './src/admin/methods/mem9.js';
38
39
  import { restart, stop, doctorFix, logs } from './src/admin/methods/system.js';
39
40
 
40
41
  export class MessageHandler {
@@ -356,6 +357,28 @@ export class MessageHandler {
356
357
  });
357
358
  }
358
359
 
360
+ async mem9Install(_data: any): Promise<any> {
361
+ return wrapAdminCall(async () => {
362
+ const context = getContext();
363
+ return await installMem9(context);
364
+ });
365
+ }
366
+
367
+ async mem9GetConfig(_data: any): Promise<any> {
368
+ return wrapAdminCall(async () => {
369
+ const context = getContext();
370
+ return await getMem9Config(context);
371
+ });
372
+ }
373
+
374
+ async mem9Reconnect(data: any): Promise<any> {
375
+ return wrapAdminCall(async () => {
376
+ const context = getContext();
377
+ const key = typeof data?.key === 'string' ? data.key : '';
378
+ return await reconnectMem9(key, context);
379
+ });
380
+ }
381
+
359
382
  /**
360
383
  * 重启 OpenClaw Gateway
361
384
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rol-websocket-channel",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Unified OpenClaw plugin: MQTT Channel + Admin Bridge for remote management",
5
5
  "license": "MIT",
6
6
  "author": "nixgnehc",
package/readme.md CHANGED
@@ -30,4 +30,9 @@ announcement/tester
30
30
  npm login
31
31
  npm publish
32
32
 
33
- ```
33
+ ```
34
+
35
+ ```
36
+ openclaw admin-bridge mem9 install
37
+ openclaw admin-bridge mem9 reconnect <key>
38
+ ```
@@ -11,6 +11,7 @@ import {
11
11
  importMemoryZip,
12
12
  listMemoryFiles
13
13
  } from './memory.ts';
14
+ import { getMem9Config, installMem9, reconnectMem9 } from './mem9.ts';
14
15
  import { getModels } from './models.ts';
15
16
  import { setModel, updateModels } from './models-extended.ts';
16
17
  import { listSessions } from './sessions.ts';
@@ -85,7 +86,12 @@ const methods = new Map<string, MethodHandler>([
85
86
  ['memory.exportZip', exportMemoryZip],
86
87
  ['memory.getPresignedPost', getMemoryPresignedPost],
87
88
  ['memory.createBackupRecord', createMemoryBackupRecord],
88
- ['memory.importZip', importMemoryZip]
89
+ ['memory.importZip', importMemoryZip],
90
+
91
+ // Mem9
92
+ ['mem9.install', installMem9],
93
+ ['mem9.getConfig', getMem9Config],
94
+ ['mem9.reconnect', reconnectMem9]
89
95
  ]);
90
96
 
91
97
  export function getMethod(methodName: string): MethodHandler {
@@ -0,0 +1,341 @@
1
+ import { execFile } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { promisify } from 'node:util';
4
+
5
+ import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
6
+ import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
7
+ import type { JsonValue, MethodContext } from '../types.ts';
8
+
9
+ const execFileAsync = promisify(execFile);
10
+
11
+ const MEM9_PLUGIN_SPEC = '@mem9/mem9';
12
+ const MEM9_PLUGIN_ID = 'mem9';
13
+ const MEM9_API_URL = 'https://api.mem9.ai';
14
+ const MEM9_CREATE_URL = `${MEM9_API_URL}/v1alpha1/mem9s`;
15
+ const GATEWAY_SERVICE = 'openclaw-gateway.service';
16
+
17
+ interface OpenClawConfig {
18
+ plugins?: {
19
+ entries?: Record<string, any>;
20
+ slots?: Record<string, any>;
21
+ installs?: Record<string, any>;
22
+ [key: string]: any;
23
+ };
24
+ [key: string]: any;
25
+ }
26
+
27
+ export async function installMem9(context: MethodContext): Promise<JsonValue> {
28
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
29
+ await ensureOpenClawCli();
30
+ await ensureNodeRuntime();
31
+
32
+ const currentState = readMem9State(config);
33
+ const installResult = currentState.installed
34
+ ? { attempted: false, installed: true }
35
+ : await installMem9Plugin(context.projectRoot);
36
+
37
+ if (currentState.configured && currentState.apiKey) {
38
+ const updated = await ensureMem9SlotConfig(context.openclawRoot, currentState.apiKey);
39
+ const restart = await restartGateway(context.projectRoot);
40
+
41
+ return {
42
+ ok: true,
43
+ installed: true,
44
+ alreadyInstalled: installResult.installed,
45
+ alreadyConfigured: true,
46
+ createdNewKey: false,
47
+ reusedExistingKey: true,
48
+ plugin: MEM9_PLUGIN_ID,
49
+ apiUrl: MEM9_API_URL,
50
+ apiKey: currentState.apiKey,
51
+ updated,
52
+ restart
53
+ };
54
+ }
55
+
56
+ const apiKey = await createMem9Key();
57
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
58
+ const restart = await restartGateway(context.projectRoot);
59
+
60
+ return {
61
+ ok: true,
62
+ installed: true,
63
+ alreadyInstalled: installResult.installed,
64
+ alreadyConfigured: false,
65
+ createdNewKey: true,
66
+ reusedExistingKey: false,
67
+ plugin: MEM9_PLUGIN_ID,
68
+ apiUrl: MEM9_API_URL,
69
+ apiKey,
70
+ updated,
71
+ restart
72
+ };
73
+ }
74
+
75
+ export async function reconnectMem9(key: string, context: MethodContext): Promise<JsonValue> {
76
+ const apiKey = key.trim();
77
+ if (!apiKey) {
78
+ throwMem9Error('MEM9_KEY_REQUIRED', 'mem9 key is required');
79
+ }
80
+
81
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
82
+ const previousState = readMem9State(config);
83
+ const updated = await writeMem9Config(context.openclawRoot, apiKey);
84
+ const restart = await restartGateway(context.projectRoot);
85
+
86
+ return {
87
+ ok: true,
88
+ reconnected: true,
89
+ replacedExistingKey: Boolean(previousState.apiKey && previousState.apiKey !== apiKey),
90
+ plugin: MEM9_PLUGIN_ID,
91
+ apiUrl: MEM9_API_URL,
92
+ apiKey,
93
+ updated: ['plugins.entries.mem9.config.apiKey', ...updated.filter((item) => item !== 'plugins.entries.mem9' && item !== 'plugins.slots.memory')],
94
+ restart
95
+ };
96
+ }
97
+
98
+ export async function getMem9Config(context: MethodContext): Promise<JsonValue> {
99
+ const config = await ensureOpenClawConfigExists(context.openclawRoot);
100
+ const state = readMem9State(config);
101
+ const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
102
+ const pluginConfig = isRecord(entry.config) ? entry.config : {};
103
+
104
+ return {
105
+ ok: true,
106
+ installed: state.installed,
107
+ configured: state.configured,
108
+ plugin: MEM9_PLUGIN_ID,
109
+ enabled: entry.enabled === true,
110
+ apiUrl: pickString(pluginConfig.apiUrl) ?? MEM9_API_URL,
111
+ apiKey: state.apiKey,
112
+ slot: config.plugins?.slots?.memory ?? null
113
+ };
114
+ }
115
+
116
+ async function ensureOpenClawConfigExists(openclawRoot: string): Promise<OpenClawConfig> {
117
+ const configPath = path.join(openclawRoot, 'openclaw.json');
118
+ if (!(await pathExists(configPath))) {
119
+ throwMem9Error('MEM9_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
120
+ }
121
+
122
+ return await readJsonFile<OpenClawConfig>(configPath);
123
+ }
124
+
125
+ async function ensureOpenClawCli(): Promise<void> {
126
+ try {
127
+ await execFileAsync('openclaw', ['--version']);
128
+ } catch (error) {
129
+ throw new JsonRpcException(
130
+ JSON_RPC_ERRORS.internalError,
131
+ 'openclaw command is not available',
132
+ { code: 'MEM9_OPENCLAW_NOT_FOUND', detail: error instanceof Error ? error.message : String(error) }
133
+ );
134
+ }
135
+ }
136
+
137
+ async function ensureNodeRuntime(): Promise<void> {
138
+ try {
139
+ await execFileAsync('node', ['--version']);
140
+ await execFileAsync('npm', ['--version']);
141
+ } catch (error) {
142
+ throw new JsonRpcException(
143
+ JSON_RPC_ERRORS.internalError,
144
+ 'node or npm command is not available',
145
+ { code: 'MEM9_NODE_NOT_FOUND', detail: error instanceof Error ? error.message : String(error) }
146
+ );
147
+ }
148
+ }
149
+
150
+ async function runOpenClawCommand(args: string[], cwd: string, code: string): Promise<void> {
151
+ try {
152
+ await execFileAsync('openclaw', args, { cwd });
153
+ } catch (error: any) {
154
+ throw new JsonRpcException(
155
+ JSON_RPC_ERRORS.internalError,
156
+ `openclaw ${args.join(' ')} failed`,
157
+ {
158
+ code,
159
+ stdout: typeof error?.stdout === 'string' ? error.stdout : '',
160
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
161
+ }
162
+ );
163
+ }
164
+ }
165
+
166
+ async function installMem9Plugin(cwd: string): Promise<{ attempted: boolean; installed: boolean }> {
167
+ await runOpenClawCommand(['plugins', 'install', MEM9_PLUGIN_SPEC], cwd, 'MEM9_PLUGIN_INSTALL_FAILED');
168
+ return {
169
+ attempted: true,
170
+ installed: true
171
+ };
172
+ }
173
+
174
+ async function createMem9Key(): Promise<string> {
175
+ const response = await fetch(MEM9_CREATE_URL, {
176
+ method: 'POST',
177
+ headers: {
178
+ accept: 'application/json',
179
+ 'content-type': 'application/json'
180
+ },
181
+ body: JSON.stringify({})
182
+ });
183
+
184
+ const rawText = await response.text();
185
+ const payload = tryParseJson(rawText);
186
+ if (!response.ok) {
187
+ throw new JsonRpcException(
188
+ JSON_RPC_ERRORS.internalError,
189
+ `mem9 key create failed: ${response.status}`,
190
+ {
191
+ code: 'MEM9_KEY_CREATE_FAILED',
192
+ status: response.status,
193
+ payload
194
+ }
195
+ );
196
+ }
197
+
198
+ const root = isRecord(payload) && isRecord(payload.data) ? payload.data : isRecord(payload) ? payload : null;
199
+ const key = pickString(root?.id) ?? pickString(root?.value) ?? pickString(root?.apiKey);
200
+ if (!key) {
201
+ throwMem9Error('MEM9_KEY_CREATE_FAILED', 'mem9 create response missing id');
202
+ }
203
+
204
+ return key;
205
+ }
206
+
207
+ async function writeMem9Config(openclawRoot: string, apiKey: string): Promise<string[]> {
208
+ const configPath = path.join(openclawRoot, 'openclaw.json');
209
+ const config = await readJsonFile<OpenClawConfig>(configPath);
210
+
211
+ if (!config.plugins) config.plugins = {};
212
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
213
+ config.plugins.entries = {};
214
+ }
215
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
216
+ config.plugins.slots = {};
217
+ }
218
+
219
+ const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
220
+ const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
221
+ const hadExistingKey = typeof existingPluginConfig.apiKey === 'string' && existingPluginConfig.apiKey.trim().length > 0;
222
+
223
+ config.plugins.entries[MEM9_PLUGIN_ID] = {
224
+ ...existingEntry,
225
+ enabled: true,
226
+ config: {
227
+ ...existingPluginConfig,
228
+ apiUrl: MEM9_API_URL,
229
+ apiKey
230
+ }
231
+ };
232
+
233
+ config.plugins.slots.memory = MEM9_PLUGIN_ID;
234
+ await writeJsonFile(configPath, config);
235
+
236
+ return [
237
+ hadExistingKey ? 'plugins.entries.mem9.config.apiKey (replaced)' : 'plugins.entries.mem9',
238
+ 'plugins.slots.memory'
239
+ ];
240
+ }
241
+
242
+ async function ensureMem9SlotConfig(openclawRoot: string, apiKey: string): Promise<string[]> {
243
+ const configPath = path.join(openclawRoot, 'openclaw.json');
244
+ const config = await readJsonFile<OpenClawConfig>(configPath);
245
+
246
+ if (!config.plugins) config.plugins = {};
247
+ if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
248
+ config.plugins.entries = {};
249
+ }
250
+ if (!config.plugins.slots || typeof config.plugins.slots !== 'object') {
251
+ config.plugins.slots = {};
252
+ }
253
+
254
+ const existingEntry = isRecord(config.plugins.entries[MEM9_PLUGIN_ID]) ? config.plugins.entries[MEM9_PLUGIN_ID] : {};
255
+ const existingPluginConfig = isRecord(existingEntry.config) ? existingEntry.config : {};
256
+
257
+ config.plugins.entries[MEM9_PLUGIN_ID] = {
258
+ ...existingEntry,
259
+ enabled: true,
260
+ config: {
261
+ ...existingPluginConfig,
262
+ apiUrl: MEM9_API_URL,
263
+ apiKey
264
+ }
265
+ };
266
+
267
+ config.plugins.slots.memory = MEM9_PLUGIN_ID;
268
+ await writeJsonFile(configPath, config);
269
+
270
+ return [
271
+ 'plugins.entries.mem9',
272
+ 'plugins.slots.memory'
273
+ ];
274
+ }
275
+
276
+ async function restartGateway(cwd: string): Promise<JsonValue> {
277
+ try {
278
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
279
+ return {
280
+ attempted: true,
281
+ success: true
282
+ };
283
+ } catch (error: any) {
284
+ return {
285
+ attempted: true,
286
+ success: false,
287
+ message: error instanceof Error ? error.message : String(error),
288
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
289
+ };
290
+ }
291
+ }
292
+
293
+ function tryParseJson(raw: string): unknown {
294
+ const trimmed = raw.trim();
295
+ if (!trimmed) {
296
+ return {};
297
+ }
298
+
299
+ try {
300
+ return JSON.parse(trimmed);
301
+ } catch {
302
+ return raw;
303
+ }
304
+ }
305
+
306
+ function pickString(value: unknown): string | null {
307
+ if (typeof value !== 'string') {
308
+ return null;
309
+ }
310
+
311
+ const trimmed = value.trim();
312
+ return trimmed.length > 0 ? trimmed : null;
313
+ }
314
+
315
+ function isRecord(value: unknown): value is Record<string, any> {
316
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
317
+ }
318
+
319
+ function readMem9State(config: OpenClawConfig): {
320
+ installed: boolean;
321
+ configured: boolean;
322
+ apiKey: string | null;
323
+ } {
324
+ const installed = Boolean(
325
+ (config.plugins?.installs && typeof config.plugins.installs === 'object' && MEM9_PLUGIN_ID in config.plugins.installs)
326
+ || (config.plugins?.entries && typeof config.plugins.entries === 'object' && MEM9_PLUGIN_ID in config.plugins.entries)
327
+ );
328
+ const entry = isRecord(config.plugins?.entries?.[MEM9_PLUGIN_ID]) ? config.plugins?.entries?.[MEM9_PLUGIN_ID] : {};
329
+ const pluginConfig = isRecord(entry.config) ? entry.config : {};
330
+ const apiKey = pickString(pluginConfig.apiKey);
331
+
332
+ return {
333
+ installed,
334
+ configured: Boolean(apiKey),
335
+ apiKey
336
+ };
337
+ }
338
+
339
+ function throwMem9Error(code: string, message: string): never {
340
+ throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
341
+ }
@@ -1,11 +1,16 @@
1
+ import { execFile } from 'node:child_process';
1
2
  import path from 'node:path';
3
+ import { promisify } from 'node:util';
2
4
 
3
5
  import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.ts';
4
6
  import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.ts';
5
7
  import type { JsonValue, MethodContext } from '../types.ts';
6
8
 
9
+ const execFileAsync = promisify(execFile);
10
+
7
11
  const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
8
12
  const DEFAULT_PAIR_ENDPOINT = 'http://api.deotaland.local/api-core-bot/front/agent/agent/key/query';
13
+ const GATEWAY_SERVICE = 'openclaw-gateway.service';
9
14
 
10
15
  interface PairingCommandOptions {
11
16
  key: string;
@@ -57,6 +62,7 @@ export async function pairWithKey(
57
62
  const payload = await exchangePairKey(key, options.endpoint, options.auth, existingMqttUrl);
58
63
  applyPairingConfig(config, key, payload);
59
64
  await writeJsonFile(configPath, config);
65
+ const restart = await restartGateway(context.projectRoot);
60
66
 
61
67
  return {
62
68
  ok: true,
@@ -69,7 +75,7 @@ export async function pairWithKey(
69
75
  `channels.${payload.pluginId}`
70
76
  ],
71
77
  channel: payload.channel,
72
- next: 'restart-openclaw-gateway'
78
+ restart
73
79
  };
74
80
  }
75
81
 
@@ -285,6 +291,23 @@ function resolveExistingMqttUrl(config: OpenClawConfig): string | null {
285
291
  return pickString(channelConfig.mqttUrl);
286
292
  }
287
293
 
294
+ async function restartGateway(cwd: string): Promise<JsonValue> {
295
+ try {
296
+ await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
297
+ return {
298
+ attempted: true,
299
+ success: true
300
+ };
301
+ } catch (error: any) {
302
+ return {
303
+ attempted: true,
304
+ success: false,
305
+ message: error instanceof Error ? error.message : String(error),
306
+ stderr: typeof error?.stderr === 'string' ? error.stderr : ''
307
+ };
308
+ }
309
+ }
310
+
288
311
  function normalizeGroupPolicy(value: string): 'pairing' | 'allowlist' | 'open' | 'disabled' {
289
312
  if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
290
313
  return value;
@@ -63,12 +63,11 @@ export function parseUsernameFromTopic(topic: string): string {
63
63
  */
64
64
  export function getSubscribeTopic(topic: string): string {
65
65
  const username = parseUsernameFromTopic(topic);
66
- if (username !== "default_name") {
67
- return `announcement/${username}/#`;
68
- }
69
- if (topic.endsWith("#")) return topic;
70
- if (topic.endsWith("/")) return `${topic}#`;
71
- return `${topic}/#`;
66
+ // if (username !== "default_name") {
67
+ // return `announcement/${username}/#`;
68
+ // }
69
+ // if (topic.endsWith("#")) return topic;
70
+ return topic;
72
71
  }
73
72
 
74
73
  /**