rol-websocket-channel 1.6.7 → 1.7.0

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 CHANGED
@@ -249,19 +249,27 @@ const WebSocketChannel = {
249
249
  }),
250
250
  },
251
251
  outbound: {
252
+ // MQTT 是 topic-based 路由,不依赖 to 做地址解析
253
+ // 使用 "direct" 模式:AI reply 走 deliver 回调;message 工具走 sendText
252
254
  deliveryMode: "direct",
253
- sendText: async ({ to, text }) => {
255
+ sendText: async ({ to, text, sessionKey }) => {
254
256
  const conn = ConnectionManager.getGlobalConnection();
255
257
  if (!conn || !conn.ws || !conn.ws.connected) {
256
258
  return { ok: false, error: "No MQTT connection" };
257
259
  }
260
+ // 与 deliver 回调保持一致,使用 receiver 格式发布消息
258
261
  const message = JSON.stringify({
259
- type: "message",
260
- to,
261
- content: text,
262
+ type: "receiver",
263
+ source: "ai",
264
+ data: { text },
262
265
  timestamp: Date.now(),
263
266
  });
264
- conn.ws.publish(conn.topic, message);
267
+ // 将 # 替换为 bot(outbound 不知道 source_type,默认 bot)
268
+ let targetTopic = conn.topic;
269
+ if (targetTopic.endsWith("#")) {
270
+ targetTopic = targetTopic.slice(0, -1) + "bot";
271
+ }
272
+ conn.ws.publish(targetTopic, message);
265
273
  return { ok: true };
266
274
  },
267
275
  sendMedia: async ({ to, text, mediaUrl }) => {
@@ -270,16 +278,75 @@ const WebSocketChannel = {
270
278
  return { ok: false, error: "No MQTT connection" };
271
279
  }
272
280
  const message = JSON.stringify({
273
- type: "media",
274
- to,
275
- content: text,
276
- mediaUrl,
281
+ type: "receiver",
282
+ source: "ai",
283
+ data: { text, mediaUrl },
277
284
  timestamp: Date.now(),
278
285
  });
279
- conn.ws.publish(conn.topic, message);
286
+ let targetTopic = conn.topic;
287
+ if (targetTopic.endsWith("#")) {
288
+ targetTopic = targetTopic.slice(0, -1) + "bot";
289
+ }
290
+ conn.ws.publish(targetTopic, message);
280
291
  return { ok: true };
281
292
  },
282
293
  },
294
+ // ============================================
295
+ // messaging: 告知框架如何解析 target 地址
296
+ // MQTT 是 topic-based,任意非空字符串都是合法 target
297
+ // ============================================
298
+ messaging: {
299
+ // 接受任何非空字符串作为合法 target(MQTT 不做地址校验)
300
+ normalizeTarget: (raw) => {
301
+ const trimmed = raw?.trim();
302
+ return trimmed || undefined;
303
+ },
304
+ // 告知框架:deliver 目标就是原始字符串本身
305
+ targetResolver: {
306
+ looksLikeId: (_raw) => true,
307
+ },
308
+ },
309
+ // ============================================
310
+ // actions: 处理 message 工具的 send action
311
+ // 直接发布到 MQTT,完全绕过 target 解析
312
+ // ============================================
313
+ actions: {
314
+ describeMessageTool: () => ({
315
+ actions: ["send"],
316
+ }),
317
+ supportsAction: ({ action }) => action === "send",
318
+ handleAction: async (ctx) => {
319
+ const { action, params } = ctx;
320
+ if (action !== "send") {
321
+ throw new Error(`Unsupported action: ${action}`);
322
+ }
323
+ const text = typeof params.message === "string" ? params.message
324
+ : typeof params.text === "string" ? params.text
325
+ : "";
326
+ const conn = ConnectionManager.getGlobalConnection();
327
+ if (!conn || !conn.ws || !conn.ws.connected) {
328
+ return {
329
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: "No MQTT connection" }) }],
330
+ details: {},
331
+ };
332
+ }
333
+ const replyMessage = JSON.stringify({
334
+ type: "receiver",
335
+ source: "ai",
336
+ data: { text },
337
+ timestamp: Date.now(),
338
+ });
339
+ let targetTopic = conn.topic;
340
+ if (targetTopic.endsWith("#")) {
341
+ targetTopic = targetTopic.slice(0, -1) + "bot";
342
+ }
343
+ conn.ws.publish(targetTopic, replyMessage);
344
+ return {
345
+ content: [{ type: "text", text: JSON.stringify({ ok: true }) }],
346
+ details: {},
347
+ };
348
+ },
349
+ },
283
350
  gateway: {
284
351
  startAccount: async (ctx) => {
285
352
  const { log, account, abortSignal, cfg } = ctx;
@@ -435,11 +502,13 @@ async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTo
435
502
  const resolvedAccountId = targetAgentId ?? route.accountId;
436
503
  const resolvedSessionKey = targetSessionId ?? route.sessionKey;
437
504
  // 构建消息上下文
505
+ // To 必须设置为 senderId:框架用它确定 message 工具的回复目标
506
+ // From 是发送方;To 是「这条对话的对端」(即 bot 应回复给谁)
438
507
  const ctxPayload = runtime.channel.reply.finalizeInboundContext({
439
508
  Body: normalizedMessage.text,
440
509
  BodyForAgent: normalizedMessage.text,
441
510
  From: normalizedMessage.senderId,
442
- To: undefined,
511
+ To: normalizedMessage.senderId,
443
512
  SessionKey: resolvedSessionKey,
444
513
  AccountId: resolvedAccountId,
445
514
  ChatType: "direct",
@@ -451,46 +520,60 @@ async function handleIncomingMessage(payload, account, cfg, runtime, log, mqttTo
451
520
  Timestamp: Date.now(),
452
521
  });
453
522
  // 调度回复
454
- await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
455
- ctx: ctxPayload,
456
- cfg,
457
- dispatcherOptions: {
458
- deliver: async (payload) => {
459
- const conn = ConnectionManager.getGlobalConnection();
460
- if (!conn || !conn.ws || !conn.ws.connected) {
461
- throw new Error("No MQTT connection available");
462
- }
463
- const replyMessage = {
464
- type: "receiver",
465
- trace_id: traceId,
466
- source: "ai",
467
- meta: {
468
- 'agentId': resolvedAccountId,
469
- 'sessionKey': resolvedSessionKey
470
- },
471
- data: payload,
472
- timestamp: Date.now(),
473
- };
474
- // 根据 source_type 修改 topic 末尾的 #
475
- let targetTopic = mqttTopic;
476
- const sourceType = innerData?.source_type;
477
- if (targetTopic.endsWith("#")) {
478
- const replacement = sourceType === "device" ? "device" : "bot";
479
- targetTopic = targetTopic.slice(0, -1) + replacement;
480
- }
481
- conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
482
- },
483
- onError: (err) => {
484
- log?.error(`[rol-websocket-channel] Delivery error: ${err.message}`);
485
- },
523
+ // withReplyDispatcher({ dispatcher, run }) 是底层封装:
524
+ // - dispatcher 由 createReplyDispatcherWithTyping 创建(负责 deliver 和分批发送)
525
+ // - run 调用 dispatchReplyFromConfig(负责路由和 AI 回复逻辑)
526
+ // - markComplete 由框架内部在 run 完成后自动调用,不需要外部传入
527
+ const { dispatcher, markRunComplete, markDispatchIdle } = runtime.channel.reply.createReplyDispatcherWithTyping({
528
+ deliver: async (payload) => {
529
+ const conn = ConnectionManager.getGlobalConnection();
530
+ if (!conn || !conn.ws || !conn.ws.connected) {
531
+ throw new Error("No MQTT connection available");
532
+ }
533
+ const replyMessage = {
534
+ type: "receiver",
535
+ trace_id: traceId,
536
+ source: "ai",
537
+ meta: {
538
+ 'agentId': resolvedAccountId,
539
+ 'sessionKey': resolvedSessionKey
540
+ },
541
+ data: payload,
542
+ timestamp: Date.now(),
543
+ };
544
+ // 根据 source_type 修改 topic 末尾的 #
545
+ let targetTopic = mqttTopic;
546
+ const sourceType = innerData?.source_type;
547
+ if (targetTopic.endsWith("#")) {
548
+ const replacement = sourceType === "device" ? "device" : "bot";
549
+ targetTopic = targetTopic.slice(0, -1) + replacement;
550
+ }
551
+ conn.ws.publish(targetTopic, JSON.stringify(replyMessage));
552
+ },
553
+ onError: (err) => {
554
+ log?.error(`[rol-websocket-channel] Delivery error: ${err.message} ${err.stack}`);
486
555
  },
487
556
  });
557
+ try {
558
+ await runtime.channel.reply.withReplyDispatcher({
559
+ dispatcher,
560
+ run: () => runtime.channel.reply.dispatchReplyFromConfig({
561
+ ctx: ctxPayload,
562
+ cfg,
563
+ dispatcher,
564
+ }),
565
+ });
566
+ }
567
+ finally {
568
+ markRunComplete?.();
569
+ markDispatchIdle?.();
570
+ }
488
571
  }
489
572
  catch (err) {
490
573
  log?.error(`[rol-websocket-channel] Failed to process message: ${err instanceof Error ? err.message : String(err)}`);
491
574
  }
492
575
  }
493
- const immediateAckMessageTypes = new Set(["openclawUpdate", "pluginSelfUpdate"]);
576
+ const immediateAckMessageTypes = new Set(["pluginSelfUpdate"]);
494
577
  export async function handleCustomMessageType(msgType, innerData, traceId, accountId, mqttTopic) {
495
578
  const isSkillInstallFlow = msgType === "skillsInstallFromClawHub" || msgType === "skillsUpdateFromClawHub";
496
579
  const response = {
@@ -18,7 +18,7 @@ import { uninstallSkill } from './src/admin/methods/skills-extended.js';
18
18
  import { toggleSkill } from './src/admin/methods/skills-toggle.js';
19
19
  import { listMemoryFiles, getMemoryFile, backupMemory, exportMemoryZip, getMemoryPresignedPost, createMemoryBackupRecord, importMemoryZip, } from './src/admin/methods/memory.js';
20
20
  import { getMem9Config, installMem9, reconnectMem9 } from './src/admin/methods/mem9.js';
21
- import { currentVersion, doctorFix, logs, openclawUpdate, pluginSelfUpdate, restart, stop } from './src/admin/methods/system.js';
21
+ import { currentVersion, doctorFix, logs, pluginSelfUpdate, restart, stop } from './src/admin/methods/system.js';
22
22
  export class MessageHandler {
23
23
  /**
24
24
  * 示例方法:处理 ping 类型的消息
@@ -497,12 +497,7 @@ export class MessageHandler {
497
497
  return await logs(data, context);
498
498
  });
499
499
  }
500
- async openclawUpdate(data) {
501
- return wrapAdminCall(async () => {
502
- const context = getContext();
503
- return await openclawUpdate(data, context);
504
- });
505
- }
500
+ // OpenClaw core updates are managed outside this plugin.
506
501
  async pluginSelfUpdate(data) {
507
502
  return wrapAdminCall(async () => {
508
503
  const context = getContext();
@@ -1,3 +1,21 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs';
1
4
  export function resolveOpenClawBin() {
2
- return process.env.OPENCLAW_BIN || 'openclaw';
5
+ if (process.env.OPENCLAW_BIN) {
6
+ return process.env.OPENCLAW_BIN;
7
+ }
8
+ if (process.platform === 'win32') {
9
+ const appData = process.env.APPDATA;
10
+ const candidates = [
11
+ appData ? path.join(appData, 'npm', 'openclaw.cmd') : null,
12
+ path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'openclaw.cmd')
13
+ ];
14
+ for (const candidate of candidates) {
15
+ if (candidate && fs.existsSync(candidate)) {
16
+ return candidate;
17
+ }
18
+ }
19
+ }
20
+ return 'openclaw';
3
21
  }
@@ -0,0 +1,37 @@
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { resolveOpenClawBin } from './openclaw-bin.js';
7
+ describe('resolveOpenClawBin', () => {
8
+ test('uses APPDATA npm shim on Windows when OPENCLAW_BIN is unset', { skip: process.platform !== 'win32' }, async () => {
9
+ const originalOpenClawBin = process.env.OPENCLAW_BIN;
10
+ const originalAppData = process.env.APPDATA;
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'openclaw-bin-'));
12
+ try {
13
+ const appData = path.join(root, 'Roaming');
14
+ const shim = path.join(appData, 'npm', 'openclaw.cmd');
15
+ await fs.mkdir(path.dirname(shim), { recursive: true });
16
+ await fs.writeFile(shim, '@echo off\r\n', 'utf8');
17
+ delete process.env.OPENCLAW_BIN;
18
+ process.env.APPDATA = appData;
19
+ assert.equal(resolveOpenClawBin(), shim);
20
+ }
21
+ finally {
22
+ if (originalOpenClawBin === undefined) {
23
+ delete process.env.OPENCLAW_BIN;
24
+ }
25
+ else {
26
+ process.env.OPENCLAW_BIN = originalOpenClawBin;
27
+ }
28
+ if (originalAppData === undefined) {
29
+ delete process.env.APPDATA;
30
+ }
31
+ else {
32
+ process.env.APPDATA = originalAppData;
33
+ }
34
+ await fs.rm(root, { recursive: true, force: true });
35
+ }
36
+ });
37
+ });
@@ -11,7 +11,7 @@ import { listSessions } from './sessions.js';
11
11
  import { getSession, prepareMessage, attachSkill } from './sessions-extended.js';
12
12
  import { installSkillFromClawHub, installSkillFromNpm, listInstalledSkills, searchClawHubSkills, updateSkillFromClawHub } from './skills.js';
13
13
  import { getInstalledSkill, uninstallSkill } from './skills-extended.js';
14
- import { currentVersion, doctorFix, logs, openclawUpdate, ping, pluginSelfUpdate, restart, stop } from './system.js';
14
+ import { currentVersion, doctorFix, logs, ping, pluginSelfUpdate, restart, stop } from './system.js';
15
15
  import { getUsageBreakdown, getUsagePageSummary, getUsageSummary, getUsageTimeseries } from './usage.js';
16
16
  const methods = new Map([
17
17
  // System
@@ -20,7 +20,7 @@ const methods = new Map([
20
20
  ['system.stop', stop],
21
21
  ['system.doctorFix', doctorFix],
22
22
  ['system.logs', logs],
23
- ['system.openclawUpdate', openclawUpdate],
23
+ // OpenClaw core updates are managed outside this plugin.
24
24
  ['system.pluginSelfUpdate', pluginSelfUpdate],
25
25
  ['system.currentVersion', currentVersion],
26
26
  // Agents