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.
@@ -1,188 +1,262 @@
1
- // MQTT 连接管理器
2
- // 负责管理 MQTT 连接的建立和维护(使用 MQTT.js 自带的重连功能)
3
-
4
- import type { MqttConnection, MqttConfig, ConnectionCallbacks } from "./types.js";
5
-
6
- // 全局连接存储
7
- const connections = new Map<string, MqttConnection>();
8
-
9
- /**
10
- * 获取连接
11
- */
12
- export function getConnection(accountId: string): MqttConnection | undefined {
13
- const conn = connections.get(accountId);
14
- console.log(`[MQTT] getConnection(${accountId}): ${conn ? 'found' : 'not found'}`);
15
- return conn;
16
- }
17
-
18
- /**
19
- * 检查连接是否存在且已连接
20
- */
21
- export function isConnected(accountId: string): boolean {
22
- const conn = connections.get(accountId);
23
- const connected = !!(conn && conn.ws && conn.ws.connected);
24
- console.log(`[MQTT] isConnected(${accountId}): ${connected}`);
25
- return connected;
26
- }
27
-
28
- /**
29
- * 删除连接
30
- */
31
- export function removeConnection(accountId: string): void {
32
- console.log(`[MQTT] removeConnection(${accountId})`);
33
- connections.delete(accountId);
34
- }
35
-
36
- /**
37
- * 创建 MQTT 连接
38
- * 使用 MQTT.js 自带的重连功能
39
- */
40
- export async function createMqttConnection(
41
- config: MqttConfig,
42
- callbacks: ConnectionCallbacks,
43
- ): Promise<MqttConnection> {
44
- const { mqttUrl, mqttTopic, accountId } = config;
45
- console.log(`[MQTT] createMqttConnection(${accountId}): url=${mqttUrl}, topic=${mqttTopic}`);
46
-
47
- // 如果已有活跃连接,直接返回(防止重复创建)
48
- const existingConn = connections.get(accountId);
49
- if (existingConn && existingConn.ws && existingConn.ws.connected) {
50
- console.log(`[MQTT] createMqttConnection(${accountId}): connection already exists and connected, skipping`);
51
- return existingConn;
52
- }
53
-
54
- // 如果有未连接的旧连接,先关闭
55
- if (existingConn) {
56
- console.log(`[MQTT] createMqttConnection(${accountId}): closing existing disconnected connection`);
57
- existingConn.ws.end(true);
58
- connections.delete(accountId);
59
- }
60
-
61
- // 动态导入 mqtt
62
- console.log(`[MQTT] createMqttConnection(${accountId}): importing mqtt library...`);
63
- const mqtt = await import("mqtt");
64
- console.log(`[MQTT] createMqttConnection(${accountId}): mqtt library imported`);
65
-
66
- // 创建 MQTT 客户端,启用自带的重连功能
67
- console.log(`[MQTT] createMqttConnection(${accountId}): creating client with reconnect...`);
68
- const client = mqtt.default.connect(mqttUrl, {
69
- reconnectPeriod: 5000, // 5秒后自动重连
70
- connectTimeout: 30000, // 连接超时 30秒
71
- keepalive: 30, // 心跳 30秒
72
- clean: true, // 清理会话
73
- });
74
- console.log(`[MQTT] createMqttConnection(${accountId}): client created`);
75
-
76
- // 存储连接
77
- const connection: MqttConnection = {
78
- ws: client,
79
- accountId,
80
- mqttTopic,
81
- };
82
- connections.set(accountId, connection);
83
- console.log(`[MQTT] createMqttConnection(${accountId}): connection stored, total connections=${connections.size}`);
84
-
85
- // 设置事件处理器
86
- setupEventHandlers(client, accountId, mqttTopic, callbacks);
87
- console.log(`[MQTT] createMqttConnection(${accountId}): event handlers set up`);
88
-
89
- return connection;
90
- }
91
-
92
- /**
93
- * 设置 MQTT 事件处理器
94
- */
95
- function setupEventHandlers(
96
- client: any,
97
- accountId: string,
98
- mqttTopic: string,
99
- callbacks: ConnectionCallbacks,
100
- ): void {
101
- const { onConnect, onDisconnect, onError, onClose, onMessage } = callbacks;
102
-
103
- // 连接成功
104
- client.on("connect", () => {
105
- console.log(`[MQTT] Connected to broker for account: ${accountId}`);
106
-
107
- // 订阅 topic
108
- console.log(`[MQTT] subscribing to topic: ${mqttTopic} for account: ${accountId}`);
109
- client.subscribe(mqttTopic, (err: Error | null) => {
110
- if (err) {
111
- console.error(`[MQTT] ❌ Failed to subscribe: ${err.message}`);
112
- } else {
113
- console.log(`[MQTT] Subscribed to: ${mqttTopic}`);
114
- }
115
- });
116
-
117
- onConnect?.();
118
- });
119
-
120
- // 收到消息
121
- client.on("message", (topic: string, payload: Buffer) => {
122
- console.log(`[MQTT] 📨 message received on topic: ${topic}, size: ${payload.length} bytes`);
123
- onMessage?.(topic, payload);
124
- });
125
-
126
- // 连接错误
127
- client.on("error", (err: Error) => {
128
- console.error(`[MQTT] ❌ Error for account ${accountId}: ${err.message}`);
129
- onError?.(err);
130
- // MQTT.js 会自动处理重连,不需要我们干预
131
- });
132
-
133
- // 连接断开
134
- client.on("disconnect", () => {
135
- console.log(`[MQTT] 🔴 Disconnected for account: ${accountId}`);
136
- onDisconnect?.();
137
- });
138
-
139
- // 连接关闭
140
- client.on("close", () => {
141
- console.log(`[MQTT] 🔴 Connection closed for account: ${accountId}`);
142
- // 注意:MQTT.js 会自动重连,不要在这里删除连接
143
- // 只有在手动停止时才删除
144
- onClose?.();
145
- });
146
-
147
- // 重连开始
148
- client.on("reconnect", () => {
149
- console.log(`[MQTT] 🔄 Reconnecting for account: ${accountId}...`);
150
- });
151
-
152
- // 离线
153
- client.on("offline", () => {
154
- console.log(`[MQTT] 😴 Client offline for account: ${accountId}`);
155
- });
156
- }
157
-
158
- /**
159
- * 关闭连接(手动停止时使用)
160
- */
161
- export function closeConnection(accountId: string): void {
162
- console.log(`[MQTT] closeConnection(${accountId}): closing connection...`);
163
- const conn = connections.get(accountId);
164
- if (conn) {
165
- console.log(`[MQTT] closeConnection(${accountId}): ending mqtt client...`);
166
- // force=true 立即关闭,不触发重连
167
- conn.ws.end(true);
168
- removeConnection(accountId);
169
- console.log(`[MQTT] closeConnection(${accountId}): connection closed`);
170
- } else {
171
- console.log(`[MQTT] closeConnection(${accountId}): no connection found`);
172
- }
173
- }
174
-
175
- /**
176
- * 发布消息
177
- */
178
- export function publishMessage(accountId: string, topic: string, message: string): boolean {
179
- const conn = connections.get(accountId);
180
- if (!conn || !conn.ws || !conn.ws.connected) {
181
- console.error(`[MQTT] No connection available for account: ${accountId}`);
182
- return false;
183
- }
184
-
185
- console.log(`[MQTT] publishMessage(${accountId}): publishing to ${topic}`);
186
- conn.ws.publish(topic, message);
187
- return true;
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";