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 +63 -1
- package/message-handler.ts +23 -0
- package/package.json +1 -1
- package/readme.md +6 -1
- package/src/admin/methods/index.ts +7 -1
- package/src/admin/methods/mem9.ts +341 -0
- package/src/admin/methods/pairing.ts +24 -1
- package/src/mqtt/connection-manager.ts +5 -6
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.
|
|
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: [
|
package/message-handler.ts
CHANGED
|
@@ -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
package/readme.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
}
|
|
69
|
-
if (topic.endsWith("#")) return topic;
|
|
70
|
-
|
|
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
|
/**
|