rol-websocket-channel 1.4.8 → 1.4.9

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