rol-websocket-channel 1.0.2 → 1.0.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/index.ts +307 -123
- package/message-handler.ts +23 -0
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -2
- package/readme.md +6 -1
- package/src/admin/methods/index.ts +7 -1
- package/src/admin/methods/mem9.ts +341 -0
- package/src/mqtt/connection-manager.ts +262 -188
- package/src/mqtt/index.ts +6 -6
- package/src/mqtt/mqtt-client.ts +170 -119
- package/src/mqtt/mqtt.test.ts +670 -0
- package/src/mqtt/types.ts +46 -36
|
@@ -1,188 +1,262 @@
|
|
|
1
|
-
// MQTT
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
console.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
*
|
|
177
|
-
*/
|
|
178
|
-
export function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1
|
+
// MQTT 全局连接管理器
|
|
2
|
+
// 全局共享单一连接(随机 clientId,从 topic 解析用户名,支持测试注入)
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
MqttConnection,
|
|
6
|
+
ConnectionCallbacks,
|
|
7
|
+
MqttConnectFn,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
// ─────────────────────────────────────────────
|
|
11
|
+
// 全局单例连接
|
|
12
|
+
// ─────────────────────────────────────────────
|
|
13
|
+
let globalConnection: MqttConnection | null = null;
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────
|
|
16
|
+
// 可注入的 mqtt connect 工厂(用于测试 mock)
|
|
17
|
+
// ─────────────────────────────────────────────
|
|
18
|
+
let _mqttConnectFn: MqttConnectFn | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 注入自定义 mqtt connect 函数(测试用)
|
|
22
|
+
* 传入 null 恢复默认行为(使用真实 mqtt 库)
|
|
23
|
+
*/
|
|
24
|
+
export function _setMqttConnectFn(fn: MqttConnectFn | null): void {
|
|
25
|
+
_mqttConnectFn = fn;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getMqttConnect(): Promise<MqttConnectFn> {
|
|
29
|
+
if (_mqttConnectFn) {
|
|
30
|
+
return _mqttConnectFn;
|
|
31
|
+
}
|
|
32
|
+
const mqtt = await import("mqtt");
|
|
33
|
+
return mqtt.default.connect.bind(mqtt.default) as MqttConnectFn;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─────────────────────────────────────────────
|
|
37
|
+
// 工具函数(纯函数,便于单元测试)
|
|
38
|
+
// ─────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 从 MQTT topic 中解析用户名
|
|
42
|
+
*
|
|
43
|
+
* 模式:announcement/{user_name}/{agent_id}/...
|
|
44
|
+
* 例如:announcement/0b9a784d-e044-4c6f-9024-ef8799c131d1/ce627d3d-9e22-47f7-b1c6-a6ab69570fef/bot
|
|
45
|
+
* → user_name = "0b9a784d-e044-4c6f-9024-ef8799c131d1"
|
|
46
|
+
*
|
|
47
|
+
* 若无法解析则返回 "default_name"
|
|
48
|
+
*/
|
|
49
|
+
export function parseUsernameFromTopic(topic: string): string {
|
|
50
|
+
if (!topic || typeof topic !== "string") return "default_name";
|
|
51
|
+
const parts = topic.split("/");
|
|
52
|
+
if (parts[0] === "announcement" && parts[1] && parts[1].trim() !== "") {
|
|
53
|
+
return parts[1].trim();
|
|
54
|
+
}
|
|
55
|
+
return "default_name";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 根据 topic 生成通配符订阅路径
|
|
60
|
+
*
|
|
61
|
+
* 若能解析到 user_name:announcement/{user_name}/#(不区分 agent_id)
|
|
62
|
+
* 否则:原始 topic 末尾补 /# 若无通配符
|
|
63
|
+
*/
|
|
64
|
+
export function getSubscribeTopic(topic: string): string {
|
|
65
|
+
const username = parseUsernameFromTopic(topic);
|
|
66
|
+
if (username !== "default_name") {
|
|
67
|
+
return `announcement/${username}/#`;
|
|
68
|
+
}
|
|
69
|
+
if (topic.endsWith("#")) return topic;
|
|
70
|
+
if (topic.endsWith("/")) return `${topic}#`;
|
|
71
|
+
return `${topic}/#`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 生成随机 MQTT 客户端 ID
|
|
76
|
+
* 格式:mqtt_client_{8位随机字符}{时间戳36进制}
|
|
77
|
+
*/
|
|
78
|
+
export function generateClientId(): string {
|
|
79
|
+
const rand = Math.random().toString(36).slice(2, 10).padEnd(8, "0");
|
|
80
|
+
const ts = Date.now().toString(36);
|
|
81
|
+
return `mqtt_client_${rand}${ts}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─────────────────────────────────────────────
|
|
85
|
+
// 全局连接 API
|
|
86
|
+
// ─────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取当前全局连接(可能为 null)
|
|
90
|
+
*/
|
|
91
|
+
export function getGlobalConnection(): MqttConnection | null {
|
|
92
|
+
return globalConnection;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 检查全局连接是否处于已连接状态
|
|
97
|
+
*/
|
|
98
|
+
export function isGlobalConnected(): boolean {
|
|
99
|
+
return !!globalConnection?.ws?.connected;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 创建全局 MQTT 连接(若已连接则复用)
|
|
104
|
+
*
|
|
105
|
+
* @param mqttUrl Broker 地址,如 ws://192.168.1.152:8083/mqtt
|
|
106
|
+
* @param mqttTopic 原始 topic,用于解析用户名和作为发布目标
|
|
107
|
+
* @param callbacks 连接事件回调
|
|
108
|
+
*/
|
|
109
|
+
export async function createGlobalMqttConnection(
|
|
110
|
+
mqttUrl: string,
|
|
111
|
+
mqttTopic: string,
|
|
112
|
+
callbacks: ConnectionCallbacks,
|
|
113
|
+
): Promise<MqttConnection> {
|
|
114
|
+
const username = parseUsernameFromTopic(mqttTopic);
|
|
115
|
+
const subscribeTopic = getSubscribeTopic(mqttTopic);
|
|
116
|
+
const clientId = generateClientId();
|
|
117
|
+
|
|
118
|
+
console.log(`[MQTT] createGlobalMqttConnection: url=${mqttUrl}`);
|
|
119
|
+
console.log(
|
|
120
|
+
`[MQTT] createGlobalMqttConnection: username=${username}, clientId=${clientId}`,
|
|
121
|
+
);
|
|
122
|
+
console.log(
|
|
123
|
+
`[MQTT] createGlobalMqttConnection: subscribeTopic=${subscribeTopic}`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// 已有活跃连接,直接复用
|
|
127
|
+
if (globalConnection?.ws?.connected) {
|
|
128
|
+
console.log(
|
|
129
|
+
"[MQTT] createGlobalMqttConnection: reusing existing active connection",
|
|
130
|
+
);
|
|
131
|
+
return globalConnection;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 存在断开的旧连接,先清理
|
|
135
|
+
if (globalConnection) {
|
|
136
|
+
console.log("[MQTT] createGlobalMqttConnection: closing stale connection");
|
|
137
|
+
const stale = globalConnection;
|
|
138
|
+
globalConnection = null;
|
|
139
|
+
try {
|
|
140
|
+
stale.ws.end(true);
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 获取 connect 函数(真实或 mock)
|
|
147
|
+
const mqttConnect = await getMqttConnect();
|
|
148
|
+
|
|
149
|
+
console.log("[MQTT] createGlobalMqttConnection: calling mqtt.connect...");
|
|
150
|
+
const client = mqttConnect(mqttUrl, {
|
|
151
|
+
clientId,
|
|
152
|
+
username,
|
|
153
|
+
reconnectPeriod: 5000, // 5 秒自动重连
|
|
154
|
+
connectTimeout: 30000, // 30 秒连接超时
|
|
155
|
+
keepalive: 30, // 30 秒心跳
|
|
156
|
+
clean: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
globalConnection = {
|
|
160
|
+
ws: client,
|
|
161
|
+
topic: mqttTopic,
|
|
162
|
+
subscribeTopic,
|
|
163
|
+
username,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
setupEventHandlers(client, subscribeTopic, callbacks);
|
|
167
|
+
|
|
168
|
+
console.log(
|
|
169
|
+
"[MQTT] createGlobalMqttConnection: connection object created, waiting for broker...",
|
|
170
|
+
);
|
|
171
|
+
return globalConnection;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 关闭全局连接
|
|
176
|
+
* 先置空再调用 end(),防止 close 事件同步触发时重入
|
|
177
|
+
*/
|
|
178
|
+
export function closeGlobalConnection(): void {
|
|
179
|
+
if (globalConnection) {
|
|
180
|
+
console.log("[MQTT] closeGlobalConnection: force-closing...");
|
|
181
|
+
const conn = globalConnection;
|
|
182
|
+
globalConnection = null;
|
|
183
|
+
try {
|
|
184
|
+
conn.ws.end(true);
|
|
185
|
+
} catch {
|
|
186
|
+
// ignore
|
|
187
|
+
}
|
|
188
|
+
console.log("[MQTT] closeGlobalConnection: done");
|
|
189
|
+
} else {
|
|
190
|
+
console.log("[MQTT] closeGlobalConnection: no active connection");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 向指定 topic 发布消息(使用全局连接)
|
|
196
|
+
* @returns 发布成功返回 true,未连接返回 false
|
|
197
|
+
*/
|
|
198
|
+
export function publishGlobalMessage(topic: string, message: string): boolean {
|
|
199
|
+
if (!isGlobalConnected()) {
|
|
200
|
+
console.error(
|
|
201
|
+
"[MQTT] ❌ publishGlobalMessage: no connected global connection",
|
|
202
|
+
);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
globalConnection!.ws.publish(topic, message);
|
|
206
|
+
console.log(`[MQTT] publishGlobalMessage: published to ${topic}`);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────
|
|
211
|
+
// 内部:事件处理
|
|
212
|
+
// ─────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
function setupEventHandlers(
|
|
215
|
+
client: any,
|
|
216
|
+
subscribeTopic: string,
|
|
217
|
+
callbacks: ConnectionCallbacks,
|
|
218
|
+
): void {
|
|
219
|
+
const { onConnect, onDisconnect, onError, onClose, onMessage } = callbacks;
|
|
220
|
+
|
|
221
|
+
client.on("connect", () => {
|
|
222
|
+
console.log("[MQTT] ✅ Global connection established");
|
|
223
|
+
client.subscribe(subscribeTopic, (err: Error | null) => {
|
|
224
|
+
if (err) {
|
|
225
|
+
console.error(`[MQTT] ❌ Subscribe failed: ${err.message}`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`[MQTT] ✅ Subscribed to: ${subscribeTopic}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
onConnect?.();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
client.on("message", (topic: string, payload: Buffer) => {
|
|
234
|
+
console.log(
|
|
235
|
+
`[MQTT] 📨 Message received on: ${topic} (${payload.length} bytes)`,
|
|
236
|
+
);
|
|
237
|
+
onMessage?.(topic, payload);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
client.on("error", (err: Error) => {
|
|
241
|
+
console.error(`[MQTT] ❌ Global connection error: ${err.message}`);
|
|
242
|
+
onError?.(err);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
client.on("disconnect", () => {
|
|
246
|
+
console.log("[MQTT] 🔴 Global connection disconnected");
|
|
247
|
+
onDisconnect?.();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
client.on("close", () => {
|
|
251
|
+
console.log("[MQTT] 🔴 Global connection closed");
|
|
252
|
+
onClose?.();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
client.on("reconnect", () => {
|
|
256
|
+
console.log("[MQTT] 🔄 Global connection reconnecting...");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
client.on("offline", () => {
|
|
260
|
+
console.log("[MQTT] 😴 Global connection offline");
|
|
261
|
+
});
|
|
262
|
+
}
|
package/src/mqtt/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// MQTT 模块入口
|
|
2
|
-
// 导出所有 MQTT 相关的类型和功能
|
|
3
|
-
|
|
4
|
-
export * from "./types.js";
|
|
5
|
-
export * from "./connection-manager.js";
|
|
6
|
-
export * from "./mqtt-client.js";
|
|
1
|
+
// MQTT 模块入口
|
|
2
|
+
// 导出所有 MQTT 相关的类型和功能
|
|
3
|
+
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export * from "./connection-manager.js";
|
|
6
|
+
export * from "./mqtt-client.js";
|