rol-websocket-channel 1.4.2 → 1.4.8
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/{MQTT-API /346/226/260/345/242/236/346/226/207/344/273/266/345/212/237/350/203/275.md" → MQTT-API 5-6.md } +89 -1
- package/dist/index.js +617 -617
- package/dist/message-handler.js +515 -503
- package/dist/src/admin/cli.js +43 -43
- package/dist/src/admin/jsonrpc.js +60 -60
- package/dist/src/admin/lib/fs.js +30 -30
- package/dist/src/admin/lib/paths.js +80 -80
- package/dist/src/admin/methods/admin.js +60 -60
- package/dist/src/admin/methods/agents-extended.js +251 -251
- package/dist/src/admin/methods/artifacts.js +736 -642
- package/dist/src/admin/methods/artifacts.test.js +210 -191
- package/dist/src/admin/methods/cron.js +250 -250
- package/dist/src/admin/methods/index.js +104 -102
- package/dist/src/admin/methods/mem9.js +309 -270
- package/dist/src/admin/methods/mem9.test.js +34 -0
- package/dist/src/admin/methods/memory.js +363 -363
- package/dist/src/admin/methods/models-extended.js +190 -190
- package/dist/src/admin/methods/models.js +195 -195
- package/dist/src/admin/methods/pairing.js +268 -268
- package/dist/src/admin/methods/sessions-extended.js +215 -215
- package/dist/src/admin/methods/sessions.js +75 -75
- package/dist/src/admin/methods/skills-extended.js +157 -157
- package/dist/src/admin/methods/skills-toggle.js +183 -183
- package/dist/src/admin/methods/skills.js +528 -528
- package/dist/src/admin/methods/system.js +271 -180
- package/dist/src/admin/methods/usage.js +1170 -1170
- package/dist/src/admin/types.js +1 -1
- package/dist/src/mqtt/connection-manager.js +209 -209
- package/dist/src/mqtt/index.js +5 -5
- package/dist/src/mqtt/mqtt-client.js +110 -110
- package/dist/src/mqtt/mqtt.test.js +418 -418
- package/dist/src/mqtt/types.js +2 -2
- package/dist/src/shared/context.js +24 -24
- package/dist/src/shared/wrapper.js +23 -23
- package/message-handler.ts +15 -1
- package/openclaw.plugin.json +73 -0
- package/package.json +1 -1
- package/src/admin/methods/artifacts.test.ts +35 -0
- package/src/admin/methods/artifacts.ts +140 -2
- package/src/admin/methods/index.ts +3 -1
- package/src/admin/methods/mem9.test.ts +39 -0
- package/src/admin/methods/mem9.ts +48 -1
- package/src/admin/methods/system.ts +129 -1
|
@@ -1,268 +1,268 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { promisify } from 'node:util';
|
|
4
|
-
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
|
|
5
|
-
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
|
-
const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
|
|
8
|
-
const DEFAULT_PRODUCTION_BASE_URL = 'https://api.deotaland.ai';
|
|
9
|
-
const PAIR_ENDPOINT_PATH = '/api-core-bot/front/agent/agent/key/query';
|
|
10
|
-
const GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
11
|
-
export async function pairWithKey(options, context) {
|
|
12
|
-
const key = options.key.trim();
|
|
13
|
-
if (!key) {
|
|
14
|
-
throwPairingError('PAIR_KEY_REQUIRED', 'pair key is required');
|
|
15
|
-
}
|
|
16
|
-
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
17
|
-
const config = await loadConfig(configPath);
|
|
18
|
-
const existingMqttUrl = resolveExistingMqttUrl(config);
|
|
19
|
-
const endpoint = buildPairEndpoint(options.endpoint, config);
|
|
20
|
-
const payload = await exchangePairKey(key, endpoint, options.auth, existingMqttUrl);
|
|
21
|
-
applyPairingConfig(config, key, payload);
|
|
22
|
-
await writeJsonFile(configPath, config);
|
|
23
|
-
const restart = await restartGateway(context.projectRoot);
|
|
24
|
-
return {
|
|
25
|
-
ok: true,
|
|
26
|
-
openclawRoot: context.openclawRoot,
|
|
27
|
-
configFile: configPath,
|
|
28
|
-
pluginId: payload.pluginId,
|
|
29
|
-
pairingEndpoint: endpoint,
|
|
30
|
-
updated: [
|
|
31
|
-
'plugins.allow',
|
|
32
|
-
`plugins.entries.${payload.pluginId}`,
|
|
33
|
-
`channels.${payload.pluginId}`
|
|
34
|
-
],
|
|
35
|
-
channel: payload.channel,
|
|
36
|
-
restart
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
async function loadConfig(configPath) {
|
|
40
|
-
if (!(await pathExists(configPath))) {
|
|
41
|
-
throwPairingError('PAIR_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
|
|
42
|
-
}
|
|
43
|
-
return await readJsonFile(configPath);
|
|
44
|
-
}
|
|
45
|
-
async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
46
|
-
const auth = pickString(authOverride);
|
|
47
|
-
const headers = {
|
|
48
|
-
'Content-Type': 'application/json'
|
|
49
|
-
};
|
|
50
|
-
if (auth) {
|
|
51
|
-
headers.Authorization = auth;
|
|
52
|
-
}
|
|
53
|
-
const response = await fetch(endpoint, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers,
|
|
56
|
-
body: JSON.stringify({ key })
|
|
57
|
-
});
|
|
58
|
-
const rawText = await response.text();
|
|
59
|
-
const payload = tryParseJson(rawText);
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed: ${response.status}`, {
|
|
62
|
-
code: 'PAIR_EXCHANGE_FAILED',
|
|
63
|
-
endpoint,
|
|
64
|
-
status: response.status,
|
|
65
|
-
payload
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return normalizePairingPayload(payload, endpoint, existingMqttUrl);
|
|
69
|
-
}
|
|
70
|
-
function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
|
|
71
|
-
const root = unwrapPayload(raw);
|
|
72
|
-
const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
|
|
73
|
-
const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
|
|
74
|
-
const channelValue = isRecord(root.channel) ? root.channel : {};
|
|
75
|
-
const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
|
|
76
|
-
?? pickString(root.authToken)
|
|
77
|
-
?? pickString(root.token);
|
|
78
|
-
const rawValue = pickString(root.value);
|
|
79
|
-
const mqttUrl = pickString(channelValue.mqttUrl)
|
|
80
|
-
?? pickString(root.mqttUrl)
|
|
81
|
-
?? existingMqttUrl
|
|
82
|
-
?? null;
|
|
83
|
-
if (!mqttUrl) {
|
|
84
|
-
throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
|
|
85
|
-
}
|
|
86
|
-
const mqttTopic = pickString(channelValue.mqttTopic)
|
|
87
|
-
?? pickString(root.mqttTopic)
|
|
88
|
-
?? rawValue
|
|
89
|
-
?? 'announcement/tester';
|
|
90
|
-
const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
|
|
91
|
-
?? pickString(channelValue.dmPolicy)
|
|
92
|
-
?? pickString(root.groupPolicy)
|
|
93
|
-
?? pickString(root.dmPolicy)
|
|
94
|
-
?? 'pairing');
|
|
95
|
-
return {
|
|
96
|
-
pluginId,
|
|
97
|
-
pairing: {
|
|
98
|
-
...(rawValue ? { rawValue } : {})
|
|
99
|
-
},
|
|
100
|
-
apiCoreBot: {
|
|
101
|
-
baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
|
|
102
|
-
...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
|
|
103
|
-
},
|
|
104
|
-
channel: {
|
|
105
|
-
mqttUrl,
|
|
106
|
-
mqttTopic,
|
|
107
|
-
groupPolicy
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
function applyPairingConfig(config, key, payload) {
|
|
112
|
-
if (!config.plugins)
|
|
113
|
-
config.plugins = {};
|
|
114
|
-
if (!Array.isArray(config.plugins.allow))
|
|
115
|
-
config.plugins.allow = [];
|
|
116
|
-
if (!config.plugins.allow.includes(payload.pluginId)) {
|
|
117
|
-
config.plugins.allow.push(payload.pluginId);
|
|
118
|
-
}
|
|
119
|
-
if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
|
|
120
|
-
config.plugins.entries = {};
|
|
121
|
-
}
|
|
122
|
-
const existingPluginEntry = isRecord(config.plugins.entries[payload.pluginId])
|
|
123
|
-
? config.plugins.entries[payload.pluginId]
|
|
124
|
-
: {};
|
|
125
|
-
const existingPluginConfig = isRecord(existingPluginEntry.config) ? existingPluginEntry.config : {};
|
|
126
|
-
config.plugins.entries[payload.pluginId] = {
|
|
127
|
-
...existingPluginEntry,
|
|
128
|
-
enabled: true,
|
|
129
|
-
config: {
|
|
130
|
-
...existingPluginConfig,
|
|
131
|
-
pairing: {
|
|
132
|
-
paired: true,
|
|
133
|
-
pairedAt: new Date().toISOString(),
|
|
134
|
-
pairingKeyLast4: key.slice(-4),
|
|
135
|
-
...(payload.pairing.rawValue ? { rawValue: payload.pairing.rawValue } : {})
|
|
136
|
-
},
|
|
137
|
-
apiCoreBot: {
|
|
138
|
-
...(isRecord(existingPluginConfig.apiCoreBot) ? existingPluginConfig.apiCoreBot : {}),
|
|
139
|
-
...(payload.apiCoreBot.authToken ? { authToken: payload.apiCoreBot.authToken } : {})
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
if (!config.channels || typeof config.channels !== 'object') {
|
|
144
|
-
config.channels = {};
|
|
145
|
-
}
|
|
146
|
-
const existingChannelEntry = isRecord(config.channels[payload.pluginId])
|
|
147
|
-
? config.channels[payload.pluginId]
|
|
148
|
-
: {};
|
|
149
|
-
const existingChannelConfig = isRecord(existingChannelEntry.config) ? existingChannelEntry.config : {};
|
|
150
|
-
config.channels[payload.pluginId] = {
|
|
151
|
-
...existingChannelEntry,
|
|
152
|
-
enabled: true,
|
|
153
|
-
config: {
|
|
154
|
-
...existingChannelConfig,
|
|
155
|
-
enabled: true,
|
|
156
|
-
mqttUrl: payload.channel.mqttUrl,
|
|
157
|
-
mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
|
|
158
|
-
groupPolicy: payload.channel.groupPolicy
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
function unwrapPayload(raw) {
|
|
163
|
-
if (isRecord(raw)) {
|
|
164
|
-
if (isRecord(raw.data)) {
|
|
165
|
-
return raw.data;
|
|
166
|
-
}
|
|
167
|
-
return raw;
|
|
168
|
-
}
|
|
169
|
-
throwPairingError('PAIR_EXCHANGE_FAILED', 'unexpected pairing payload');
|
|
170
|
-
}
|
|
171
|
-
function tryParseJson(raw) {
|
|
172
|
-
const trimmed = raw.trim();
|
|
173
|
-
if (!trimmed) {
|
|
174
|
-
return {};
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
return JSON.parse(trimmed);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return raw;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function buildPairEndpoint(endpointOverride, config) {
|
|
184
|
-
const override = pickString(endpointOverride);
|
|
185
|
-
if (override) {
|
|
186
|
-
return normalizePairEndpoint(override);
|
|
187
|
-
}
|
|
188
|
-
const configured = resolveConfiguredPairingEndpoint(config);
|
|
189
|
-
return normalizePairEndpoint(configured ?? DEFAULT_PRODUCTION_BASE_URL);
|
|
190
|
-
}
|
|
191
|
-
function resolveConfiguredPairingEndpoint(config) {
|
|
192
|
-
const pluginEntry = isRecord(config.plugins?.entries?.[DEFAULT_PLUGIN_ID])
|
|
193
|
-
? config.plugins?.entries?.[DEFAULT_PLUGIN_ID]
|
|
194
|
-
: {};
|
|
195
|
-
const pluginConfig = isRecord(pluginEntry.config) ? pluginEntry.config : {};
|
|
196
|
-
const channelEntry = isRecord(config.channels?.[DEFAULT_PLUGIN_ID])
|
|
197
|
-
? config.channels?.[DEFAULT_PLUGIN_ID]
|
|
198
|
-
: {};
|
|
199
|
-
const channelConfig = isRecord(channelEntry.config) ? channelEntry.config : {};
|
|
200
|
-
return pickString(pluginConfig.pairingEndpoint)
|
|
201
|
-
?? pickString(channelEntry.pairingEndpoint)
|
|
202
|
-
?? pickString(channelConfig.pairingEndpoint);
|
|
203
|
-
}
|
|
204
|
-
function normalizePairEndpoint(value) {
|
|
205
|
-
const trimmed = value.trim().replace(/\/+$/, '');
|
|
206
|
-
try {
|
|
207
|
-
const parsed = new URL(trimmed);
|
|
208
|
-
if (!parsed.pathname || parsed.pathname === '/') {
|
|
209
|
-
return `${trimmed}${PAIR_ENDPOINT_PATH}`;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// Keep non-URL values unchanged so the eventual fetch error reports the actual input.
|
|
214
|
-
}
|
|
215
|
-
return trimmed;
|
|
216
|
-
}
|
|
217
|
-
function inferBaseUrl(endpoint) {
|
|
218
|
-
try {
|
|
219
|
-
const parsed = new URL(endpoint);
|
|
220
|
-
return parsed.origin;
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return DEFAULT_PRODUCTION_BASE_URL;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
function resolveExistingMqttUrl(config) {
|
|
227
|
-
const channelConfig = config.channels?.[DEFAULT_PLUGIN_ID]?.config;
|
|
228
|
-
if (!channelConfig || typeof channelConfig !== 'object' || Array.isArray(channelConfig)) {
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
return pickString(channelConfig.mqttUrl);
|
|
232
|
-
}
|
|
233
|
-
async function restartGateway(cwd) {
|
|
234
|
-
try {
|
|
235
|
-
await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
|
|
236
|
-
return {
|
|
237
|
-
attempted: true,
|
|
238
|
-
success: true
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
catch (error) {
|
|
242
|
-
return {
|
|
243
|
-
attempted: true,
|
|
244
|
-
success: false,
|
|
245
|
-
message: error instanceof Error ? error.message : String(error),
|
|
246
|
-
stderr: typeof error?.stderr === 'string' ? error.stderr : ''
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function normalizeGroupPolicy(value) {
|
|
251
|
-
if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
|
|
252
|
-
return value;
|
|
253
|
-
}
|
|
254
|
-
return 'pairing';
|
|
255
|
-
}
|
|
256
|
-
function pickString(value) {
|
|
257
|
-
if (typeof value !== 'string') {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
const trimmed = value.trim();
|
|
261
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
262
|
-
}
|
|
263
|
-
function isRecord(value) {
|
|
264
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
265
|
-
}
|
|
266
|
-
function throwPairingError(code, message) {
|
|
267
|
-
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
|
|
268
|
-
}
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { pathExists, readJsonFile, writeJsonFile } from '../lib/fs.js';
|
|
5
|
+
import { JsonRpcException, JSON_RPC_ERRORS } from '../jsonrpc.js';
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const DEFAULT_PLUGIN_ID = 'rol-websocket-channel';
|
|
8
|
+
const DEFAULT_PRODUCTION_BASE_URL = 'https://api.deotaland.ai';
|
|
9
|
+
const PAIR_ENDPOINT_PATH = '/api-core-bot/front/agent/agent/key/query';
|
|
10
|
+
const GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
11
|
+
export async function pairWithKey(options, context) {
|
|
12
|
+
const key = options.key.trim();
|
|
13
|
+
if (!key) {
|
|
14
|
+
throwPairingError('PAIR_KEY_REQUIRED', 'pair key is required');
|
|
15
|
+
}
|
|
16
|
+
const configPath = path.join(context.openclawRoot, 'openclaw.json');
|
|
17
|
+
const config = await loadConfig(configPath);
|
|
18
|
+
const existingMqttUrl = resolveExistingMqttUrl(config);
|
|
19
|
+
const endpoint = buildPairEndpoint(options.endpoint, config);
|
|
20
|
+
const payload = await exchangePairKey(key, endpoint, options.auth, existingMqttUrl);
|
|
21
|
+
applyPairingConfig(config, key, payload);
|
|
22
|
+
await writeJsonFile(configPath, config);
|
|
23
|
+
const restart = await restartGateway(context.projectRoot);
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
openclawRoot: context.openclawRoot,
|
|
27
|
+
configFile: configPath,
|
|
28
|
+
pluginId: payload.pluginId,
|
|
29
|
+
pairingEndpoint: endpoint,
|
|
30
|
+
updated: [
|
|
31
|
+
'plugins.allow',
|
|
32
|
+
`plugins.entries.${payload.pluginId}`,
|
|
33
|
+
`channels.${payload.pluginId}`
|
|
34
|
+
],
|
|
35
|
+
channel: payload.channel,
|
|
36
|
+
restart
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function loadConfig(configPath) {
|
|
40
|
+
if (!(await pathExists(configPath))) {
|
|
41
|
+
throwPairingError('PAIR_CONFIG_NOT_FOUND', `openclaw.json not found: ${configPath}`);
|
|
42
|
+
}
|
|
43
|
+
return await readJsonFile(configPath);
|
|
44
|
+
}
|
|
45
|
+
async function exchangePairKey(key, endpoint, authOverride, existingMqttUrl) {
|
|
46
|
+
const auth = pickString(authOverride);
|
|
47
|
+
const headers = {
|
|
48
|
+
'Content-Type': 'application/json'
|
|
49
|
+
};
|
|
50
|
+
if (auth) {
|
|
51
|
+
headers.Authorization = auth;
|
|
52
|
+
}
|
|
53
|
+
const response = await fetch(endpoint, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify({ key })
|
|
57
|
+
});
|
|
58
|
+
const rawText = await response.text();
|
|
59
|
+
const payload = tryParseJson(rawText);
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.internalError, `pair exchange failed: ${response.status}`, {
|
|
62
|
+
code: 'PAIR_EXCHANGE_FAILED',
|
|
63
|
+
endpoint,
|
|
64
|
+
status: response.status,
|
|
65
|
+
payload
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return normalizePairingPayload(payload, endpoint, existingMqttUrl);
|
|
69
|
+
}
|
|
70
|
+
function normalizePairingPayload(raw, endpoint, existingMqttUrl) {
|
|
71
|
+
const root = unwrapPayload(raw);
|
|
72
|
+
const pluginId = pickString(root.pluginId) ?? DEFAULT_PLUGIN_ID;
|
|
73
|
+
const apiCoreBotValue = isRecord(root.apiCoreBot) ? root.apiCoreBot : {};
|
|
74
|
+
const channelValue = isRecord(root.channel) ? root.channel : {};
|
|
75
|
+
const apiCoreBotAuthToken = pickString(apiCoreBotValue.authToken)
|
|
76
|
+
?? pickString(root.authToken)
|
|
77
|
+
?? pickString(root.token);
|
|
78
|
+
const rawValue = pickString(root.value);
|
|
79
|
+
const mqttUrl = pickString(channelValue.mqttUrl)
|
|
80
|
+
?? pickString(root.mqttUrl)
|
|
81
|
+
?? existingMqttUrl
|
|
82
|
+
?? null;
|
|
83
|
+
if (!mqttUrl) {
|
|
84
|
+
throwPairingError('PAIR_CHANNEL_CONFIG_INVALID', 'mqttUrl is missing from pairing payload');
|
|
85
|
+
}
|
|
86
|
+
const mqttTopic = pickString(channelValue.mqttTopic)
|
|
87
|
+
?? pickString(root.mqttTopic)
|
|
88
|
+
?? rawValue
|
|
89
|
+
?? 'announcement/tester';
|
|
90
|
+
const groupPolicy = normalizeGroupPolicy(pickString(channelValue.groupPolicy)
|
|
91
|
+
?? pickString(channelValue.dmPolicy)
|
|
92
|
+
?? pickString(root.groupPolicy)
|
|
93
|
+
?? pickString(root.dmPolicy)
|
|
94
|
+
?? 'pairing');
|
|
95
|
+
return {
|
|
96
|
+
pluginId,
|
|
97
|
+
pairing: {
|
|
98
|
+
...(rawValue ? { rawValue } : {})
|
|
99
|
+
},
|
|
100
|
+
apiCoreBot: {
|
|
101
|
+
baseUrl: pickString(apiCoreBotValue.baseUrl) ?? inferBaseUrl(endpoint),
|
|
102
|
+
...(apiCoreBotAuthToken ? { authToken: apiCoreBotAuthToken } : {})
|
|
103
|
+
},
|
|
104
|
+
channel: {
|
|
105
|
+
mqttUrl,
|
|
106
|
+
mqttTopic,
|
|
107
|
+
groupPolicy
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function applyPairingConfig(config, key, payload) {
|
|
112
|
+
if (!config.plugins)
|
|
113
|
+
config.plugins = {};
|
|
114
|
+
if (!Array.isArray(config.plugins.allow))
|
|
115
|
+
config.plugins.allow = [];
|
|
116
|
+
if (!config.plugins.allow.includes(payload.pluginId)) {
|
|
117
|
+
config.plugins.allow.push(payload.pluginId);
|
|
118
|
+
}
|
|
119
|
+
if (!config.plugins.entries || typeof config.plugins.entries !== 'object') {
|
|
120
|
+
config.plugins.entries = {};
|
|
121
|
+
}
|
|
122
|
+
const existingPluginEntry = isRecord(config.plugins.entries[payload.pluginId])
|
|
123
|
+
? config.plugins.entries[payload.pluginId]
|
|
124
|
+
: {};
|
|
125
|
+
const existingPluginConfig = isRecord(existingPluginEntry.config) ? existingPluginEntry.config : {};
|
|
126
|
+
config.plugins.entries[payload.pluginId] = {
|
|
127
|
+
...existingPluginEntry,
|
|
128
|
+
enabled: true,
|
|
129
|
+
config: {
|
|
130
|
+
...existingPluginConfig,
|
|
131
|
+
pairing: {
|
|
132
|
+
paired: true,
|
|
133
|
+
pairedAt: new Date().toISOString(),
|
|
134
|
+
pairingKeyLast4: key.slice(-4),
|
|
135
|
+
...(payload.pairing.rawValue ? { rawValue: payload.pairing.rawValue } : {})
|
|
136
|
+
},
|
|
137
|
+
apiCoreBot: {
|
|
138
|
+
...(isRecord(existingPluginConfig.apiCoreBot) ? existingPluginConfig.apiCoreBot : {}),
|
|
139
|
+
...(payload.apiCoreBot.authToken ? { authToken: payload.apiCoreBot.authToken } : {})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
if (!config.channels || typeof config.channels !== 'object') {
|
|
144
|
+
config.channels = {};
|
|
145
|
+
}
|
|
146
|
+
const existingChannelEntry = isRecord(config.channels[payload.pluginId])
|
|
147
|
+
? config.channels[payload.pluginId]
|
|
148
|
+
: {};
|
|
149
|
+
const existingChannelConfig = isRecord(existingChannelEntry.config) ? existingChannelEntry.config : {};
|
|
150
|
+
config.channels[payload.pluginId] = {
|
|
151
|
+
...existingChannelEntry,
|
|
152
|
+
enabled: true,
|
|
153
|
+
config: {
|
|
154
|
+
...existingChannelConfig,
|
|
155
|
+
enabled: true,
|
|
156
|
+
mqttUrl: payload.channel.mqttUrl,
|
|
157
|
+
mqttTopic: payload.channel.mqttTopic ?? existingChannelConfig.mqttTopic ?? 'announcement/tester',
|
|
158
|
+
groupPolicy: payload.channel.groupPolicy
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function unwrapPayload(raw) {
|
|
163
|
+
if (isRecord(raw)) {
|
|
164
|
+
if (isRecord(raw.data)) {
|
|
165
|
+
return raw.data;
|
|
166
|
+
}
|
|
167
|
+
return raw;
|
|
168
|
+
}
|
|
169
|
+
throwPairingError('PAIR_EXCHANGE_FAILED', 'unexpected pairing payload');
|
|
170
|
+
}
|
|
171
|
+
function tryParseJson(raw) {
|
|
172
|
+
const trimmed = raw.trim();
|
|
173
|
+
if (!trimmed) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(trimmed);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return raw;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function buildPairEndpoint(endpointOverride, config) {
|
|
184
|
+
const override = pickString(endpointOverride);
|
|
185
|
+
if (override) {
|
|
186
|
+
return normalizePairEndpoint(override);
|
|
187
|
+
}
|
|
188
|
+
const configured = resolveConfiguredPairingEndpoint(config);
|
|
189
|
+
return normalizePairEndpoint(configured ?? DEFAULT_PRODUCTION_BASE_URL);
|
|
190
|
+
}
|
|
191
|
+
function resolveConfiguredPairingEndpoint(config) {
|
|
192
|
+
const pluginEntry = isRecord(config.plugins?.entries?.[DEFAULT_PLUGIN_ID])
|
|
193
|
+
? config.plugins?.entries?.[DEFAULT_PLUGIN_ID]
|
|
194
|
+
: {};
|
|
195
|
+
const pluginConfig = isRecord(pluginEntry.config) ? pluginEntry.config : {};
|
|
196
|
+
const channelEntry = isRecord(config.channels?.[DEFAULT_PLUGIN_ID])
|
|
197
|
+
? config.channels?.[DEFAULT_PLUGIN_ID]
|
|
198
|
+
: {};
|
|
199
|
+
const channelConfig = isRecord(channelEntry.config) ? channelEntry.config : {};
|
|
200
|
+
return pickString(pluginConfig.pairingEndpoint)
|
|
201
|
+
?? pickString(channelEntry.pairingEndpoint)
|
|
202
|
+
?? pickString(channelConfig.pairingEndpoint);
|
|
203
|
+
}
|
|
204
|
+
function normalizePairEndpoint(value) {
|
|
205
|
+
const trimmed = value.trim().replace(/\/+$/, '');
|
|
206
|
+
try {
|
|
207
|
+
const parsed = new URL(trimmed);
|
|
208
|
+
if (!parsed.pathname || parsed.pathname === '/') {
|
|
209
|
+
return `${trimmed}${PAIR_ENDPOINT_PATH}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Keep non-URL values unchanged so the eventual fetch error reports the actual input.
|
|
214
|
+
}
|
|
215
|
+
return trimmed;
|
|
216
|
+
}
|
|
217
|
+
function inferBaseUrl(endpoint) {
|
|
218
|
+
try {
|
|
219
|
+
const parsed = new URL(endpoint);
|
|
220
|
+
return parsed.origin;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return DEFAULT_PRODUCTION_BASE_URL;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function resolveExistingMqttUrl(config) {
|
|
227
|
+
const channelConfig = config.channels?.[DEFAULT_PLUGIN_ID]?.config;
|
|
228
|
+
if (!channelConfig || typeof channelConfig !== 'object' || Array.isArray(channelConfig)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
return pickString(channelConfig.mqttUrl);
|
|
232
|
+
}
|
|
233
|
+
async function restartGateway(cwd) {
|
|
234
|
+
try {
|
|
235
|
+
await execFileAsync('systemctl', ['--user', 'restart', GATEWAY_SERVICE], { cwd });
|
|
236
|
+
return {
|
|
237
|
+
attempted: true,
|
|
238
|
+
success: true
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
attempted: true,
|
|
244
|
+
success: false,
|
|
245
|
+
message: error instanceof Error ? error.message : String(error),
|
|
246
|
+
stderr: typeof error?.stderr === 'string' ? error.stderr : ''
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function normalizeGroupPolicy(value) {
|
|
251
|
+
if (value === 'pairing' || value === 'allowlist' || value === 'open' || value === 'disabled') {
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
return 'pairing';
|
|
255
|
+
}
|
|
256
|
+
function pickString(value) {
|
|
257
|
+
if (typeof value !== 'string') {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
const trimmed = value.trim();
|
|
261
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
262
|
+
}
|
|
263
|
+
function isRecord(value) {
|
|
264
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
265
|
+
}
|
|
266
|
+
function throwPairingError(code, message) {
|
|
267
|
+
throw new JsonRpcException(JSON_RPC_ERRORS.invalidParams, message, { code });
|
|
268
|
+
}
|