mm_mqtt_server 1.0.2 → 2.0.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/README.md CHANGED
@@ -1,158 +1,121 @@
1
1
  # mm_mqtt_server
2
2
 
3
- MQTT 代理服务器入站适配器模块。继承 [mm_corejs](https://gitee.com/qiuwenwu91/mm_corejs) 的 `Mod` 基类,基于 [aedes](https://www.npmjs.com/package/aedes) 实现 MQTT Broker,提供连接管理、消息处理管道与 JSON-RPC 2.0 消息插件,可脱离完整框架独立运行。
3
+ 最薄的 MQTT 代理服务器入站适配器。继承 `mm_mod` 的 `Mod`,基于 [aedes](https://www.npmjs.com/package/aedes) 提供 Broker,把每条入站发布直接交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。
4
4
 
5
5
  - 入口:`index.js`(CommonJS)
6
- - 导出:`MqttServer`(代理模块)、`MessagePlugin`(JSON-RPC 消息插件)
7
- - 运行时依赖:`mm_corejs`、`aedes`
8
- - 测试覆盖率:100%(`jest.config.js` 四项阈值)
6
+ - 导出:`MqttServer`、`admin`、`mqttServerAdmin`
7
+ - 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`、`aedes`
9
8
 
10
- ## 特性
9
+ ## 消息路径
11
10
 
12
- - **标准 Broker**:`net.Server` + `aedes` 监听 TCP 1883,支持订阅、发布、QoS 0-2 与保留消息。
13
- - **认证策略**:`allow_anonymous` 放行匿名连接;配置 `username` 后校验用户名与口令,失败以 `returnCode 5` 拒绝。
14
- - **消息管道**:入站发布消息经 `Handle` 的 `before / check / main / render` 阶段处理,管道产出 `{ topic, payload }` 时由代理重新发布(可用于改写、应答);`getHandle()` 可注册主题守卫等钩子。
15
- - **连接管理**:`Store` 登记在线客户端,支持 `publish / closeClient / listClients(条件查询与分页)/ hasClient / setClientOptions`。
16
- - **JSON-RPC 2.0 插件**:`MessagePlugin` 挂载后接管 RPC 形态报文,应答发布到「请求主题 + 后缀」,支持通知回推、主动推送与服务器反向调用客户端方法。
17
- - **选项透传**:连接级 `options`(user_id / device_id / agent_id 等)随每条入站消息透传给管道钩子与 RPC 处理函数,适配多用户 / 多设备 / 多智能体场景。
18
-
19
- ## 安装
20
-
21
- ```bash
22
- npm install mm_mqtt_server
11
+ ```
12
+ net → aedes → publish(topic, packet) → ctx = { client_id, topic, qos, retain, packet, client, broker }
13
+ → eventManager.run(topic, params, ctx, options)
14
+ → 结果形如 { topic, payload } 时由代理重发
23
15
  ```
24
16
 
25
- ## 环境要求
26
-
27
- - Node.js >= 22.5.0
28
- - 模块规范:CommonJS
17
+ 事件 `target` 即发布主题;`mm_event` 按字符串级 `*` 匹配(如 `sensor/*`、`*/temp`),不支持 `#`。`params` 为 `{ client_id, topic, payload, qos, retain }`,`payload` 已按 UTF-8 转为字符串。
29
18
 
30
19
  ## 快速开始
31
20
 
32
21
  ```js
33
22
  'use strict';
34
23
 
35
- const { MqttServer, MessagePlugin } = require('mm_mqtt_server');
24
+ const { MqttServer } = require('mm_mqtt_server');
36
25
 
37
- /** 最小 Server 容器:满足 Base 契约即可(见 example/demo.js 完整实现) */
38
- const server = {
39
- services: new Map(),
40
- getService(name) { return this.services.get(name); },
41
- hasService(name) { return this.services.has(name); },
42
- setService(name, instance) { this.services.set(name, instance); return this; },
43
- getManager() { return undefined; },
44
- hasManager() { return false; }
45
- };
46
-
47
- const mqtt = new MqttServer({ name: 'mqtt_server', host: '127.0.0.1', port: 1883 }, server);
48
-
49
- // 主题守卫:check 阶段产出即短路
50
- mqtt.getHandle().on('check', (ctx, params) => {
51
- if (params.topic.startsWith('blocked/')) return { dropped: true };
52
- }, 100, 'guard');
26
+ const mqtt = new MqttServer({ name: 'mqtt', host: '127.0.0.1', port: 1883 });
27
+ const events = mqtt.getEventManager();
53
28
 
54
- // JSON-RPC 插件
55
- const rpc = new MessagePlugin({ reply_suffix: '/response', qos: 0 });
56
- mqtt.use(rpc);
57
- rpc.addMethod('sum', (params, extra) => ({ sum: params.reduce((a, b) => a + b, 0), from: extra.topic }));
58
-
59
- // 事件
60
- mqtt.on('connection', (client) => mqtt.setClientOptions(client.id, { device_id: 'd1' }));
61
- mqtt.on('message', (client_id, info) => console.log(client_id, info.topic, info.payload));
62
- mqtt.on('subscribe', (client_id, topics) => console.log(client_id, topics));
63
- mqtt.on('close', (client_id) => console.log('断开', client_id));
64
- mqtt.on('error', (err) => console.error(err.message));
29
+ events.add({ name: 'echo', target: 'demo/echo', end: true }).main = async (params) => ({
30
+ topic: 'demo/echo/out',
31
+ payload: params.payload
32
+ });
65
33
 
66
34
  await mqtt.init();
67
35
  await mqtt.start();
68
36
 
69
- // 主动操作
70
- await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
71
- await mqtt.run({}, { action: 'list_clients' });
72
-
73
- await mqtt.stop();
74
- await mqtt.dispose();
37
+ console.log(mqtt.getAddress());
75
38
  ```
76
39
 
77
- ## 服务器配置(config)
78
-
79
- 顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
40
+ ## 服务器配置
80
41
 
81
42
  | 字段 | 类型 | 默认值 | 说明 |
82
43
  | --- | --- | --- | --- |
83
- | `name` | string | `'mqtt_server'` | 模块名称(必填) |
44
+ | `name` | string | `'os'` | 模块名称(必填) |
45
+ | `title` | string | `'MQTT代理服务器模块'` | 模块标题 |
46
+ | `description` | string | — | 模块描述 |
47
+ | `version` | string | `'2.0.0'` | 版本号 |
84
48
  | `host` | string | `'0.0.0.0'` | 监听地址 |
85
- | `port` | integer | `1883` | 监听端口(1-65535) |
49
+ | `port` | number | `1883` | 监听端口 |
86
50
  | `allow_anonymous` | boolean | `true` | 未配置 `username` 时放行匿名连接 |
87
51
  | `username` | string | `''` | 认证用户名,非空时启用口令认证 |
88
52
  | `password` | string | `''` | 认证口令 |
53
+ | `options` | object | `{}` | 透传给 `Aedes.createBroker` 的额外选项(`authenticate` 固定由本模块提供) |
54
+ | `params` | object | `{ action }` | `main` 的参数约定 |
89
55
 
90
56
  ## API
91
57
 
92
- ### MqttServer
93
-
94
58
  | 成员 | 说明 |
95
59
  | --- | --- |
96
- | `run(ctx, params)` | 主动操作入口,`params.action` 派发:`publish`(需 `topic`,可带 `payload` / `qos` / `retain`)、`close_client`(需 `client_id`)、`list_clients`(可选 `query` 条件查询) |
60
+ | `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
61
+ | `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
62
+ | `getEventManager()` | 获取 mm_event 的 `mqtt` 作用域事件管理器(与其他实例共享),用于注册事件 |
63
+ | `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
64
+ | `getAddress()` | 实际监听地址,未启动为 `null` |
65
+ | `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `publish` / `close` / `list` |
97
66
  | `publish(topic, payload, opts)` | 代理端发布消息(进入正常路由,订阅者可收到),返回是否成功 |
98
67
  | `closeClient(client_id)` | 关闭指定客户端连接,返回是否发起 |
99
- | `listClients(query)` | 在线客户端列表(`{ id, address }`),支持 Store 条件 / 分页 / keyword |
68
+ | `listClients()` | 在线客户端列表 `{ id, address }` |
100
69
  | `hasClient(client_id)` | 是否在线 |
101
- | `setClientOptions(client_id, options)` | 绑定连接级选项(`null` 清空),随入站消息透传 |
102
- | `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
103
- | `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;产出 `{ topic, payload }` 时代理重发 |
104
- | `getAddress()` | 实际监听地址,未启动为 `null` |
70
+ | `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
105
71
 
106
- ### 事件
72
+ ## 请求上下文(ctx)
107
73
 
108
- | 事件 | 参数 | 触发时机 |
109
- | --- | --- | --- |
110
- | `connection` | `(client)` | 客户端接入,`client` 为 `{ id, address }` |
111
- | `message` | `(client_id, info)` | 入站发布消息经管道内置 `emit_message` 钩子分发,`info` 为 `{ topic, payload, qos, retain }` |
112
- | `subscribe` | `(client_id, topics)` | 客户端订阅主题 |
113
- | `unsubscribe` | `(client_id, topics)` | 客户端取消订阅 |
114
- | `close` | `(client_id)` | 客户端断开 |
115
- | `error` | `(err)` | TCP / broker / 管道钩子错误 |
74
+ | 字段 | 说明 |
75
+ | --- | --- |
76
+ | `client_id` | 发布者客户端 ID |
77
+ | `topic` | 发布主题 |
78
+ | `qos` / `retain` | 发布报文的服务质量与保留标记 |
79
+ | `packet` | aedes 原始发布报文 |
80
+ | `client` | aedes 客户端实例,可直接调用其方法 |
81
+ | `broker` | aedes broker 实例,可自行发布或操作会话 |
116
82
 
117
- ### 消息管道
83
+ ## 事件处理函数
118
84
 
119
- 每条入站发布消息以 `{ client_id, topic, payload, qos, retain }` 为 `params`、连接级 `options` 为第三参进入管道:
85
+ 签名固定为 `(params, ctx, options)`:
120
86
 
121
- ```
122
- before → check → main(业务钩子按 sort 依次执行)→ render
87
+ ```js
88
+ events.add({ name: 'relay', target: 'sensor/*', end: true }).main = async (params, ctx, options) => {
89
+ return { topic: 'archive/sensor', payload: params.payload };
90
+ };
123
91
  ```
124
92
 
125
- 产出 `{ topic, payload }` 时由代理重新发布该消息;代理自身重发的报文 `client` 为空,不再进入管道,因此无回环。
93
+ 返回 `{ topic, payload }` 时由代理重发该消息(可带 `qos` / `retain`);返回其他值或 `undefined` 时不重发。事件也可直接用 `ctx.broker.publish()` 或 `ctx.client` 自行处理。代理自身重发的报文 `client` 为空,不会再次进入事件,因此无回环。
126
94
 
127
- ## MessagePlugin(JSON-RPC 2.0)
95
+ 事件抛出的异常由 `mm_event` 记入 `options.error`,本模块再通过 `error` 事件上报,不会中断 broker。
128
96
 
129
- 挂载后接管 RPC 形态报文(非 RPC 消息透传给业务钩子):
97
+ ## 主动操作
130
98
 
131
99
  ```js
132
- const rpc = new MessagePlugin({ reply_suffix: '/response', push_prefix: 'rpc/push', call_prefix: 'rpc/call', qos: 0, retain: false });
133
- mqtt.use(rpc);
134
- rpc.addMethod('echo', (params, extra, options) => params); // extra.client_id / extra.topic
100
+ await mqtt.run({ action: 'publish', topic: 'sys/notice', payload: 'hi' });
101
+ await mqtt.run({ action: 'close', client_id: 'device_1' });
102
+ await mqtt.run({ action: 'list' });
135
103
  ```
136
104
 
137
- | 成员 | 说明 |
138
- | --- | --- |
139
- | `addMethod(name, handler)` / `delMethod(name)` / `listMethods(query)` | 方法表管理,处理函数签名 `(params, extra, options)` |
140
- | `push(client_id, data, options)` | 发布数据到 `options.topic`(缺省 `push_prefix/client_id`),客户端订阅即接收 |
141
- | `call(client_id, method, params, options)` | 发布 JSON-RPC 请求到 `options.topic`(缺省 `call_prefix/client_id`)并等待客户端反馈,`options.timeout_ms` 缺省 30000 |
142
- | `bind(server)` / `isBound()` | 挂载与状态查询 |
105
+ ## 生命周期
143
106
 
144
- 协议行为:请求(含 `id`)应答发布到「请求主题 + `reply_suffix`」;通知(无 `id`)处理函数有返回值时,把服务器发起的通知 `{ jsonrpc, method, params }` 发布到应答主题;客户端把响应形态报文(含 `result` / `error`、无 `method`)发布回 RPC 主题即结算对应的反向调用。
107
+ `onStart` 创建 aedes broker 与 `net.Server` 并开始监听,绑定连接表与入站发布;`onStop` 断开全部客户端、关闭服务器与 broker。事件管理器取自 mm_event 的 `mqtt` 作用域,由全局 `Admin` 共享,停止时不清理。端口被占用时 `start()` 抛错,模块进入 `error` 状态,可 `dispose()` 释放。
145
108
 
146
109
  ## 示例
147
110
 
148
111
  ```bash
149
- npm run demo # 启动 MQTT 代理演示服务器(127.0.0.1:1883),见 example/demo.js
112
+ npm run demo # 启动演示代理(127.0.0.1:1883),见 example/demo.js
150
113
  ```
151
114
 
152
115
  ## 测试
153
116
 
154
117
  ```bash
155
- npm test # jest --coverage,四项覆盖率阈值均为 100%
118
+ npm test # node --test
156
119
  ```
157
120
 
158
121
  ## 许可证
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const { MqttServer } = require('..');
4
+
5
+ /** 演示监听端口 */
6
+ const DEMO_PORT = 1883;
7
+
8
+ /**
9
+ * 注册演示事件:发布主题即事件 target
10
+ * @param {Object} events 事件管理器
11
+ * @returns {void}
12
+ */
13
+ function bindEvents(events) {
14
+ events.add({ name: 'echo', target: 'demo/echo', end: true }).main = async (params) => ({
15
+ topic: 'demo/echo/out',
16
+ payload: params.payload
17
+ });
18
+
19
+ events.add({ name: 'time', target: 'demo/time', end: true }).main = async () => ({
20
+ topic: 'demo/time/out',
21
+ payload: new Date().toISOString()
22
+ });
23
+
24
+ events.add({ name: 'upper', target: 'demo/upper', end: true }).main = async (params) => ({
25
+ topic: 'demo/lower',
26
+ payload: String(params.payload).toLowerCase()
27
+ });
28
+ }
29
+
30
+ /**
31
+ * 演示入口:启动 MQTT 代理并打印监听地址
32
+ * @returns {Promise<void>} 无返回值
33
+ */
34
+ async function main() {
35
+ const mqtt = new MqttServer({
36
+ name: 'demo',
37
+ host: '127.0.0.1',
38
+ port: DEMO_PORT
39
+ });
40
+
41
+ bindEvents(mqtt.getEventManager());
42
+ mqtt.on('error', (err) => console.error(`[demo] 错误: ${err.message}`));
43
+
44
+ await mqtt.init();
45
+ await mqtt.start();
46
+
47
+ const address = mqtt.getAddress();
48
+ console.log(`mqtt://127.0.0.1:${address.port}`);
49
+ console.log('订阅 demo/echo/out 后发布到 demo/echo,事件返回的 { topic, payload } 会被代理重发');
50
+ console.log('demo/time 返回当前时间,demo/upper 把内容转小写后发到 demo/lower');
51
+ }
52
+
53
+ main().catch((err) => {
54
+ console.error(err);
55
+ process.exit(1);
56
+ });
package/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  const MqttServer = require('./lib/mqtt_server');
4
- const MessagePlugin = require('./lib/message');
5
4
  const Admin = require('mm_corejs').Admin;
6
5
 
7
6
  const admin = new Admin();
@@ -9,11 +8,10 @@ admin.setClass(MqttServer);
9
8
 
10
9
  module.exports = {
11
10
  MqttServer,
12
- MessagePlugin,
13
11
  admin,
14
12
  /**
15
13
  * 获取指定作用域的 MQTT 服务器
16
- * @param {string} [scope='sys'] 作用域名称
14
+ * @param {string} [scope='os'] 作用域名称
17
15
  * @param {object} [config] 配置参数
18
16
  * @returns {MqttServer} MQTT 服务器实例
19
17
  */
@@ -21,6 +19,6 @@ module.exports = {
21
19
  if (!scope) {
22
20
  scope = 'os';
23
21
  }
24
- return admin.get(scope, { name: scope, ...config });
22
+ return admin.get(scope, config);
25
23
  }
26
24
  };
@@ -2,49 +2,27 @@
2
2
 
3
3
  const net = require('net');
4
4
  const { Aedes } = require('aedes');
5
- const { Mod, Store } = require('mm_corejs');
6
- const { Handle } = require('mm_handle');
5
+ const { Mod } = require('mm_mod');
6
+ const { eventAdmin } = require('mm_event');
7
7
 
8
8
  /**
9
- * 服务器设置校验 schema(校验对象为顶层 config)
10
- */
11
- const SERVER_SCHEMA = {
12
- type: 'object',
13
- properties: {
14
- host: { type: 'string', min_length: 1 },
15
- port: { type: 'integer', minimum: 1, maximum: 65535 },
16
- allow_anonymous: { type: 'boolean' },
17
- username: { type: 'string' },
18
- password: { type: 'string' }
19
- }
20
- };
21
-
22
- /**
23
- * MqttServer —— MQTT 代理服务器(入站适配器模块)。
9
+ * MqttServer —— 最薄的 MQTT 代理传输层(入站适配器模块)。
24
10
  *
25
- * 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
26
- * 调用参数校验由 config.params 声明式定义;
27
- * 服务器设置(host / port / allow_anonymous / username / password)为顶层 config 字段,
28
- * 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
29
- *
30
- * 职责:
31
- * 1. 监听:onStart 创建 aedes broker 与 net.Server,按 host / port 监听 MQTT 连接;
32
- * 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
33
- * 3. 消息管道:入站发布消息经 Handle 的 before/check/main/render 阶段处理,
34
- * 内置 main 钩子发 message 事件保持兼容;管道产出 { topic, payload } 时
35
- * 由代理重新发布该消息;getHandle() 可注册自定义钩子(如主题黑名单);
36
- * 4. 事件分发:connection / message / subscribe / unsubscribe / close / error;
37
- * 5. 主动操作:run(ctx, params) 按 params.action 派发 publish / close_client / list_clients。
11
+ * 只做三件事:
12
+ * 1. 生命周期:由 Mod 的 init / start / stop 驱动 aedes broker 与 net.Server;
13
+ * 2. 消息转交:以发布主题为 target,直接调用
14
+ * EventManager.run(topic, params, ctx, options),不做路由、不做插件;
15
+ * 3. 结果重发:事件返回 { topic, payload } 时由代理重新发布,事件也可用
16
+ * ctx.broker / ctx.client 自行处理。
38
17
  *
39
18
  * 用法:
40
- * const mqtt = new MqttServer({ name: 'mqtt_server', host: '127.0.0.1', port: 1883 }, server);
41
- * mqtt.getHandle().on('check', (ctx, params) => {
42
- * if (params.topic.startsWith('blocked/')) return { dropped: true };
43
- * }, 100, 'guard');
44
- * mqtt.on('message', (client_id, info) => console.log(client_id, info.topic, info.payload));
19
+ * const mqtt = new MqttServer({ name: 'mqtt', port: 1883 });
20
+ * mqtt.getEventManager().add({ name: 'echo', target: 'mm/*' }).main = async (params) => ({
21
+ * topic: params.topic,
22
+ * payload: params.payload
23
+ * });
45
24
  * await mqtt.init();
46
25
  * await mqtt.start();
47
- * await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
48
26
  */
49
27
  class MqttServer extends Mod {
50
28
  /**
@@ -53,8 +31,8 @@ class MqttServer extends Mod {
53
31
  static config = {
54
32
  name: 'os',
55
33
  title: 'MQTT代理服务器模块',
56
- description: '基于 aedes 的 MQTT 代理:管理客户端连接、主题订阅与消息路由',
57
- version: '1.0.0',
34
+ description: 'MQTT 传输层:消息直达事件管理器,不做路由与插件',
35
+ version: '2.0.0',
58
36
  host: '0.0.0.0',
59
37
  port: 1883,
60
38
  allow_anonymous: true,
@@ -65,13 +43,12 @@ class MqttServer extends Mod {
65
43
  type: 'object',
66
44
  required: ['action'],
67
45
  properties: {
68
- action: { type: 'string', enum: ['publish', 'close_client', 'list_clients'] },
46
+ action: { type: 'string', enum: ['publish', 'close', 'list'] },
69
47
  client_id: { type: 'string', min_length: 1 },
70
48
  topic: { type: 'string', min_length: 1 },
71
49
  payload: { type: 'string' },
72
50
  qos: { type: 'integer', minimum: 0, maximum: 2 },
73
- retain: { type: 'boolean' },
74
- query: { type: 'object' }
51
+ retain: { type: 'boolean' }
75
52
  }
76
53
  }
77
54
  };
@@ -87,27 +64,29 @@ class MqttServer extends Mod {
87
64
  #server = null;
88
65
 
89
66
  /**
90
- * 连接注册表:Store 存储,主键 id、名称字段 name
67
+ * 事件管理器(取自 mm_event 的 mqtt 作用域,与其他实例共享)
91
68
  */
92
- #clients = new Store({ key: 'id', name: 'name' });
69
+ #events = null;
93
70
 
94
71
  /**
95
- * 消息处理管道(before/check/main/render/error/success/after)
96
- * 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
72
+ * 在线连接表:client_id -> 连接信息
97
73
  */
98
- #handle = new Handle();
74
+ #clients = new Map();
99
75
 
100
- // ---------- 生命周期钩子 ----------
101
76
  /**
102
- * 初始化钩子:校验服务器设置并装配消息管道
77
+ * 构造函数
78
+ * @param {Object} config 模块配置
79
+ * @param {Object} server 框架实例
103
80
  */
104
- async onInit() {
105
- await this._validateConfig();
106
- this._initHandle();
81
+ constructor(config, server) {
82
+ super(config, server);
83
+ this.#events = eventAdmin('mqtt');
107
84
  }
108
85
 
86
+ // ---------- 生命周期钩子 ----------
109
87
  /**
110
88
  * 启动钩子:创建 broker 与 TCP 服务器并开始监听
89
+ * @returns {Promise<void>} 无返回值
111
90
  */
112
91
  async onStart() {
113
92
  const broker = await this._createBroker();
@@ -120,7 +99,8 @@ class MqttServer extends Mod {
120
99
  }
121
100
 
122
101
  /**
123
- * 停止钩子:断开全部客户端,关闭 TCP 服务器与 broker
102
+ * 停止钩子:断开全部连接、关闭服务器与 broker(事件管理器与其他实例共享,不清理)
103
+ * @returns {Promise<void>} 无返回值
124
104
  */
125
105
  async onStop() {
126
106
  this._closeAllClients();
@@ -128,13 +108,6 @@ class MqttServer extends Mod {
128
108
  await this._closeBroker();
129
109
  }
130
110
 
131
- /**
132
- * 释放钩子:清空连接注册表
133
- */
134
- async onDispose() {
135
- this.#clients.clear();
136
- }
137
-
138
111
  // ---------- 执行入口 ----------
139
112
  /**
140
113
  * 模块主逻辑:按 action 派发操作
@@ -146,8 +119,8 @@ class MqttServer extends Mod {
146
119
  async main(params, ctx, options) {
147
120
  const action = params.action;
148
121
  if (action === 'publish') return this._actionPublish(params);
149
- if (action === 'close_client') return this._actionCloseClient(params);
150
- return this._actionListClients(params);
122
+ if (action === 'close') return this._actionClose(params);
123
+ return this._actionList();
151
124
  }
152
125
 
153
126
  /**
@@ -166,19 +139,50 @@ class MqttServer extends Mod {
166
139
  * @param {Object} params 调用参数,含 client_id
167
140
  * @returns {Object} 操作结果
168
141
  */
169
- _actionCloseClient(params) {
142
+ _actionClose(params) {
170
143
  this._requireClientId(params);
171
144
  const success = this.closeClient(params.client_id);
172
- return { action: 'close_client', client_id: params.client_id, success };
145
+ return { action: 'close', client_id: params.client_id, success };
173
146
  }
174
147
 
175
148
  /**
176
- * 执行列出客户端操作
177
- * @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
149
+ * 执行列出连接操作
178
150
  * @returns {Object} 操作结果,clients 为在线客户端列表
179
151
  */
180
- _actionListClients(params) {
181
- return { action: 'list_clients', clients: this.listClients(params.query) };
152
+ _actionList() {
153
+ return { action: 'list', clients: this.listClients() };
154
+ }
155
+
156
+ // ---------- 事件管理器 ----------
157
+ /**
158
+ * 获取事件管理器,用于注册事件
159
+ * @returns {Object} 事件管理器实例
160
+ */
161
+ getEventManager() {
162
+ return this.#events;
163
+ }
164
+
165
+ /**
166
+ * 注入外部事件管理器
167
+ * @param {Object} manager 事件管理器实例
168
+ * @returns {MqttServer} 当前实例
169
+ * @throws {TypeError} manager 不具备 run 方法时
170
+ */
171
+ setEventManager(manager) {
172
+ if (!manager || typeof manager.run !== 'function') {
173
+ throw new TypeError(`${this._label()} manager must provide run()`);
174
+ }
175
+ this.#events = manager;
176
+ return this;
177
+ }
178
+
179
+ /**
180
+ * 获取实际监听地址
181
+ * @returns {Object|null} 监听地址信息,未启动时为 null
182
+ */
183
+ getAddress() {
184
+ const server = this.#server;
185
+ return server ? server.address() : null;
182
186
  }
183
187
 
184
188
  // ---------- 公开操作 ----------
@@ -186,7 +190,7 @@ class MqttServer extends Mod {
186
190
  * 代理端发布消息到指定主题
187
191
  * @param {string} topic 主题
188
192
  * @param {string|Buffer} payload 消息内容
189
- * @param {Object} opts 发布选项,含 qos 与 retain
193
+ * @param {Object} [opts] 发布选项,含 qos 与 retain
190
194
  * @returns {Promise<boolean>} 是否发布成功
191
195
  */
192
196
  publish(topic, payload, opts = {}) {
@@ -205,48 +209,27 @@ class MqttServer extends Mod {
205
209
  }
206
210
 
207
211
  /**
208
- * 关闭指定客户端连接
212
+ * 关闭指定客户端连接(连接表由 broker 的断开回调清理)
209
213
  * @param {string} client_id 客户端 ID
210
214
  * @returns {boolean} 是否发起关闭
211
215
  */
212
216
  closeClient(client_id) {
213
- const entry = this.#clients.getById(client_id);
214
- if (!entry) return false;
215
- entry.client.close();
217
+ const client = this.#clients.get(client_id);
218
+ if (!client) return false;
219
+ client.client.close();
216
220
  return true;
217
221
  }
218
222
 
219
223
  /**
220
- * 列出在线客户端(支持 Store 条件查询与分页)
221
- * @param {Object} [query] 查询条件,缺省时列出全部
224
+ * 列出在线客户端
222
225
  * @returns {Array<Object>} 客户端信息列表,含 id 与 address
223
226
  */
224
- listClients(query) {
225
- return this.#clients.list(query).map((entry) => ({ id: entry.id, address: entry.address }));
226
- }
227
-
228
- /**
229
- * 绑定连接级选项(如 user_id / device_id / agent_id),入站消息将作为管道 options 透传给钩子与 RPC 处理函数
230
- * @param {string} client_id 客户端 ID
231
- * @param {Object|null} options 选项对象,null 时清空
232
- * @returns {boolean} 是否设置成功(客户端不在线时失败)
233
- */
234
- setClientOptions(client_id, options) {
235
- const entry = this.#clients.getById(client_id);
236
- if (!entry) return false;
237
- entry.options = options || null;
238
- return true;
239
- }
240
-
241
- /**
242
- * 读取连接级选项,未绑定时返回空对象
243
- * @param {string} client_id 客户端 ID
244
- * @returns {Object} 连接级选项
245
- */
246
- _clientOptions(client_id) {
247
- const entry = this.#clients.getById(client_id);
248
- if (!entry || !entry.options) return {};
249
- return entry.options;
227
+ listClients() {
228
+ const list = [];
229
+ for (const client of this.#clients.values()) {
230
+ list.push({ id: client.id, address: client.address });
231
+ }
232
+ return list;
250
233
  }
251
234
 
252
235
  /**
@@ -255,40 +238,31 @@ class MqttServer extends Mod {
255
238
  * @returns {boolean} 是否在线
256
239
  */
257
240
  hasClient(client_id) {
258
- return !!this.#clients.getById(client_id);
241
+ return this.#clients.has(client_id);
259
242
  }
260
243
 
261
244
  /**
262
- * 挂载插件:调用 plugin.bind(this),由插件自行向管道注册钩子;
263
- * 如 MessagePlugin 挂载后接管 JSON-RPC 请求并应答到请求主题 + 后缀
264
- * @param {Object} plugin 具备 bind(server) 方法的插件实例
265
- * @returns {MqttServer} 当前实例
266
- * @throws {TypeError} plugin 不具备 bind 方法时
267
- */
268
- use(plugin) {
269
- if (!plugin || typeof plugin.bind !== 'function') {
270
- throw new TypeError(`${this._label()} plugin must provide bind()`);
271
- }
272
- plugin.bind(this);
273
- return this;
274
- }
275
-
276
- /**
277
- * 获取消息处理管道,供外部注册 before/check/main 等钩子;
278
- * 钩子产出 { topic, payload } 时由代理重新发布该消息
279
- * @returns {Handle} 消息管道实例
245
+ * 绑定连接级选项,入站消息将作为 options 透传给事件
246
+ * @param {string} client_id 客户端 ID
247
+ * @param {Object|null} options 选项对象,null 时清空
248
+ * @returns {boolean} 是否设置成功(客户端不在线时失败)
280
249
  */
281
- getHandle() {
282
- return this.#handle;
250
+ setClientOptions(client_id, options) {
251
+ const client = this.#clients.get(client_id);
252
+ if (!client) return false;
253
+ client.options = options || null;
254
+ return true;
283
255
  }
284
256
 
285
257
  /**
286
- * 获取实际监听地址
287
- * @returns {Object|null} 监听地址信息,未启动时为 null
258
+ * 获取连接级选项(复制一份,避免事件管理器回写)
259
+ * @param {string} client_id 客户端 ID
260
+ * @returns {Object} 选项对象
288
261
  */
289
- getAddress() {
290
- const server = this.#server;
291
- return server ? server.address() : null;
262
+ _clientOptions(client_id) {
263
+ const client = this.#clients.get(client_id);
264
+ if (!client || !client.options) return {};
265
+ return { ...client.options };
292
266
  }
293
267
 
294
268
  // ---------- broker 装配 ----------
@@ -297,50 +271,44 @@ class MqttServer extends Mod {
297
271
  * @returns {Promise<Object>} broker 实例
298
272
  */
299
273
  async _createBroker() {
300
- const broker = await Aedes.createBroker({
274
+ return Aedes.createBroker({
275
+ ...this.config.options,
301
276
  authenticate: (client, username, password, done) => {
302
277
  this._authenticate(client, username, password, done);
303
278
  }
304
279
  });
305
- return broker;
306
280
  }
307
281
 
308
282
  /**
309
- * 把 broker 事件映射为模块事件,并维护连接注册表
283
+ * 绑定 broker 事件:维护连接表并把入站发布转交事件管理器
310
284
  * @param {Object} broker aedes broker 实例
311
285
  */
312
286
  _bindBrokerEvents(broker) {
313
- broker.on('client', (client) => {
314
- this.#clients.delById(client.id);
315
- this.#clients.add({
316
- id: client.id,
317
- name: client.id,
318
- address: this._remoteAddress(client),
319
- client
320
- });
321
- this._safeEmit('connection', this._clientInfo(client));
322
- });
323
- broker.on('clientDisconnect', (client) => {
324
- const existed = this.#clients.delById(client.id);
325
- if (existed) this._safeEmit('close', client.id);
326
- });
287
+ broker.on('client', (client) => this._registerClient(client));
288
+ broker.on('clientDisconnect', (client) => this.#clients.delete(client.id));
327
289
  broker.on('publish', (packet, client) => {
328
290
  if (!client) return;
329
- this._handlePublish(client, packet).catch((err) => this._emitError(err));
330
- });
331
- broker.on('subscribe', (subscriptions, client) => {
332
- const topics = subscriptions.map((item) => item.topic);
333
- this._safeEmit('subscribe', client.id, topics);
334
- });
335
- broker.on('unsubscribe', (topics, client) => {
336
- this._safeEmit('unsubscribe', client.id, topics);
291
+ this._handlePublish(client, packet);
337
292
  });
338
293
  broker.on('clientError', (client, err) => this._emitError(err));
339
294
  broker.on('connectionError', (client, err) => this._emitError(err));
340
295
  }
341
296
 
342
297
  /**
343
- * 入站发布消息经管道处理:错误上抛,产出 { topic, payload } 时代理重发
298
+ * 登记在线客户端
299
+ * @param {Object} client aedes client 实例
300
+ */
301
+ _registerClient(client) {
302
+ this.#clients.set(client.id, {
303
+ id: client.id,
304
+ address: this._remoteAddress(client),
305
+ client,
306
+ options: null
307
+ });
308
+ }
309
+
310
+ /**
311
+ * 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
344
312
  * @param {Object} client aedes client 实例
345
313
  * @param {Object} packet aedes 发布报文
346
314
  * @returns {Promise<void>} 无返回值
@@ -353,17 +321,31 @@ class MqttServer extends Mod {
353
321
  qos: packet.qos,
354
322
  retain: packet.retain
355
323
  };
356
- const ctx = { client_id: client.id };
357
- const done = await this.#handle.run(ctx, params, this._clientOptions(client.id));
358
- if (done.error) this._emitError(done.error);
359
- if (this._isRepublish(done.result)) {
360
- await this.publish(done.result.topic, done.result.payload, done.result);
324
+ const ctx = {
325
+ client_id: client.id,
326
+ topic: packet.topic,
327
+ qos: packet.qos,
328
+ retain: packet.retain,
329
+ packet,
330
+ client,
331
+ broker: this.#broker
332
+ };
333
+ const options = this._clientOptions(client.id);
334
+ try {
335
+ const result = await this.#events.run(packet.topic, params, ctx, options);
336
+ const error = ctx.error || options.error;
337
+ if (error) this._emitError(error);
338
+ if (this._isRepublish(result)) {
339
+ await this.publish(result.topic, result.payload, result);
340
+ }
341
+ } catch (err) {
342
+ this._emitError(err);
361
343
  }
362
344
  }
363
345
 
364
346
  /**
365
- * 判断管道产出是否为待重发的发布描述(须含 topic 与 payload)
366
- * @param {*} result 管道产出的结果
347
+ * 判断事件结果是否为待重发的发布描述(须含 topic 与 payload)
348
+ * @param {*} result 事件结果
367
349
  * @returns {boolean} 是否为发布描述
368
350
  */
369
351
  _isRepublish(result) {
@@ -400,8 +382,7 @@ class MqttServer extends Mod {
400
382
  * @returns {boolean} 是否放行
401
383
  */
402
384
  _checkAnonymous() {
403
- const anonymous = this.config.allow_anonymous && !this.config.username;
404
- return anonymous;
385
+ return !!(this.config.allow_anonymous && !this.config.username);
405
386
  }
406
387
 
407
388
  /**
@@ -439,24 +420,12 @@ class MqttServer extends Mod {
439
420
  return {
440
421
  cmd: 'publish',
441
422
  topic,
442
- payload: Buffer.from(String(payload)),
423
+ payload: Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload)),
443
424
  qos: opts.qos || 0,
444
425
  retain: opts.retain || false
445
426
  };
446
427
  }
447
428
 
448
- /**
449
- * 提取客户端信息
450
- * @param {Object} client aedes client 实例
451
- * @returns {Object} 含 id 与 address 的信息
452
- */
453
- _clientInfo(client) {
454
- return {
455
- id: client.id,
456
- address: this._remoteAddress(client)
457
- };
458
- }
459
-
460
429
  /**
461
430
  * 获取远端地址字符串
462
431
  * @param {Object} client aedes client 实例
@@ -487,11 +456,11 @@ class MqttServer extends Mod {
487
456
  }
488
457
 
489
458
  /**
490
- * 断开全部客户端连接并清空注册表
459
+ * 断开全部客户端连接并清空连接表
491
460
  */
492
461
  _closeAllClients() {
493
- for (const entry of this.#clients.list()) {
494
- entry.client.close();
462
+ for (const client of this.#clients.values()) {
463
+ client.client.close();
495
464
  }
496
465
  this.#clients.clear();
497
466
  }
@@ -505,7 +474,7 @@ class MqttServer extends Mod {
505
474
  this.#server = null;
506
475
  if (!server) return Promise.resolve();
507
476
  return new Promise((resolve) => {
508
- server.close(resolve);
477
+ server.close(() => resolve());
509
478
  });
510
479
  }
511
480
 
@@ -518,36 +487,11 @@ class MqttServer extends Mod {
518
487
  this.#broker = null;
519
488
  if (!broker) return Promise.resolve();
520
489
  return new Promise((resolve) => {
521
- broker.close(resolve);
490
+ broker.close(() => resolve());
522
491
  });
523
492
  }
524
493
 
525
- // ---------- 消息管道 ----------
526
- /**
527
- * 装配内置管道钩子:main 发 message 事件保持向后兼容
528
- */
529
- _initHandle() {
530
- this.#handle.on('main', (params, ctx) => {
531
- this._safeEmit('message', params.client_id, {
532
- topic: params.topic,
533
- payload: params.payload,
534
- qos: params.qos,
535
- retain: params.retain
536
- });
537
- }, 1000, 'emit_message');
538
- }
539
-
540
494
  // ---------- 校验辅助 ----------
541
- /**
542
- * 校验顶层 config 中的服务器设置
543
- * @returns {Promise<void>} 无返回值
544
- * @throws {Error} 校验不通过时
545
- */
546
- async _validateConfig() {
547
- const tip = await this._validate(this.config, SERVER_SCHEMA);
548
- if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
549
- }
550
-
551
495
  /**
552
496
  * 断言调用参数含 client_id
553
497
  * @param {Object} params 调用参数
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mm_mqtt_server",
3
- "version": "1.0.2",
4
- "description": "MQTT 代理服务器入站适配器模块,基于 aedes,继承 mm_corejs 的 Mod 基类,支持独立运行。",
3
+ "version": "2.0.0",
4
+ "description": "最薄的 MQTT 代理服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
5
  "keywords": [
6
6
  "超级美眉",
7
7
  "入站适配器",
@@ -22,7 +22,8 @@
22
22
  "main": "index.js",
23
23
  "files": [
24
24
  "index.js",
25
- "lib"
25
+ "lib",
26
+ "example"
26
27
  ],
27
28
  "engines": {
28
29
  "node": ">=22.5.0"
@@ -38,13 +39,15 @@
38
39
  "scripts": {
39
40
  "start": "node example/demo.js",
40
41
  "demo": "node example/demo.js",
41
- "test": "jest --coverage"
42
+ "test": "node --test \"test/*.test.js\""
42
43
  },
43
44
  "dependencies": {
44
45
  "aedes": "^1.2.0",
45
- "mm_corejs": "^1.1.4"
46
+ "mm_corejs": "^1.1.4",
47
+ "mm_event": "^2.2.2",
48
+ "mm_mod": "^1.0.1"
46
49
  },
47
50
  "devDependencies": {
48
- "jest": "^30.5.2"
51
+ "mqtt-packet": "^9.0.2"
49
52
  }
50
53
  }
package/lib/message.js DELETED
@@ -1,197 +0,0 @@
1
- 'use strict';
2
-
3
- const { JsonRpc } = require('mm_corejs');
4
-
5
- /**
6
- * 默认应答主题后缀
7
- */
8
- const DEFAULT_REPLY_SUFFIX = '/response';
9
-
10
- /**
11
- * 默认主动推送主题前缀(完整主题为 前缀/客户端ID)
12
- */
13
- const DEFAULT_PUSH_PREFIX = 'rpc/push';
14
-
15
- /**
16
- * 默认反向调用主题前缀(完整主题为 前缀/客户端ID,客户端订阅后接收调用请求)
17
- */
18
- const DEFAULT_CALL_PREFIX = 'rpc/call';
19
-
20
- /**
21
- * MessagePlugin —— JSON-RPC 2.0 消息插件(mm_mqtt_server 的可选业务组件)。
22
- *
23
- * 职责:
24
- * 1. 方法表:内部持有 JsonRpc 核心,addMethod / delMethod / listMethods 登记可调用方法;
25
- * 2. 管道挂载:bind(mqtt) 后向消息管道注册 main 钩子 "json_rpc"(sort 100,
26
- * 先于内置 emit_message 执行),命中 JSON-RPC 请求时产出重发描述
27
- * { topic: 请求主题 + reply_suffix, payload: 响应 JSON },由代理发布应答;
28
- * 3. 通知回推:通知(无 id)处理函数返回非 undefined 结果时,产出服务器发起的
29
- * 通知对象 { jsonrpc, method, params: result } 并发布到应答主题;无结果或出错时
30
- * 不产出;非 JSON-RPC 消息(解析失败、缺 jsonrpc 字段)透传,控制权交还管道;
31
- * 4. 主动推送:push(client_id, data, options) 发布到 options.topic
32
- * (缺省为 push_prefix/client_id),处理函数或客户端可在任意时刻
33
- * (如异步任务完成后)向订阅该主题的客户端回推结果;
34
- * 5. 反向调用:call(client_id, method, params, options) 发布 JSON-RPC 请求到
35
- * options.topic(缺省为 call_prefix/client_id)并等待反馈;客户端把响应
36
- * (含 result/error、无 method)发布回 RPC 服务主题,入站时被优先识别并结算;
37
- * 6. 无回环:代理重发的应答 client 为空,不进入消息管道。
38
- *
39
- * 用法:
40
- * const mqtt = new MqttServer({ name: 'mqtt_server', port: 1883 }, server);
41
- * const rpc = new MessagePlugin({ reply_suffix: '/response', push_prefix: 'rpc/push' });
42
- * mqtt.use(rpc);
43
- * rpc.addMethod('echo', (params) => params);
44
- * await mqtt.init();
45
- * await mqtt.start();
46
- */
47
- class MessagePlugin {
48
- /**
49
- * JSON-RPC 协议核心
50
- */
51
- #rpc = new JsonRpc();
52
-
53
- /**
54
- * 已挂载的 MqttServer 实例,未挂载时为 null
55
- */
56
- #server = null;
57
-
58
- /**
59
- * 应答策略配置
60
- */
61
- #config = {
62
- reply_suffix: DEFAULT_REPLY_SUFFIX,
63
- push_prefix: DEFAULT_PUSH_PREFIX,
64
- call_prefix: DEFAULT_CALL_PREFIX,
65
- qos: 0,
66
- retain: false
67
- };
68
-
69
- /**
70
- * 构造函数
71
- * @param {Object} [config] 应答策略配置,含 reply_suffix / push_prefix / call_prefix / qos / retain
72
- */
73
- constructor(config = {}) {
74
- this.#config = { ...this.#config, ...config };
75
- }
76
-
77
- /**
78
- * 挂载到 MqttServer 的消息管道,注册内置 main 钩子
79
- * @param {Object} server 具备 getHandle() 的 MqttServer 实例
80
- * @returns {MessagePlugin} 当前实例
81
- * @throws {TypeError} server 不具备 getHandle 方法时
82
- */
83
- bind(server) {
84
- if (!server || typeof server.getHandle !== 'function') {
85
- throw new TypeError('[MessagePlugin] server must provide getHandle()');
86
- }
87
- this.#server = server;
88
- server.getHandle().on('main', (params, ctx, options) => {
89
- return this._execute(params, ctx, options);
90
- }, 100, 'json_rpc');
91
- return this;
92
- }
93
-
94
- /**
95
- * 判断是否已挂载
96
- * @returns {boolean} 是否已挂载到 MqttServer
97
- */
98
- isBound() {
99
- return this.#server !== null;
100
- }
101
-
102
- /**
103
- * 登记 RPC 方法
104
- * @param {string} name 方法名
105
- * @param {Function} handler 处理函数,签名 handler(params, extra, options),extra 含 client_id 与 topic,options 为管道请求选项
106
- * @returns {MessagePlugin} 当前实例
107
- */
108
- addMethod(name, handler) {
109
- this.#rpc.addMethod(name, handler);
110
- return this;
111
- }
112
-
113
- /**
114
- * 删除 RPC 方法
115
- * @param {string} name 方法名
116
- * @returns {boolean} 是否删除成功
117
- */
118
- delMethod(name) {
119
- return this.#rpc.delMethod(name);
120
- }
121
-
122
- /**
123
- * 列出 RPC 方法(支持 Store 条件查询与分页)
124
- * @param {Object} [query] 查询条件,缺省时列出全部
125
- * @returns {Array<Object>} 方法列表,含 name
126
- */
127
- listMethods(query) {
128
- return this.#rpc.listMethods(query);
129
- }
130
-
131
- /**
132
- * 主动推送:发布数据到指定主题(缺省为 push_prefix/client_id,客户端订阅后接收)
133
- * @param {string} client_id 目标客户端 ID,用于组装缺省主题
134
- * @param {string|Object} data 待推送数据,对象时 JSON 序列化
135
- * @param {Object} [options] 推送选项,含 topic(覆盖缺省主题)
136
- * @returns {Promise<boolean>} 是否发布成功
137
- * @throws {Error} 插件尚未挂载到服务器时
138
- */
139
- push(client_id, data, options = {}) {
140
- if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
141
- const config = this.#config;
142
- const topic = options.topic || `${config.push_prefix}/${client_id}`;
143
- const payload = typeof data === 'string' ? data : JSON.stringify(data);
144
- return this.#server.publish(topic, payload, { qos: config.qos, retain: config.retain });
145
- }
146
-
147
- /**
148
- * 实时调用客户端方法:发布 JSON-RPC 请求并等待客户端反馈 result
149
- * @param {string} client_id 目标客户端 ID,用于组装缺省调用主题
150
- * @param {string} method 客户端方法名
151
- * @param {*} [params] 调用参数
152
- * @param {Object} [options] 选项,含 topic(覆盖缺省调用主题)与 timeout_ms(缺省 30000)
153
- * @returns {Promise<*>} 客户端反馈的 result;错误响应、送达失败或超时时 reject
154
- * @throws {Error} 插件尚未挂载到服务器时
155
- */
156
- call(client_id, method, params, options = {}) {
157
- if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
158
- const config = this.#config;
159
- const topic = options.topic || `${config.call_prefix}/${client_id}`;
160
- /** 发送函数:把调用请求发布到调用主题 */
161
- const send = (request) => this.#server.publish(topic, JSON.stringify(request), { qos: config.qos, retain: false });
162
- return this.#rpc.call(send, method, params, options);
163
- }
164
-
165
- /**
166
- * 内置 main 钩子逻辑:优先识别客户端反馈,其次执行 RPC 请求并产出应答重发描述
167
- * @param {Object} params 消息参数,含 client_id / topic / payload
168
- * @param {Object} ctx 执行上下文
169
- * @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
170
- * @returns {Promise<Object|undefined>} 重发描述 { topic, payload, qos, retain };反馈、透传与无结果通知时为 undefined
171
- */
172
- async _execute(params, ctx, options) {
173
- if (this.#rpc.acceptResponse(params.payload)) return undefined;
174
- const extra = { client_id: params.client_id, topic: params.topic };
175
- const response = await this.#rpc.handle(params.payload, extra, options);
176
- if (response === undefined) return undefined;
177
- return this._buildReply(params.topic, response);
178
- }
179
-
180
- /**
181
- * 组装应答重发描述
182
- * @param {string} topic 请求主题
183
- * @param {Object} response 响应对象
184
- * @returns {Object} 重发描述,payload 为响应 JSON 字符串
185
- */
186
- _buildReply(topic, response) {
187
- const config = this.#config;
188
- return {
189
- topic: `${topic}${config.reply_suffix}`,
190
- payload: JSON.stringify(response),
191
- qos: config.qos,
192
- retain: config.retain
193
- };
194
- }
195
- }
196
-
197
- module.exports = MessagePlugin;