rol-websocket-channel 1.5.5 → 1.5.6
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/dist/index.js +686 -686
- package/dist/message-handler.js +515 -515
- package/dist/src/admin/cli-manifest.test.js +85 -85
- 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 -736
- package/dist/src/admin/methods/artifacts.test.js +210 -210
- package/dist/src/admin/methods/cron.js +250 -250
- package/dist/src/admin/methods/index.js +104 -104
- package/dist/src/admin/methods/mem9.js +319 -319
- package/dist/src/admin/methods/mem9.test.js +34 -34
- 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 +315 -315
- package/dist/src/admin/methods/pairing.test.js +114 -114
- 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 -271
- 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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,686 +1,686 @@
|
|
|
1
|
-
// extensions/rol-websocket-channel/index.ts
|
|
2
|
-
// WebSocket Channel 插件实现
|
|
3
|
-
// 提供基于 WebSocket 的双向通信能力,支持 AI 回复和主动消息
|
|
4
|
-
import { messageHandler } from "./message-handler.js";
|
|
5
|
-
import { GlobalMqttClient } from "./src/mqtt/mqtt-client.js";
|
|
6
|
-
import * as ConnectionManager from "./src/mqtt/connection-manager.js";
|
|
7
|
-
// ============================================
|
|
8
|
-
// 3. 全局状态
|
|
9
|
-
// ============================================
|
|
10
|
-
import { getContext, initializeContext } from "./src/shared/context.js";
|
|
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
|
-
}
|
|
32
|
-
export function getPluginRuntime() {
|
|
33
|
-
return pluginRuntime;
|
|
34
|
-
}
|
|
35
|
-
export function formatCliErrorPayload(error) {
|
|
36
|
-
const data = error?.data;
|
|
37
|
-
const dataObject = data && typeof data === "object" && !Array.isArray(data)
|
|
38
|
-
? data
|
|
39
|
-
: {};
|
|
40
|
-
const code = dataObject.code ?? error?.code;
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
error: {
|
|
44
|
-
message: error instanceof Error ? error.message : String(error),
|
|
45
|
-
...dataObject,
|
|
46
|
-
...(code === undefined ? {} : { code }),
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
// ============================================
|
|
51
|
-
// 4. 插件主体定义
|
|
52
|
-
// ============================================
|
|
53
|
-
const WebSocketChannel = {
|
|
54
|
-
id: "rol-websocket-channel",
|
|
55
|
-
meta: {
|
|
56
|
-
id: "rol-websocket-channel",
|
|
57
|
-
label: "Websocket Channel",
|
|
58
|
-
selectionLabel: "Websocket Channel (Custom)",
|
|
59
|
-
docsPath: "/channels/rol-websocket-channel",
|
|
60
|
-
blurb: "WebSocket based messaging channel.",
|
|
61
|
-
aliases: ["ws"],
|
|
62
|
-
},
|
|
63
|
-
capabilities: {
|
|
64
|
-
chatTypes: ["direct", "group"],
|
|
65
|
-
media: {
|
|
66
|
-
maxSizeBytes: 10 * 1024 * 1024,
|
|
67
|
-
supportedTypes: ["image/jpeg", "image/png", "video/mp4"],
|
|
68
|
-
},
|
|
69
|
-
supports: {
|
|
70
|
-
threads: true,
|
|
71
|
-
reactions: false,
|
|
72
|
-
mentions: true,
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
configSchema: {
|
|
76
|
-
schema: {
|
|
77
|
-
type: "object",
|
|
78
|
-
additionalProperties: false,
|
|
79
|
-
properties: {
|
|
80
|
-
enabled: { type: "boolean" },
|
|
81
|
-
dmPolicy: {
|
|
82
|
-
type: "string",
|
|
83
|
-
enum: ["pairing", "allowlist", "open", "disabled"],
|
|
84
|
-
},
|
|
85
|
-
allowFrom: {
|
|
86
|
-
type: "array",
|
|
87
|
-
items: { type: "string" },
|
|
88
|
-
},
|
|
89
|
-
pairing: {
|
|
90
|
-
type: "object",
|
|
91
|
-
additionalProperties: false,
|
|
92
|
-
properties: {
|
|
93
|
-
paired: { type: "boolean" },
|
|
94
|
-
pairedAt: { type: "string" },
|
|
95
|
-
pairingKeyLast4: { type: "string" },
|
|
96
|
-
userId: { type: "string" },
|
|
97
|
-
rawValue: { type: "string" },
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
apiCoreBot: {
|
|
101
|
-
type: "object",
|
|
102
|
-
additionalProperties: false,
|
|
103
|
-
properties: {
|
|
104
|
-
baseUrl: { type: "string" },
|
|
105
|
-
authToken: { type: "string" },
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
pairingEndpoint: { type: "string" },
|
|
109
|
-
config: {
|
|
110
|
-
type: "object",
|
|
111
|
-
additionalProperties: false,
|
|
112
|
-
properties: {
|
|
113
|
-
enabled: { type: "boolean" },
|
|
114
|
-
pairingEndpoint: { type: "string" },
|
|
115
|
-
mqttUrl: { type: "string" },
|
|
116
|
-
mqttTopic: { type: "string" },
|
|
117
|
-
groupPolicy: {
|
|
118
|
-
type: "string",
|
|
119
|
-
enum: ["pairing", "allowlist", "open", "disabled"],
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
required: ["mqttUrl", "mqttTopic"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
uiHints: {
|
|
127
|
-
enabled: { label: "Enabled", description: "Enable MQTT Channel" },
|
|
128
|
-
dmPolicy: {
|
|
129
|
-
label: "DM Policy",
|
|
130
|
-
description: "Pairing/allowlist/open policy for direct messages",
|
|
131
|
-
},
|
|
132
|
-
allowFrom: {
|
|
133
|
-
label: "Allow From",
|
|
134
|
-
description: "Allowed sender IDs when using allowlist or pairing mode",
|
|
135
|
-
},
|
|
136
|
-
pairing: {
|
|
137
|
-
label: "Pairing",
|
|
138
|
-
description: "Pairing status and resolved identity info",
|
|
139
|
-
},
|
|
140
|
-
apiCoreBot: {
|
|
141
|
-
label: "API Core Bot",
|
|
142
|
-
description: "Backend API endpoint and auth token used by the plugin",
|
|
143
|
-
},
|
|
144
|
-
pairingEndpoint: {
|
|
145
|
-
label: "Pairing Endpoint",
|
|
146
|
-
description: "Optional pairing API endpoint or base URL for staging/local environments",
|
|
147
|
-
},
|
|
148
|
-
config: {
|
|
149
|
-
label: "Configuration",
|
|
150
|
-
description: "MQTT connection configuration",
|
|
151
|
-
},
|
|
152
|
-
"config.pairingEndpoint": {
|
|
153
|
-
label: "Pairing Endpoint",
|
|
154
|
-
description: "Optional pairing API endpoint or base URL for staging/local environments",
|
|
155
|
-
},
|
|
156
|
-
"config.enabled": {
|
|
157
|
-
label: "Enabled",
|
|
158
|
-
description: "Enable this configuration",
|
|
159
|
-
},
|
|
160
|
-
"config.mqttUrl": {
|
|
161
|
-
label: "MQTT Broker URL",
|
|
162
|
-
placeholder: "ws://mqtt.example.com:8083/mqtt",
|
|
163
|
-
help: "MQTT broker WebSocket URL",
|
|
164
|
-
},
|
|
165
|
-
"config.mqttTopic": {
|
|
166
|
-
label: "MQTT Topic",
|
|
167
|
-
placeholder: "announcement/{userId}/{agentId}/#",
|
|
168
|
-
help: "MQTT topic to subscribe/publish",
|
|
169
|
-
},
|
|
170
|
-
"config.groupPolicy": {
|
|
171
|
-
label: "Group Policy",
|
|
172
|
-
description: "Message policy for group chats",
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
config: {
|
|
177
|
-
listAccountIds: (cfg) => {
|
|
178
|
-
const channelCfg = getChannelConfig(cfg);
|
|
179
|
-
const mqttUrl = channelCfg
|
|
180
|
-
? pickNonEmptyString(channelCfg.nested.mqttUrl, channelCfg.entry.mqttUrl)
|
|
181
|
-
: undefined;
|
|
182
|
-
if (!mqttUrl) {
|
|
183
|
-
return [];
|
|
184
|
-
}
|
|
185
|
-
return ["default"];
|
|
186
|
-
},
|
|
187
|
-
resolveAccount: (cfg, accountId) => {
|
|
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) {
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
accountId: "default",
|
|
203
|
-
mqttUrl,
|
|
204
|
-
mqttTopic,
|
|
205
|
-
enabled: config.enabled !== false,
|
|
206
|
-
dmPolicy: channelCfg.entry.dmPolicy || config.groupPolicy || "open",
|
|
207
|
-
allowFrom: Array.isArray(channelCfg.entry.allowFrom)
|
|
208
|
-
? channelCfg.entry.allowFrom
|
|
209
|
-
: [],
|
|
210
|
-
groupPolicy: config.groupPolicy || "open",
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
isConfigured: async (account, cfg) => {
|
|
214
|
-
return Boolean(account.mqttUrl && account.mqttUrl.trim() !== "");
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
status: {
|
|
218
|
-
defaultRuntime: {
|
|
219
|
-
accountId: "default",
|
|
220
|
-
running: false,
|
|
221
|
-
connected: false,
|
|
222
|
-
mqttUrl: null,
|
|
223
|
-
mqttTopic: null,
|
|
224
|
-
groupPolicy: null,
|
|
225
|
-
lastStartAt: null,
|
|
226
|
-
lastStopAt: null,
|
|
227
|
-
lastError: null,
|
|
228
|
-
},
|
|
229
|
-
buildChannelSummary: ({ snapshot }) => ({
|
|
230
|
-
mqttUrl: snapshot.mqttUrl ?? null,
|
|
231
|
-
mqttTopic: snapshot.mqttTopic ?? null,
|
|
232
|
-
connected: snapshot.connected ?? null,
|
|
233
|
-
groupPolicy: snapshot.groupPolicy ?? null,
|
|
234
|
-
}),
|
|
235
|
-
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
236
|
-
accountId: account.accountId,
|
|
237
|
-
enabled: account.enabled,
|
|
238
|
-
configured: account.configured,
|
|
239
|
-
mqttUrl: account.mqttUrl,
|
|
240
|
-
mqttTopic: account.mqttTopic,
|
|
241
|
-
running: runtime?.running ?? false,
|
|
242
|
-
connected: runtime?.connected ?? false,
|
|
243
|
-
groupPolicy: runtime?.groupPolicy ?? null,
|
|
244
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
245
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
246
|
-
lastError: runtime?.lastError ?? null,
|
|
247
|
-
}),
|
|
248
|
-
},
|
|
249
|
-
outbound: {
|
|
250
|
-
deliveryMode: "direct",
|
|
251
|
-
sendText: async ({ to, text }) => {
|
|
252
|
-
const conn = ConnectionManager.getGlobalConnection();
|
|
253
|
-
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
254
|
-
return { ok: false, error: "No MQTT connection" };
|
|
255
|
-
}
|
|
256
|
-
const message = JSON.stringify({
|
|
257
|
-
type: "message",
|
|
258
|
-
to,
|
|
259
|
-
content: text,
|
|
260
|
-
timestamp: Date.now(),
|
|
261
|
-
});
|
|
262
|
-
conn.ws.publish(conn.topic, message);
|
|
263
|
-
return { ok: true };
|
|
264
|
-
},
|
|
265
|
-
sendMedia: async ({ to, text, mediaUrl }) => {
|
|
266
|
-
const conn = ConnectionManager.getGlobalConnection();
|
|
267
|
-
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
268
|
-
return { ok: false, error: "No MQTT connection" };
|
|
269
|
-
}
|
|
270
|
-
const message = JSON.stringify({
|
|
271
|
-
type: "media",
|
|
272
|
-
to,
|
|
273
|
-
content: text,
|
|
274
|
-
mediaUrl,
|
|
275
|
-
timestamp: Date.now(),
|
|
276
|
-
});
|
|
277
|
-
conn.ws.publish(conn.topic, message);
|
|
278
|
-
return { ok: true };
|
|
279
|
-
},
|
|
280
|
-
},
|
|
281
|
-
gateway: {
|
|
282
|
-
startAccount: async (ctx) => {
|
|
283
|
-
const { log, account, abortSignal, cfg } = ctx;
|
|
284
|
-
const runtime = pluginRuntime;
|
|
285
|
-
console.log(`[MQTT] startAccount(${account.accountId}): starting...`);
|
|
286
|
-
log?.info(`[rol-websocket-channel] Starting MQTT Channel for ${account.accountId}`);
|
|
287
|
-
// 检查是否已有活跃连接,防止重复启动
|
|
288
|
-
if (ConnectionManager.isGlobalConnected()) {
|
|
289
|
-
console.log(`[MQTT] startAccount(${account.accountId}): already connected, skipping`);
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (!runtime?.channel?.reply?.withReplyDispatcher) {
|
|
293
|
-
console.error(`[MQTT] startAccount(${account.accountId}): Runtime API not available`);
|
|
294
|
-
throw new Error("Runtime API not available");
|
|
295
|
-
}
|
|
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
|
-
}
|
|
321
|
-
console.log(`[MQTT] startAccount(${account.accountId}): url=${mqttUrl}, topic=${mqttTopic}`);
|
|
322
|
-
ctx.setStatus({
|
|
323
|
-
accountId: account.accountId,
|
|
324
|
-
mqttUrl,
|
|
325
|
-
mqttTopic,
|
|
326
|
-
running: true,
|
|
327
|
-
connected: false,
|
|
328
|
-
groupPolicy: account.groupPolicy || "open",
|
|
329
|
-
});
|
|
330
|
-
// 创建 MQTT 客户端
|
|
331
|
-
console.log(`[MQTT] startAccount(${account.accountId}): creating GlobalMqttClient...`);
|
|
332
|
-
const client = new GlobalMqttClient({
|
|
333
|
-
mqttUrl,
|
|
334
|
-
mqttTopic,
|
|
335
|
-
abortSignal,
|
|
336
|
-
onConnect: () => {
|
|
337
|
-
console.log(`[MQTT] startAccount(${account.accountId}): onConnect - setting status to connected`);
|
|
338
|
-
ctx.setStatus({
|
|
339
|
-
accountId: account.accountId,
|
|
340
|
-
connected: true,
|
|
341
|
-
});
|
|
342
|
-
},
|
|
343
|
-
onDisconnect: () => {
|
|
344
|
-
console.log(`[MQTT] startAccount(${account.accountId}): onDisconnect - setting status to disconnected`);
|
|
345
|
-
ctx.setStatus({
|
|
346
|
-
accountId: account.accountId,
|
|
347
|
-
connected: false,
|
|
348
|
-
});
|
|
349
|
-
},
|
|
350
|
-
onError: (err) => {
|
|
351
|
-
console.error(`[MQTT] startAccount(${account.accountId}): onError - ${err.message}`);
|
|
352
|
-
log?.error(`[rol-websocket-channel] MQTT error: ${err.message}`);
|
|
353
|
-
},
|
|
354
|
-
onMessage: async (topic, payload) => {
|
|
355
|
-
console.log(`[MQTT] startAccount(${account.accountId}): onMessage received`);
|
|
356
|
-
await handleIncomingMessage(payload, account, cfg, runtime, log, mqttTopic);
|
|
357
|
-
},
|
|
358
|
-
});
|
|
359
|
-
// 启动连接
|
|
360
|
-
console.log(`[MQTT] startAccount(${account.accountId}): calling GlobalMqttClient.connect()...`);
|
|
361
|
-
await client.connect();
|
|
362
|
-
console.log(`[MQTT] startAccount(${account.accountId}): GlobalMqttClient.connect() returned`);
|
|
363
|
-
},
|
|
364
|
-
stopAccount: async (ctx) => {
|
|
365
|
-
const { log, account } = ctx;
|
|
366
|
-
console.log(`[MQTT] stopAccount(${account.accountId}): stopping...`);
|
|
367
|
-
log?.info(`[rol-websocket-channel] Stopping MQTT Channel for ${account.accountId}`);
|
|
368
|
-
ConnectionManager.closeGlobalConnection();
|
|
369
|
-
console.log(`[MQTT] stopAccount(${account.accountId}): stopped`);
|
|
370
|
-
},
|
|
371
|
-
},
|
|
372
|
-
security: {
|
|
373
|
-
getDmPolicy: (account) => account.dmPolicy ?? "open",
|
|
374
|
-
getAllowFrom: (account) => account.allowFrom ?? [],
|
|
375
|
-
checkGroupAccess: (account, groupId) => {
|
|
376
|
-
const groups = account.groups ?? {};
|
|
377
|
-
return "*" in groups || groupId in groups;
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
// ============================================
|
|
382
|
-
// 5. 消息处理函数
|
|
383
|
-
// ============================================
|
|
384
|
-
async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTopic) {
|
|
385
|
-
try {
|
|
386
|
-
const rawData = payload.toString();
|
|
387
|
-
const eventData = JSON.parse(rawData);
|
|
388
|
-
const msgType = eventData.type || "sender";
|
|
389
|
-
const innerData = eventData.data || {};
|
|
390
|
-
const traceId = eventData.trace_id || eventData.traceId || `${Date.now()}`;
|
|
391
|
-
// 忽略 receiver 类型的消息
|
|
392
|
-
if (msgType === "receiver") {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// 处理非标准消息类型
|
|
396
|
-
if (msgType !== "sender") {
|
|
397
|
-
await handleCustomMessageType(msgType, innerData, traceId, account.accountId, mqttTopic);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
// 构造标准消息格式
|
|
401
|
-
const attachments = (innerData.attachments || []).map((att) => ({
|
|
402
|
-
url: att.url,
|
|
403
|
-
type: att.type || "application/octet-stream",
|
|
404
|
-
name: att.name || "file",
|
|
405
|
-
size: att.size,
|
|
406
|
-
}));
|
|
407
|
-
const normalizedMessage = {
|
|
408
|
-
id: `${eventData.source || "mqtt"}-${Date.now()}`,
|
|
409
|
-
channel: "rol-websocket-channel",
|
|
410
|
-
accountId: account.accountId,
|
|
411
|
-
senderId: innerData.source || eventData.source || "unknown",
|
|
412
|
-
senderName: innerData.source || eventData.source || "Unknown",
|
|
413
|
-
text: innerData.content || innerData.text || "",
|
|
414
|
-
timestamp: innerData.timestamp || new Date().toISOString(),
|
|
415
|
-
isGroup: false,
|
|
416
|
-
groupId: undefined,
|
|
417
|
-
attachments,
|
|
418
|
-
metadata: { mqttTopic, traceId },
|
|
419
|
-
};
|
|
420
|
-
const targetAgentId = innerData.agentId ?? innerData.agent_id ?? null;
|
|
421
|
-
const targetSessionId = innerData.sessionKey ?? innerData.session_key ?? null;
|
|
422
|
-
log?.info(`[rol-websocket-channel] 📨 Received: "${normalizedMessage.text}" from ${normalizedMessage.senderId}` +
|
|
423
|
-
(targetAgentId ? ` → agent:${targetAgentId}` : "") +
|
|
424
|
-
(targetSessionId ? ` → session:${targetSessionId}` : ""));
|
|
425
|
-
// 解析路由
|
|
426
|
-
const route = runtime.channel.routing.resolveAgentRoute({
|
|
427
|
-
cfg,
|
|
428
|
-
channel: "rol-websocket-channel",
|
|
429
|
-
accountId: account.accountId,
|
|
430
|
-
peer: { kind: "direct", id: normalizedMessage.senderId },
|
|
431
|
-
});
|
|
432
|
-
// 用户传参覆盖自动路由结果
|
|
433
|
-
const resolvedAccountId = targetAgentId ?? route.accountId;
|
|
434
|
-
const resolvedSessionKey = targetSessionId ?? route.sessionKey;
|
|
435
|
-
// 构建消息上下文
|
|
436
|
-
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
|
437
|
-
Body: normalizedMessage.text,
|
|
438
|
-
BodyForAgent: normalizedMessage.text,
|
|
439
|
-
From: normalizedMessage.senderId,
|
|
440
|
-
To: undefined,
|
|
441
|
-
SessionKey: resolvedSessionKey,
|
|
442
|
-
AccountId: resolvedAccountId,
|
|
443
|
-
ChatType: "direct",
|
|
444
|
-
SenderName: normalizedMessage.senderName,
|
|
445
|
-
SenderId: normalizedMessage.senderId,
|
|
446
|
-
Provider: "rol-websocket-channel",
|
|
447
|
-
Surface: "rol-websocket-channel",
|
|
448
|
-
MessageSid: normalizedMessage.id,
|
|
449
|
-
Timestamp: Date.now(),
|
|
450
|
-
});
|
|
451
|
-
// 调度回复
|
|
452
|
-
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
453
|
-
ctx: ctxPayload,
|
|
454
|
-
cfg,
|
|
455
|
-
dispatcherOptions: {
|
|
456
|
-
deliver: async (payload) => {
|
|
457
|
-
const conn = ConnectionManager.getGlobalConnection();
|
|
458
|
-
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
459
|
-
throw new Error("No MQTT connection available");
|
|
460
|
-
}
|
|
461
|
-
const replyMessage = {
|
|
462
|
-
type: "receiver",
|
|
463
|
-
trace_id: traceId,
|
|
464
|
-
source: "ai",
|
|
465
|
-
meta: {
|
|
466
|
-
'agentId': resolvedAccountId,
|
|
467
|
-
'sessionKey': resolvedSessionKey
|
|
468
|
-
},
|
|
469
|
-
data: payload,
|
|
470
|
-
timestamp: Date.now(),
|
|
471
|
-
};
|
|
472
|
-
// 根据 source_type 修改 topic 末尾的 #
|
|
473
|
-
let targetTopic = mqttTopic;
|
|
474
|
-
const sourceType = innerData?.source_type;
|
|
475
|
-
if (targetTopic.endsWith("#")) {
|
|
476
|
-
const replacement = sourceType === "device" ? "device" : "bot";
|
|
477
|
-
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
478
|
-
}
|
|
479
|
-
conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
|
|
480
|
-
},
|
|
481
|
-
onError: (err) => {
|
|
482
|
-
log?.error(`[rol-websocket-channel] Delivery error: ${err.message}`);
|
|
483
|
-
},
|
|
484
|
-
},
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
catch (err) {
|
|
488
|
-
log?.error(`[rol-websocket-channel] Failed to process message: ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
async function handleCustomMessageType(msgType, innerData, traceId, accountId, mqttTopic) {
|
|
492
|
-
const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
|
|
493
|
-
const response = {
|
|
494
|
-
type: "receiver",
|
|
495
|
-
trace_id: traceId,
|
|
496
|
-
source: "system",
|
|
497
|
-
timestamp: Date.now(),
|
|
498
|
-
};
|
|
499
|
-
if (isSkillInstallFlow) {
|
|
500
|
-
console.log(`[rol-websocket-channel] custom message start: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, accountId=${accountId}, topic=${mqttTopic}`);
|
|
501
|
-
}
|
|
502
|
-
const handlerMethod = messageHandler[msgType];
|
|
503
|
-
if (typeof handlerMethod === "function") {
|
|
504
|
-
try {
|
|
505
|
-
const methodResult = await handlerMethod.call(messageHandler, innerData);
|
|
506
|
-
// 兼容两种返回格式:
|
|
507
|
-
// 1. { ok, result, error } 格式(新的 admin 方法)
|
|
508
|
-
// 2. 直接返回数据格式(原有的 ping、echo、status 方法)
|
|
509
|
-
if (typeof methodResult === "object" &&
|
|
510
|
-
methodResult !== null &&
|
|
511
|
-
"ok" in methodResult) {
|
|
512
|
-
// 新格式:{ ok, result, error }
|
|
513
|
-
response.success = methodResult.ok;
|
|
514
|
-
response.data = methodResult.result;
|
|
515
|
-
if (!methodResult.ok) {
|
|
516
|
-
response.error = methodResult.error?.message || "Unknown error";
|
|
517
|
-
if (isSkillInstallFlow) {
|
|
518
|
-
console.error(`[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else if (isSkillInstallFlow) {
|
|
522
|
-
console.log(`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
else {
|
|
526
|
-
// 旧格式:直接返回数据
|
|
527
|
-
response.success = true;
|
|
528
|
-
response.data = methodResult;
|
|
529
|
-
if (isSkillInstallFlow) {
|
|
530
|
-
console.log(`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
catch (handlerErr) {
|
|
535
|
-
response.success = false;
|
|
536
|
-
response.error = handlerErr.message;
|
|
537
|
-
if (isSkillInstallFlow) {
|
|
538
|
-
console.error(`[rol-websocket-channel] custom message threw: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${handlerErr?.message ?? String(handlerErr)}`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
else {
|
|
543
|
-
response.success = false;
|
|
544
|
-
response.error = `Unknown message type: ${msgType}`;
|
|
545
|
-
}
|
|
546
|
-
const conn = ConnectionManager.getGlobalConnection();
|
|
547
|
-
if (conn && conn.ws && conn.ws.connected) {
|
|
548
|
-
// 根据 source_type 修改 topic 末尾的 #
|
|
549
|
-
let targetTopic = mqttTopic;
|
|
550
|
-
const sourceType = innerData?.source_type;
|
|
551
|
-
if (targetTopic.endsWith("#")) {
|
|
552
|
-
const replacement = sourceType === "device" ? "device" : "bot";
|
|
553
|
-
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
554
|
-
}
|
|
555
|
-
conn.ws.publish(targetTopic, JSON.stringify(response));
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
// ============================================
|
|
559
|
-
// 6. 插件注册入口
|
|
560
|
-
// ============================================
|
|
561
|
-
let isPluginRegistered = false;
|
|
562
|
-
export default function register(api) {
|
|
563
|
-
console.log("[mqtt] Register rol-websocket-channel");
|
|
564
|
-
if (isPluginRegistered) {
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
console.log("[mqtt] Register rol-websocket-channel real");
|
|
568
|
-
isPluginRegistered = true;
|
|
569
|
-
pluginRuntime = api.runtime;
|
|
570
|
-
// 初始化共享 context
|
|
571
|
-
initializeContext();
|
|
572
|
-
// 注册 Channel
|
|
573
|
-
api.registerChannel({ plugin: WebSocketChannel });
|
|
574
|
-
// 注册 CLI(保留供外部调用)
|
|
575
|
-
registerAdminBridgeCli(api);
|
|
576
|
-
}
|
|
577
|
-
// ============================================
|
|
578
|
-
// 7. CLI 注册(保留供外部调用)
|
|
579
|
-
// ============================================
|
|
580
|
-
function registerAdminBridgeCli(api) {
|
|
581
|
-
api.registerCli(({ program }) => {
|
|
582
|
-
const root = program
|
|
583
|
-
.command("admin-bridge")
|
|
584
|
-
.alias("rol-websocket-channel")
|
|
585
|
-
.description("OpenClaw admin bridge utilities")
|
|
586
|
-
.addHelpText("after", () => "\nCommands return JSON to stdout for Python or shell orchestration.\n");
|
|
587
|
-
// 这里可以添加 CLI 命令,但现在先保持简单
|
|
588
|
-
// 如果需要完整的 CLI,可以从 openclaw-ts/index.ts 复制过来
|
|
589
|
-
root
|
|
590
|
-
.command("ping")
|
|
591
|
-
.description("Test admin bridge connection")
|
|
592
|
-
.action(async () => {
|
|
593
|
-
try {
|
|
594
|
-
const { ping } = await import("./src/admin/methods/system.js");
|
|
595
|
-
const { getContext } = await import("./src/shared/context.js");
|
|
596
|
-
const result = await ping(undefined, getContext());
|
|
597
|
-
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
598
|
-
}
|
|
599
|
-
catch (error) {
|
|
600
|
-
process.exitCode = 1;
|
|
601
|
-
process.stderr.write(JSON.stringify({
|
|
602
|
-
ok: false,
|
|
603
|
-
error: {
|
|
604
|
-
message: error instanceof Error ? error.message : String(error),
|
|
605
|
-
},
|
|
606
|
-
}, null, 2) + "\n");
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
root
|
|
610
|
-
.command("pair <key>")
|
|
611
|
-
.description("Pair rol-websocket-channel and write OpenClaw configuration")
|
|
612
|
-
.option("--endpoint <url>", "Pair exchange endpoint")
|
|
613
|
-
.option("--auth <token>", "Authorization header value for pair exchange")
|
|
614
|
-
.action(async (key, options) => {
|
|
615
|
-
try {
|
|
616
|
-
const { pairWithKey } = await import("./src/admin/methods/pairing.js");
|
|
617
|
-
const result = await pairWithKey({
|
|
618
|
-
key,
|
|
619
|
-
endpoint: options.endpoint,
|
|
620
|
-
auth: options.auth,
|
|
621
|
-
}, getContext());
|
|
622
|
-
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
623
|
-
}
|
|
624
|
-
catch (error) {
|
|
625
|
-
process.exitCode = 1;
|
|
626
|
-
process.stderr.write(JSON.stringify(formatCliErrorPayload(error), null, 2) + "\n");
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
const mem9 = root
|
|
630
|
-
.command("mem9")
|
|
631
|
-
.description("Mem9 installer and reconnect utilities");
|
|
632
|
-
mem9
|
|
633
|
-
.command("install")
|
|
634
|
-
.description("Install mem9 plugin, create cloud key, write config, and restart gateway")
|
|
635
|
-
.action(async () => {
|
|
636
|
-
try {
|
|
637
|
-
const { installMem9 } = await import("./src/admin/methods/mem9.js");
|
|
638
|
-
const result = await installMem9(getContext());
|
|
639
|
-
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
640
|
-
}
|
|
641
|
-
catch (error) {
|
|
642
|
-
process.exitCode = 1;
|
|
643
|
-
process.stderr.write(JSON.stringify({
|
|
644
|
-
ok: false,
|
|
645
|
-
error: {
|
|
646
|
-
message: error instanceof Error ? error.message : String(error),
|
|
647
|
-
code: error?.data?.code,
|
|
648
|
-
},
|
|
649
|
-
}, null, 2) + "\n");
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
mem9
|
|
653
|
-
.command("reconnect <key>")
|
|
654
|
-
.description("Replace mem9 apiKey, update config, and restart gateway")
|
|
655
|
-
.action(async (key) => {
|
|
656
|
-
try {
|
|
657
|
-
const { reconnectMem9 } = await import("./src/admin/methods/mem9.js");
|
|
658
|
-
const result = await reconnectMem9(key, getContext());
|
|
659
|
-
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
660
|
-
}
|
|
661
|
-
catch (error) {
|
|
662
|
-
process.exitCode = 1;
|
|
663
|
-
process.stderr.write(JSON.stringify({
|
|
664
|
-
ok: false,
|
|
665
|
-
error: {
|
|
666
|
-
message: error instanceof Error ? error.message : String(error),
|
|
667
|
-
code: error?.data?.code,
|
|
668
|
-
},
|
|
669
|
-
}, null, 2) + "\n");
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
}, {
|
|
673
|
-
descriptors: [
|
|
674
|
-
{
|
|
675
|
-
name: "admin-bridge",
|
|
676
|
-
description: "OpenClaw admin bridge commands",
|
|
677
|
-
hasSubcommands: true,
|
|
678
|
-
},
|
|
679
|
-
{
|
|
680
|
-
name: "rol-websocket-channel",
|
|
681
|
-
description: "OpenClaw admin bridge commands",
|
|
682
|
-
hasSubcommands: true,
|
|
683
|
-
},
|
|
684
|
-
],
|
|
685
|
-
});
|
|
686
|
-
}
|
|
1
|
+
// extensions/rol-websocket-channel/index.ts
|
|
2
|
+
// WebSocket Channel 插件实现
|
|
3
|
+
// 提供基于 WebSocket 的双向通信能力,支持 AI 回复和主动消息
|
|
4
|
+
import { messageHandler } from "./message-handler.js";
|
|
5
|
+
import { GlobalMqttClient } from "./src/mqtt/mqtt-client.js";
|
|
6
|
+
import * as ConnectionManager from "./src/mqtt/connection-manager.js";
|
|
7
|
+
// ============================================
|
|
8
|
+
// 3. 全局状态
|
|
9
|
+
// ============================================
|
|
10
|
+
import { getContext, initializeContext } from "./src/shared/context.js";
|
|
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
|
+
}
|
|
32
|
+
export function getPluginRuntime() {
|
|
33
|
+
return pluginRuntime;
|
|
34
|
+
}
|
|
35
|
+
export function formatCliErrorPayload(error) {
|
|
36
|
+
const data = error?.data;
|
|
37
|
+
const dataObject = data && typeof data === "object" && !Array.isArray(data)
|
|
38
|
+
? data
|
|
39
|
+
: {};
|
|
40
|
+
const code = dataObject.code ?? error?.code;
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: {
|
|
44
|
+
message: error instanceof Error ? error.message : String(error),
|
|
45
|
+
...dataObject,
|
|
46
|
+
...(code === undefined ? {} : { code }),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// ============================================
|
|
51
|
+
// 4. 插件主体定义
|
|
52
|
+
// ============================================
|
|
53
|
+
const WebSocketChannel = {
|
|
54
|
+
id: "rol-websocket-channel",
|
|
55
|
+
meta: {
|
|
56
|
+
id: "rol-websocket-channel",
|
|
57
|
+
label: "Websocket Channel",
|
|
58
|
+
selectionLabel: "Websocket Channel (Custom)",
|
|
59
|
+
docsPath: "/channels/rol-websocket-channel",
|
|
60
|
+
blurb: "WebSocket based messaging channel.",
|
|
61
|
+
aliases: ["ws"],
|
|
62
|
+
},
|
|
63
|
+
capabilities: {
|
|
64
|
+
chatTypes: ["direct", "group"],
|
|
65
|
+
media: {
|
|
66
|
+
maxSizeBytes: 10 * 1024 * 1024,
|
|
67
|
+
supportedTypes: ["image/jpeg", "image/png", "video/mp4"],
|
|
68
|
+
},
|
|
69
|
+
supports: {
|
|
70
|
+
threads: true,
|
|
71
|
+
reactions: false,
|
|
72
|
+
mentions: true,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
configSchema: {
|
|
76
|
+
schema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
additionalProperties: false,
|
|
79
|
+
properties: {
|
|
80
|
+
enabled: { type: "boolean" },
|
|
81
|
+
dmPolicy: {
|
|
82
|
+
type: "string",
|
|
83
|
+
enum: ["pairing", "allowlist", "open", "disabled"],
|
|
84
|
+
},
|
|
85
|
+
allowFrom: {
|
|
86
|
+
type: "array",
|
|
87
|
+
items: { type: "string" },
|
|
88
|
+
},
|
|
89
|
+
pairing: {
|
|
90
|
+
type: "object",
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
properties: {
|
|
93
|
+
paired: { type: "boolean" },
|
|
94
|
+
pairedAt: { type: "string" },
|
|
95
|
+
pairingKeyLast4: { type: "string" },
|
|
96
|
+
userId: { type: "string" },
|
|
97
|
+
rawValue: { type: "string" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
apiCoreBot: {
|
|
101
|
+
type: "object",
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
properties: {
|
|
104
|
+
baseUrl: { type: "string" },
|
|
105
|
+
authToken: { type: "string" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
pairingEndpoint: { type: "string" },
|
|
109
|
+
config: {
|
|
110
|
+
type: "object",
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
enabled: { type: "boolean" },
|
|
114
|
+
pairingEndpoint: { type: "string" },
|
|
115
|
+
mqttUrl: { type: "string" },
|
|
116
|
+
mqttTopic: { type: "string" },
|
|
117
|
+
groupPolicy: {
|
|
118
|
+
type: "string",
|
|
119
|
+
enum: ["pairing", "allowlist", "open", "disabled"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["mqttUrl", "mqttTopic"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
uiHints: {
|
|
127
|
+
enabled: { label: "Enabled", description: "Enable MQTT Channel" },
|
|
128
|
+
dmPolicy: {
|
|
129
|
+
label: "DM Policy",
|
|
130
|
+
description: "Pairing/allowlist/open policy for direct messages",
|
|
131
|
+
},
|
|
132
|
+
allowFrom: {
|
|
133
|
+
label: "Allow From",
|
|
134
|
+
description: "Allowed sender IDs when using allowlist or pairing mode",
|
|
135
|
+
},
|
|
136
|
+
pairing: {
|
|
137
|
+
label: "Pairing",
|
|
138
|
+
description: "Pairing status and resolved identity info",
|
|
139
|
+
},
|
|
140
|
+
apiCoreBot: {
|
|
141
|
+
label: "API Core Bot",
|
|
142
|
+
description: "Backend API endpoint and auth token used by the plugin",
|
|
143
|
+
},
|
|
144
|
+
pairingEndpoint: {
|
|
145
|
+
label: "Pairing Endpoint",
|
|
146
|
+
description: "Optional pairing API endpoint or base URL for staging/local environments",
|
|
147
|
+
},
|
|
148
|
+
config: {
|
|
149
|
+
label: "Configuration",
|
|
150
|
+
description: "MQTT connection configuration",
|
|
151
|
+
},
|
|
152
|
+
"config.pairingEndpoint": {
|
|
153
|
+
label: "Pairing Endpoint",
|
|
154
|
+
description: "Optional pairing API endpoint or base URL for staging/local environments",
|
|
155
|
+
},
|
|
156
|
+
"config.enabled": {
|
|
157
|
+
label: "Enabled",
|
|
158
|
+
description: "Enable this configuration",
|
|
159
|
+
},
|
|
160
|
+
"config.mqttUrl": {
|
|
161
|
+
label: "MQTT Broker URL",
|
|
162
|
+
placeholder: "ws://mqtt.example.com:8083/mqtt",
|
|
163
|
+
help: "MQTT broker WebSocket URL",
|
|
164
|
+
},
|
|
165
|
+
"config.mqttTopic": {
|
|
166
|
+
label: "MQTT Topic",
|
|
167
|
+
placeholder: "announcement/{userId}/{agentId}/#",
|
|
168
|
+
help: "MQTT topic to subscribe/publish",
|
|
169
|
+
},
|
|
170
|
+
"config.groupPolicy": {
|
|
171
|
+
label: "Group Policy",
|
|
172
|
+
description: "Message policy for group chats",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
config: {
|
|
177
|
+
listAccountIds: (cfg) => {
|
|
178
|
+
const channelCfg = getChannelConfig(cfg);
|
|
179
|
+
const mqttUrl = channelCfg
|
|
180
|
+
? pickNonEmptyString(channelCfg.nested.mqttUrl, channelCfg.entry.mqttUrl)
|
|
181
|
+
: undefined;
|
|
182
|
+
if (!mqttUrl) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
return ["default"];
|
|
186
|
+
},
|
|
187
|
+
resolveAccount: (cfg, accountId) => {
|
|
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) {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
accountId: "default",
|
|
203
|
+
mqttUrl,
|
|
204
|
+
mqttTopic,
|
|
205
|
+
enabled: config.enabled !== false,
|
|
206
|
+
dmPolicy: channelCfg.entry.dmPolicy || config.groupPolicy || "open",
|
|
207
|
+
allowFrom: Array.isArray(channelCfg.entry.allowFrom)
|
|
208
|
+
? channelCfg.entry.allowFrom
|
|
209
|
+
: [],
|
|
210
|
+
groupPolicy: config.groupPolicy || "open",
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
isConfigured: async (account, cfg) => {
|
|
214
|
+
return Boolean(account.mqttUrl && account.mqttUrl.trim() !== "");
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
status: {
|
|
218
|
+
defaultRuntime: {
|
|
219
|
+
accountId: "default",
|
|
220
|
+
running: false,
|
|
221
|
+
connected: false,
|
|
222
|
+
mqttUrl: null,
|
|
223
|
+
mqttTopic: null,
|
|
224
|
+
groupPolicy: null,
|
|
225
|
+
lastStartAt: null,
|
|
226
|
+
lastStopAt: null,
|
|
227
|
+
lastError: null,
|
|
228
|
+
},
|
|
229
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
230
|
+
mqttUrl: snapshot.mqttUrl ?? null,
|
|
231
|
+
mqttTopic: snapshot.mqttTopic ?? null,
|
|
232
|
+
connected: snapshot.connected ?? null,
|
|
233
|
+
groupPolicy: snapshot.groupPolicy ?? null,
|
|
234
|
+
}),
|
|
235
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
236
|
+
accountId: account.accountId,
|
|
237
|
+
enabled: account.enabled,
|
|
238
|
+
configured: account.configured,
|
|
239
|
+
mqttUrl: account.mqttUrl,
|
|
240
|
+
mqttTopic: account.mqttTopic,
|
|
241
|
+
running: runtime?.running ?? false,
|
|
242
|
+
connected: runtime?.connected ?? false,
|
|
243
|
+
groupPolicy: runtime?.groupPolicy ?? null,
|
|
244
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
245
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
246
|
+
lastError: runtime?.lastError ?? null,
|
|
247
|
+
}),
|
|
248
|
+
},
|
|
249
|
+
outbound: {
|
|
250
|
+
deliveryMode: "direct",
|
|
251
|
+
sendText: async ({ to, text }) => {
|
|
252
|
+
const conn = ConnectionManager.getGlobalConnection();
|
|
253
|
+
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
254
|
+
return { ok: false, error: "No MQTT connection" };
|
|
255
|
+
}
|
|
256
|
+
const message = JSON.stringify({
|
|
257
|
+
type: "message",
|
|
258
|
+
to,
|
|
259
|
+
content: text,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
});
|
|
262
|
+
conn.ws.publish(conn.topic, message);
|
|
263
|
+
return { ok: true };
|
|
264
|
+
},
|
|
265
|
+
sendMedia: async ({ to, text, mediaUrl }) => {
|
|
266
|
+
const conn = ConnectionManager.getGlobalConnection();
|
|
267
|
+
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
268
|
+
return { ok: false, error: "No MQTT connection" };
|
|
269
|
+
}
|
|
270
|
+
const message = JSON.stringify({
|
|
271
|
+
type: "media",
|
|
272
|
+
to,
|
|
273
|
+
content: text,
|
|
274
|
+
mediaUrl,
|
|
275
|
+
timestamp: Date.now(),
|
|
276
|
+
});
|
|
277
|
+
conn.ws.publish(conn.topic, message);
|
|
278
|
+
return { ok: true };
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
gateway: {
|
|
282
|
+
startAccount: async (ctx) => {
|
|
283
|
+
const { log, account, abortSignal, cfg } = ctx;
|
|
284
|
+
const runtime = pluginRuntime;
|
|
285
|
+
console.log(`[MQTT] startAccount(${account.accountId}): starting...`);
|
|
286
|
+
log?.info(`[rol-websocket-channel] Starting MQTT Channel for ${account.accountId}`);
|
|
287
|
+
// 检查是否已有活跃连接,防止重复启动
|
|
288
|
+
if (ConnectionManager.isGlobalConnected()) {
|
|
289
|
+
console.log(`[MQTT] startAccount(${account.accountId}): already connected, skipping`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!runtime?.channel?.reply?.withReplyDispatcher) {
|
|
293
|
+
console.error(`[MQTT] startAccount(${account.accountId}): Runtime API not available`);
|
|
294
|
+
throw new Error("Runtime API not available");
|
|
295
|
+
}
|
|
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
|
+
}
|
|
321
|
+
console.log(`[MQTT] startAccount(${account.accountId}): url=${mqttUrl}, topic=${mqttTopic}`);
|
|
322
|
+
ctx.setStatus({
|
|
323
|
+
accountId: account.accountId,
|
|
324
|
+
mqttUrl,
|
|
325
|
+
mqttTopic,
|
|
326
|
+
running: true,
|
|
327
|
+
connected: false,
|
|
328
|
+
groupPolicy: account.groupPolicy || "open",
|
|
329
|
+
});
|
|
330
|
+
// 创建 MQTT 客户端
|
|
331
|
+
console.log(`[MQTT] startAccount(${account.accountId}): creating GlobalMqttClient...`);
|
|
332
|
+
const client = new GlobalMqttClient({
|
|
333
|
+
mqttUrl,
|
|
334
|
+
mqttTopic,
|
|
335
|
+
abortSignal,
|
|
336
|
+
onConnect: () => {
|
|
337
|
+
console.log(`[MQTT] startAccount(${account.accountId}): onConnect - setting status to connected`);
|
|
338
|
+
ctx.setStatus({
|
|
339
|
+
accountId: account.accountId,
|
|
340
|
+
connected: true,
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
onDisconnect: () => {
|
|
344
|
+
console.log(`[MQTT] startAccount(${account.accountId}): onDisconnect - setting status to disconnected`);
|
|
345
|
+
ctx.setStatus({
|
|
346
|
+
accountId: account.accountId,
|
|
347
|
+
connected: false,
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
onError: (err) => {
|
|
351
|
+
console.error(`[MQTT] startAccount(${account.accountId}): onError - ${err.message}`);
|
|
352
|
+
log?.error(`[rol-websocket-channel] MQTT error: ${err.message}`);
|
|
353
|
+
},
|
|
354
|
+
onMessage: async (topic, payload) => {
|
|
355
|
+
console.log(`[MQTT] startAccount(${account.accountId}): onMessage received`);
|
|
356
|
+
await handleIncomingMessage(payload, account, cfg, runtime, log, mqttTopic);
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
// 启动连接
|
|
360
|
+
console.log(`[MQTT] startAccount(${account.accountId}): calling GlobalMqttClient.connect()...`);
|
|
361
|
+
await client.connect();
|
|
362
|
+
console.log(`[MQTT] startAccount(${account.accountId}): GlobalMqttClient.connect() returned`);
|
|
363
|
+
},
|
|
364
|
+
stopAccount: async (ctx) => {
|
|
365
|
+
const { log, account } = ctx;
|
|
366
|
+
console.log(`[MQTT] stopAccount(${account.accountId}): stopping...`);
|
|
367
|
+
log?.info(`[rol-websocket-channel] Stopping MQTT Channel for ${account.accountId}`);
|
|
368
|
+
ConnectionManager.closeGlobalConnection();
|
|
369
|
+
console.log(`[MQTT] stopAccount(${account.accountId}): stopped`);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
security: {
|
|
373
|
+
getDmPolicy: (account) => account.dmPolicy ?? "open",
|
|
374
|
+
getAllowFrom: (account) => account.allowFrom ?? [],
|
|
375
|
+
checkGroupAccess: (account, groupId) => {
|
|
376
|
+
const groups = account.groups ?? {};
|
|
377
|
+
return "*" in groups || groupId in groups;
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
// ============================================
|
|
382
|
+
// 5. 消息处理函数
|
|
383
|
+
// ============================================
|
|
384
|
+
async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTopic) {
|
|
385
|
+
try {
|
|
386
|
+
const rawData = payload.toString();
|
|
387
|
+
const eventData = JSON.parse(rawData);
|
|
388
|
+
const msgType = eventData.type || "sender";
|
|
389
|
+
const innerData = eventData.data || {};
|
|
390
|
+
const traceId = eventData.trace_id || eventData.traceId || `${Date.now()}`;
|
|
391
|
+
// 忽略 receiver 类型的消息
|
|
392
|
+
if (msgType === "receiver") {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// 处理非标准消息类型
|
|
396
|
+
if (msgType !== "sender") {
|
|
397
|
+
await handleCustomMessageType(msgType, innerData, traceId, account.accountId, mqttTopic);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// 构造标准消息格式
|
|
401
|
+
const attachments = (innerData.attachments || []).map((att) => ({
|
|
402
|
+
url: att.url,
|
|
403
|
+
type: att.type || "application/octet-stream",
|
|
404
|
+
name: att.name || "file",
|
|
405
|
+
size: att.size,
|
|
406
|
+
}));
|
|
407
|
+
const normalizedMessage = {
|
|
408
|
+
id: `${eventData.source || "mqtt"}-${Date.now()}`,
|
|
409
|
+
channel: "rol-websocket-channel",
|
|
410
|
+
accountId: account.accountId,
|
|
411
|
+
senderId: innerData.source || eventData.source || "unknown",
|
|
412
|
+
senderName: innerData.source || eventData.source || "Unknown",
|
|
413
|
+
text: innerData.content || innerData.text || "",
|
|
414
|
+
timestamp: innerData.timestamp || new Date().toISOString(),
|
|
415
|
+
isGroup: false,
|
|
416
|
+
groupId: undefined,
|
|
417
|
+
attachments,
|
|
418
|
+
metadata: { mqttTopic, traceId },
|
|
419
|
+
};
|
|
420
|
+
const targetAgentId = innerData.agentId ?? innerData.agent_id ?? null;
|
|
421
|
+
const targetSessionId = innerData.sessionKey ?? innerData.session_key ?? null;
|
|
422
|
+
log?.info(`[rol-websocket-channel] 📨 Received: "${normalizedMessage.text}" from ${normalizedMessage.senderId}` +
|
|
423
|
+
(targetAgentId ? ` → agent:${targetAgentId}` : "") +
|
|
424
|
+
(targetSessionId ? ` → session:${targetSessionId}` : ""));
|
|
425
|
+
// 解析路由
|
|
426
|
+
const route = runtime.channel.routing.resolveAgentRoute({
|
|
427
|
+
cfg,
|
|
428
|
+
channel: "rol-websocket-channel",
|
|
429
|
+
accountId: account.accountId,
|
|
430
|
+
peer: { kind: "direct", id: normalizedMessage.senderId },
|
|
431
|
+
});
|
|
432
|
+
// 用户传参覆盖自动路由结果
|
|
433
|
+
const resolvedAccountId = targetAgentId ?? route.accountId;
|
|
434
|
+
const resolvedSessionKey = targetSessionId ?? route.sessionKey;
|
|
435
|
+
// 构建消息上下文
|
|
436
|
+
const ctxPayload = runtime.channel.reply.finalizeInboundContext({
|
|
437
|
+
Body: normalizedMessage.text,
|
|
438
|
+
BodyForAgent: normalizedMessage.text,
|
|
439
|
+
From: normalizedMessage.senderId,
|
|
440
|
+
To: undefined,
|
|
441
|
+
SessionKey: resolvedSessionKey,
|
|
442
|
+
AccountId: resolvedAccountId,
|
|
443
|
+
ChatType: "direct",
|
|
444
|
+
SenderName: normalizedMessage.senderName,
|
|
445
|
+
SenderId: normalizedMessage.senderId,
|
|
446
|
+
Provider: "rol-websocket-channel",
|
|
447
|
+
Surface: "rol-websocket-channel",
|
|
448
|
+
MessageSid: normalizedMessage.id,
|
|
449
|
+
Timestamp: Date.now(),
|
|
450
|
+
});
|
|
451
|
+
// 调度回复
|
|
452
|
+
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
453
|
+
ctx: ctxPayload,
|
|
454
|
+
cfg,
|
|
455
|
+
dispatcherOptions: {
|
|
456
|
+
deliver: async (payload) => {
|
|
457
|
+
const conn = ConnectionManager.getGlobalConnection();
|
|
458
|
+
if (!conn || !conn.ws || !conn.ws.connected) {
|
|
459
|
+
throw new Error("No MQTT connection available");
|
|
460
|
+
}
|
|
461
|
+
const replyMessage = {
|
|
462
|
+
type: "receiver",
|
|
463
|
+
trace_id: traceId,
|
|
464
|
+
source: "ai",
|
|
465
|
+
meta: {
|
|
466
|
+
'agentId': resolvedAccountId,
|
|
467
|
+
'sessionKey': resolvedSessionKey
|
|
468
|
+
},
|
|
469
|
+
data: payload,
|
|
470
|
+
timestamp: Date.now(),
|
|
471
|
+
};
|
|
472
|
+
// 根据 source_type 修改 topic 末尾的 #
|
|
473
|
+
let targetTopic = mqttTopic;
|
|
474
|
+
const sourceType = innerData?.source_type;
|
|
475
|
+
if (targetTopic.endsWith("#")) {
|
|
476
|
+
const replacement = sourceType === "device" ? "device" : "bot";
|
|
477
|
+
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
478
|
+
}
|
|
479
|
+
conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
|
|
480
|
+
},
|
|
481
|
+
onError: (err) => {
|
|
482
|
+
log?.error(`[rol-websocket-channel] Delivery error: ${err.message}`);
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
catch (err) {
|
|
488
|
+
log?.error(`[rol-websocket-channel] Failed to process message: ${err instanceof Error ? err.message : String(err)}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function handleCustomMessageType(msgType, innerData, traceId, accountId, mqttTopic) {
|
|
492
|
+
const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
|
|
493
|
+
const response = {
|
|
494
|
+
type: "receiver",
|
|
495
|
+
trace_id: traceId,
|
|
496
|
+
source: "system",
|
|
497
|
+
timestamp: Date.now(),
|
|
498
|
+
};
|
|
499
|
+
if (isSkillInstallFlow) {
|
|
500
|
+
console.log(`[rol-websocket-channel] custom message start: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, accountId=${accountId}, topic=${mqttTopic}`);
|
|
501
|
+
}
|
|
502
|
+
const handlerMethod = messageHandler[msgType];
|
|
503
|
+
if (typeof handlerMethod === "function") {
|
|
504
|
+
try {
|
|
505
|
+
const methodResult = await handlerMethod.call(messageHandler, innerData);
|
|
506
|
+
// 兼容两种返回格式:
|
|
507
|
+
// 1. { ok, result, error } 格式(新的 admin 方法)
|
|
508
|
+
// 2. 直接返回数据格式(原有的 ping、echo、status 方法)
|
|
509
|
+
if (typeof methodResult === "object" &&
|
|
510
|
+
methodResult !== null &&
|
|
511
|
+
"ok" in methodResult) {
|
|
512
|
+
// 新格式:{ ok, result, error }
|
|
513
|
+
response.success = methodResult.ok;
|
|
514
|
+
response.data = methodResult.result;
|
|
515
|
+
if (!methodResult.ok) {
|
|
516
|
+
response.error = methodResult.error?.message || "Unknown error";
|
|
517
|
+
if (isSkillInstallFlow) {
|
|
518
|
+
console.error(`[rol-websocket-channel] custom message failed: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${response.error}, detail=${JSON.stringify(methodResult.error?.data ?? {})}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
else if (isSkillInstallFlow) {
|
|
522
|
+
console.log(`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// 旧格式:直接返回数据
|
|
527
|
+
response.success = true;
|
|
528
|
+
response.data = methodResult;
|
|
529
|
+
if (isSkillInstallFlow) {
|
|
530
|
+
console.log(`[rol-websocket-channel] custom message success: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (handlerErr) {
|
|
535
|
+
response.success = false;
|
|
536
|
+
response.error = handlerErr.message;
|
|
537
|
+
if (isSkillInstallFlow) {
|
|
538
|
+
console.error(`[rol-websocket-channel] custom message threw: type=${msgType}, traceId=${traceId}, slug=${innerData?.slug ?? ""}, error=${handlerErr?.message ?? String(handlerErr)}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
response.success = false;
|
|
544
|
+
response.error = `Unknown message type: ${msgType}`;
|
|
545
|
+
}
|
|
546
|
+
const conn = ConnectionManager.getGlobalConnection();
|
|
547
|
+
if (conn && conn.ws && conn.ws.connected) {
|
|
548
|
+
// 根据 source_type 修改 topic 末尾的 #
|
|
549
|
+
let targetTopic = mqttTopic;
|
|
550
|
+
const sourceType = innerData?.source_type;
|
|
551
|
+
if (targetTopic.endsWith("#")) {
|
|
552
|
+
const replacement = sourceType === "device" ? "device" : "bot";
|
|
553
|
+
targetTopic = targetTopic.slice(0, -1) + replacement;
|
|
554
|
+
}
|
|
555
|
+
conn.ws.publish(targetTopic, JSON.stringify(response));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// ============================================
|
|
559
|
+
// 6. 插件注册入口
|
|
560
|
+
// ============================================
|
|
561
|
+
let isPluginRegistered = false;
|
|
562
|
+
export default function register(api) {
|
|
563
|
+
console.log("[mqtt] Register rol-websocket-channel");
|
|
564
|
+
if (isPluginRegistered) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
console.log("[mqtt] Register rol-websocket-channel real");
|
|
568
|
+
isPluginRegistered = true;
|
|
569
|
+
pluginRuntime = api.runtime;
|
|
570
|
+
// 初始化共享 context
|
|
571
|
+
initializeContext();
|
|
572
|
+
// 注册 Channel
|
|
573
|
+
api.registerChannel({ plugin: WebSocketChannel });
|
|
574
|
+
// 注册 CLI(保留供外部调用)
|
|
575
|
+
registerAdminBridgeCli(api);
|
|
576
|
+
}
|
|
577
|
+
// ============================================
|
|
578
|
+
// 7. CLI 注册(保留供外部调用)
|
|
579
|
+
// ============================================
|
|
580
|
+
function registerAdminBridgeCli(api) {
|
|
581
|
+
api.registerCli(({ program }) => {
|
|
582
|
+
const root = program
|
|
583
|
+
.command("admin-bridge")
|
|
584
|
+
.alias("rol-websocket-channel")
|
|
585
|
+
.description("OpenClaw admin bridge utilities")
|
|
586
|
+
.addHelpText("after", () => "\nCommands return JSON to stdout for Python or shell orchestration.\n");
|
|
587
|
+
// 这里可以添加 CLI 命令,但现在先保持简单
|
|
588
|
+
// 如果需要完整的 CLI,可以从 openclaw-ts/index.ts 复制过来
|
|
589
|
+
root
|
|
590
|
+
.command("ping")
|
|
591
|
+
.description("Test admin bridge connection")
|
|
592
|
+
.action(async () => {
|
|
593
|
+
try {
|
|
594
|
+
const { ping } = await import("./src/admin/methods/system.js");
|
|
595
|
+
const { getContext } = await import("./src/shared/context.js");
|
|
596
|
+
const result = await ping(undefined, getContext());
|
|
597
|
+
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
598
|
+
}
|
|
599
|
+
catch (error) {
|
|
600
|
+
process.exitCode = 1;
|
|
601
|
+
process.stderr.write(JSON.stringify({
|
|
602
|
+
ok: false,
|
|
603
|
+
error: {
|
|
604
|
+
message: error instanceof Error ? error.message : String(error),
|
|
605
|
+
},
|
|
606
|
+
}, null, 2) + "\n");
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
root
|
|
610
|
+
.command("pair <key>")
|
|
611
|
+
.description("Pair rol-websocket-channel and write OpenClaw configuration")
|
|
612
|
+
.option("--endpoint <url>", "Pair exchange endpoint")
|
|
613
|
+
.option("--auth <token>", "Authorization header value for pair exchange")
|
|
614
|
+
.action(async (key, options) => {
|
|
615
|
+
try {
|
|
616
|
+
const { pairWithKey } = await import("./src/admin/methods/pairing.js");
|
|
617
|
+
const result = await pairWithKey({
|
|
618
|
+
key,
|
|
619
|
+
endpoint: options.endpoint,
|
|
620
|
+
auth: options.auth,
|
|
621
|
+
}, getContext());
|
|
622
|
+
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
process.exitCode = 1;
|
|
626
|
+
process.stderr.write(JSON.stringify(formatCliErrorPayload(error), null, 2) + "\n");
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
const mem9 = root
|
|
630
|
+
.command("mem9")
|
|
631
|
+
.description("Mem9 installer and reconnect utilities");
|
|
632
|
+
mem9
|
|
633
|
+
.command("install")
|
|
634
|
+
.description("Install mem9 plugin, create cloud key, write config, and restart gateway")
|
|
635
|
+
.action(async () => {
|
|
636
|
+
try {
|
|
637
|
+
const { installMem9 } = await import("./src/admin/methods/mem9.js");
|
|
638
|
+
const result = await installMem9(getContext());
|
|
639
|
+
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
process.exitCode = 1;
|
|
643
|
+
process.stderr.write(JSON.stringify({
|
|
644
|
+
ok: false,
|
|
645
|
+
error: {
|
|
646
|
+
message: error instanceof Error ? error.message : String(error),
|
|
647
|
+
code: error?.data?.code,
|
|
648
|
+
},
|
|
649
|
+
}, null, 2) + "\n");
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
mem9
|
|
653
|
+
.command("reconnect <key>")
|
|
654
|
+
.description("Replace mem9 apiKey, update config, and restart gateway")
|
|
655
|
+
.action(async (key) => {
|
|
656
|
+
try {
|
|
657
|
+
const { reconnectMem9 } = await import("./src/admin/methods/mem9.js");
|
|
658
|
+
const result = await reconnectMem9(key, getContext());
|
|
659
|
+
process.stdout.write(JSON.stringify({ ok: true, result }, null, 2) + "\n");
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
process.exitCode = 1;
|
|
663
|
+
process.stderr.write(JSON.stringify({
|
|
664
|
+
ok: false,
|
|
665
|
+
error: {
|
|
666
|
+
message: error instanceof Error ? error.message : String(error),
|
|
667
|
+
code: error?.data?.code,
|
|
668
|
+
},
|
|
669
|
+
}, null, 2) + "\n");
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}, {
|
|
673
|
+
descriptors: [
|
|
674
|
+
{
|
|
675
|
+
name: "admin-bridge",
|
|
676
|
+
description: "OpenClaw admin bridge commands",
|
|
677
|
+
hasSubcommands: true,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "rol-websocket-channel",
|
|
681
|
+
description: "OpenClaw admin bridge commands",
|
|
682
|
+
hasSubcommands: true,
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
});
|
|
686
|
+
}
|