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 +127 -44
- package/dist/message-handler.js +2 -7
- package/dist/src/admin/lib/openclaw-bin.js +19 -1
- package/dist/src/admin/lib/openclaw-bin.test.js +37 -0
- package/dist/src/admin/methods/index.js +2 -2
- package/dist/src/admin/methods/mem9.js +265 -28
- package/dist/src/admin/methods/mem9.test.js +226 -1
- package/dist/src/admin/methods/system.js +18 -21
- package/index.ts +140 -45
- package/message-handler.ts +2 -7
- package/package.json +2 -2
- package/src/admin/lib/openclaw-bin.test.ts +38 -0
- package/src/admin/lib/openclaw-bin.ts +20 -1
- package/src/admin/methods/index.ts +2 -2
- package/src/admin/methods/mem9.test.ts +280 -1
- package/src/admin/methods/mem9.ts +314 -29
- package/src/admin/methods/system.ts +23 -27
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: "
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
type: "receiver",
|
|
263
|
+
source: "ai",
|
|
264
|
+
data: { text },
|
|
262
265
|
timestamp: Date.now(),
|
|
263
266
|
});
|
|
264
|
-
|
|
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: "
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
mediaUrl,
|
|
281
|
+
type: "receiver",
|
|
282
|
+
source: "ai",
|
|
283
|
+
data: { text, mediaUrl },
|
|
277
284
|
timestamp: Date.now(),
|
|
278
285
|
});
|
|
279
|
-
conn.
|
|
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:
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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(["
|
|
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 = {
|
package/dist/message-handler.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
23
|
+
// OpenClaw core updates are managed outside this plugin.
|
|
24
24
|
['system.pluginSelfUpdate', pluginSelfUpdate],
|
|
25
25
|
['system.currentVersion', currentVersion],
|
|
26
26
|
// Agents
|