mm_mqtt_server 1.0.1 → 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,9 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  const MqttServer = require('./lib/mqtt_server');
4
- const MessagePlugin = require('./lib/message');
4
+ const Admin = require('mm_corejs').Admin;
5
+
6
+ const admin = new Admin();
7
+ admin.setClass(MqttServer);
5
8
 
6
9
  module.exports = {
7
10
  MqttServer,
8
- MessagePlugin
11
+ admin,
12
+ /**
13
+ * 获取指定作用域的 MQTT 服务器
14
+ * @param {string} [scope='os'] 作用域名称
15
+ * @param {object} [config] 配置参数
16
+ * @returns {MqttServer} MQTT 服务器实例
17
+ */
18
+ mqttServerAdmin(scope, config) {
19
+ if (!scope) {
20
+ scope = 'os';
21
+ }
22
+ return admin.get(scope, config);
23
+ }
9
24
  };
@@ -2,58 +2,37 @@
2
2
 
3
3
  const net = require('net');
4
4
  const { Aedes } = require('aedes');
5
- const { Mod, Handle, Store } = require('mm_corejs');
5
+ const { Mod } = require('mm_mod');
6
+ const { eventAdmin } = require('mm_event');
6
7
 
7
8
  /**
8
- * 服务器设置校验 schema(校验对象为顶层 config)
9
- */
10
- const SERVER_SCHEMA = {
11
- type: 'object',
12
- properties: {
13
- host: { type: 'string', min_length: 1 },
14
- port: { type: 'integer', minimum: 1, maximum: 65535 },
15
- allow_anonymous: { type: 'boolean' },
16
- username: { type: 'string' },
17
- password: { type: 'string' }
18
- }
19
- };
20
-
21
- /**
22
- * MqttServer —— MQTT 代理服务器(入站端口模块)。
9
+ * MqttServer —— 最薄的 MQTT 代理传输层(入站适配器模块)。
23
10
  *
24
- * 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
25
- * 调用参数校验由 config.params 声明式定义;
26
- * 服务器设置(host / port / allow_anonymous / username / password)为顶层 config 字段,
27
- * 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
28
- *
29
- * 职责:
30
- * 1. 监听:onStart 创建 aedes broker 与 net.Server,按 host / port 监听 MQTT 连接;
31
- * 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
32
- * 3. 消息管道:入站发布消息经 Handle 的 before/check/main/render 阶段处理,
33
- * 内置 main 钩子发 message 事件保持兼容;管道产出 { topic, payload } 时
34
- * 由代理重新发布该消息;getHandle() 可注册自定义钩子(如主题黑名单);
35
- * 4. 事件分发:connection / message / subscribe / unsubscribe / close / error;
36
- * 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 自行处理。
37
17
  *
38
18
  * 用法:
39
- * const mqtt = new MqttServer({ name: 'mqtt_server', host: '127.0.0.1', port: 1883 }, server);
40
- * mqtt.getHandle().on('check', (ctx, params) => {
41
- * if (params.topic.startsWith('blocked/')) return { dropped: true };
42
- * }, 100, 'guard');
43
- * 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
+ * });
44
24
  * await mqtt.init();
45
25
  * await mqtt.start();
46
- * await mqtt.run({}, { action: 'publish', topic: 'mm/test', payload: 'hello' });
47
26
  */
48
27
  class MqttServer extends Mod {
49
28
  /**
50
29
  * 模块默认配置
51
30
  */
52
31
  static config = {
53
- name: 'mqtt_server',
32
+ name: 'os',
54
33
  title: 'MQTT代理服务器模块',
55
- description: '基于 aedes 的 MQTT 代理:管理客户端连接、主题订阅与消息路由',
56
- version: '1.0.0',
34
+ description: 'MQTT 传输层:消息直达事件管理器,不做路由与插件',
35
+ version: '2.0.0',
57
36
  host: '0.0.0.0',
58
37
  port: 1883,
59
38
  allow_anonymous: true,
@@ -64,13 +43,12 @@ class MqttServer extends Mod {
64
43
  type: 'object',
65
44
  required: ['action'],
66
45
  properties: {
67
- action: { type: 'string', enum: ['publish', 'close_client', 'list_clients'] },
46
+ action: { type: 'string', enum: ['publish', 'close', 'list'] },
68
47
  client_id: { type: 'string', min_length: 1 },
69
48
  topic: { type: 'string', min_length: 1 },
70
49
  payload: { type: 'string' },
71
50
  qos: { type: 'integer', minimum: 0, maximum: 2 },
72
- retain: { type: 'boolean' },
73
- query: { type: 'object' }
51
+ retain: { type: 'boolean' }
74
52
  }
75
53
  }
76
54
  };
@@ -86,26 +64,29 @@ class MqttServer extends Mod {
86
64
  #server = null;
87
65
 
88
66
  /**
89
- * 连接注册表:Store 存储,主键 id、名称字段 name
67
+ * 事件管理器(取自 mm_event 的 mqtt 作用域,与其他实例共享)
90
68
  */
91
- #clients = new Store({ key: 'id', name: 'name' });
69
+ #events = null;
92
70
 
93
71
  /**
94
- * 消息处理管道(before/check/main/render/error/success/after)
72
+ * 在线连接表:client_id -> 连接信息
95
73
  */
96
- #handle = new Handle();
74
+ #clients = new Map();
97
75
 
98
- // ---------- 生命周期钩子 ----------
99
76
  /**
100
- * 初始化钩子:校验服务器设置并装配消息管道
77
+ * 构造函数
78
+ * @param {Object} config 模块配置
79
+ * @param {Object} server 框架实例
101
80
  */
102
- async onInit() {
103
- await this._validateConfig();
104
- this._initHandle();
81
+ constructor(config, server) {
82
+ super(config, server);
83
+ this.#events = eventAdmin('mqtt');
105
84
  }
106
85
 
86
+ // ---------- 生命周期钩子 ----------
107
87
  /**
108
88
  * 启动钩子:创建 broker 与 TCP 服务器并开始监听
89
+ * @returns {Promise<void>} 无返回值
109
90
  */
110
91
  async onStart() {
111
92
  const broker = await this._createBroker();
@@ -118,7 +99,8 @@ class MqttServer extends Mod {
118
99
  }
119
100
 
120
101
  /**
121
- * 停止钩子:断开全部客户端,关闭 TCP 服务器与 broker
102
+ * 停止钩子:断开全部连接、关闭服务器与 broker(事件管理器与其他实例共享,不清理)
103
+ * @returns {Promise<void>} 无返回值
122
104
  */
123
105
  async onStop() {
124
106
  this._closeAllClients();
@@ -126,26 +108,19 @@ class MqttServer extends Mod {
126
108
  await this._closeBroker();
127
109
  }
128
110
 
129
- /**
130
- * 释放钩子:清空连接注册表
131
- */
132
- async onDispose() {
133
- this.#clients.clear();
134
- }
135
-
136
111
  // ---------- 执行入口 ----------
137
112
  /**
138
113
  * 模块主逻辑:按 action 派发操作
139
- * @param {Object} ctx 执行上下文
140
114
  * @param {Object} params 调用参数,含 action 及操作所需字段
115
+ * @param {Object} ctx 执行上下文
141
116
  * @param {Object} options 合并后的选项
142
117
  * @returns {Promise<Object>} 操作结果
143
118
  */
144
- async main(ctx, params, options) {
119
+ async main(params, ctx, options) {
145
120
  const action = params.action;
146
121
  if (action === 'publish') return this._actionPublish(params);
147
- if (action === 'close_client') return this._actionCloseClient(params);
148
- return this._actionListClients(params);
122
+ if (action === 'close') return this._actionClose(params);
123
+ return this._actionList();
149
124
  }
150
125
 
151
126
  /**
@@ -164,19 +139,50 @@ class MqttServer extends Mod {
164
139
  * @param {Object} params 调用参数,含 client_id
165
140
  * @returns {Object} 操作结果
166
141
  */
167
- _actionCloseClient(params) {
142
+ _actionClose(params) {
168
143
  this._requireClientId(params);
169
144
  const success = this.closeClient(params.client_id);
170
- return { action: 'close_client', client_id: params.client_id, success };
145
+ return { action: 'close', client_id: params.client_id, success };
171
146
  }
172
147
 
173
148
  /**
174
- * 执行列出客户端操作
175
- * @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
149
+ * 执行列出连接操作
176
150
  * @returns {Object} 操作结果,clients 为在线客户端列表
177
151
  */
178
- _actionListClients(params) {
179
- 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;
180
186
  }
181
187
 
182
188
  // ---------- 公开操作 ----------
@@ -184,7 +190,7 @@ class MqttServer extends Mod {
184
190
  * 代理端发布消息到指定主题
185
191
  * @param {string} topic 主题
186
192
  * @param {string|Buffer} payload 消息内容
187
- * @param {Object} opts 发布选项,含 qos 与 retain
193
+ * @param {Object} [opts] 发布选项,含 qos 与 retain
188
194
  * @returns {Promise<boolean>} 是否发布成功
189
195
  */
190
196
  publish(topic, payload, opts = {}) {
@@ -203,48 +209,27 @@ class MqttServer extends Mod {
203
209
  }
204
210
 
205
211
  /**
206
- * 关闭指定客户端连接
212
+ * 关闭指定客户端连接(连接表由 broker 的断开回调清理)
207
213
  * @param {string} client_id 客户端 ID
208
214
  * @returns {boolean} 是否发起关闭
209
215
  */
210
216
  closeClient(client_id) {
211
- const entry = this.#clients.getById(client_id);
212
- if (!entry) return false;
213
- entry.client.close();
217
+ const client = this.#clients.get(client_id);
218
+ if (!client) return false;
219
+ client.client.close();
214
220
  return true;
215
221
  }
216
222
 
217
223
  /**
218
- * 列出在线客户端(支持 Store 条件查询与分页)
219
- * @param {Object} [query] 查询条件,缺省时列出全部
224
+ * 列出在线客户端
220
225
  * @returns {Array<Object>} 客户端信息列表,含 id 与 address
221
226
  */
222
- listClients(query) {
223
- return this.#clients.list(query).map((entry) => ({ id: entry.id, address: entry.address }));
224
- }
225
-
226
- /**
227
- * 绑定连接级选项(如 user_id / device_id / agent_id),入站消息将作为管道 options 透传给钩子与 RPC 处理函数
228
- * @param {string} client_id 客户端 ID
229
- * @param {Object|null} options 选项对象,null 时清空
230
- * @returns {boolean} 是否设置成功(客户端不在线时失败)
231
- */
232
- setClientOptions(client_id, options) {
233
- const entry = this.#clients.getById(client_id);
234
- if (!entry) return false;
235
- entry.options = options || null;
236
- return true;
237
- }
238
-
239
- /**
240
- * 读取连接级选项,未绑定时返回空对象
241
- * @param {string} client_id 客户端 ID
242
- * @returns {Object} 连接级选项
243
- */
244
- _clientOptions(client_id) {
245
- const entry = this.#clients.getById(client_id);
246
- if (!entry || !entry.options) return {};
247
- 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;
248
233
  }
249
234
 
250
235
  /**
@@ -253,40 +238,31 @@ class MqttServer extends Mod {
253
238
  * @returns {boolean} 是否在线
254
239
  */
255
240
  hasClient(client_id) {
256
- return !!this.#clients.getById(client_id);
241
+ return this.#clients.has(client_id);
257
242
  }
258
243
 
259
244
  /**
260
- * 挂载插件:调用 plugin.bind(this),由插件自行向管道注册钩子;
261
- * 如 MessagePlugin 挂载后接管 JSON-RPC 请求并应答到请求主题 + 后缀
262
- * @param {Object} plugin 具备 bind(server) 方法的插件实例
263
- * @returns {MqttServer} 当前实例
264
- * @throws {TypeError} plugin 不具备 bind 方法时
265
- */
266
- use(plugin) {
267
- if (!plugin || typeof plugin.bind !== 'function') {
268
- throw new TypeError(`${this._label()} plugin must provide bind()`);
269
- }
270
- plugin.bind(this);
271
- return this;
272
- }
273
-
274
- /**
275
- * 获取消息处理管道,供外部注册 before/check/main 等钩子;
276
- * 钩子产出 { topic, payload } 时由代理重新发布该消息
277
- * @returns {Handle} 消息管道实例
245
+ * 绑定连接级选项,入站消息将作为 options 透传给事件
246
+ * @param {string} client_id 客户端 ID
247
+ * @param {Object|null} options 选项对象,null 时清空
248
+ * @returns {boolean} 是否设置成功(客户端不在线时失败)
278
249
  */
279
- getHandle() {
280
- 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;
281
255
  }
282
256
 
283
257
  /**
284
- * 获取实际监听地址
285
- * @returns {Object|null} 监听地址信息,未启动时为 null
258
+ * 获取连接级选项(复制一份,避免事件管理器回写)
259
+ * @param {string} client_id 客户端 ID
260
+ * @returns {Object} 选项对象
286
261
  */
287
- getAddress() {
288
- const server = this.#server;
289
- 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 };
290
266
  }
291
267
 
292
268
  // ---------- broker 装配 ----------
@@ -295,50 +271,44 @@ class MqttServer extends Mod {
295
271
  * @returns {Promise<Object>} broker 实例
296
272
  */
297
273
  async _createBroker() {
298
- const broker = await Aedes.createBroker({
274
+ return Aedes.createBroker({
275
+ ...this.config.options,
299
276
  authenticate: (client, username, password, done) => {
300
277
  this._authenticate(client, username, password, done);
301
278
  }
302
279
  });
303
- return broker;
304
280
  }
305
281
 
306
282
  /**
307
- * 把 broker 事件映射为模块事件,并维护连接注册表
283
+ * 绑定 broker 事件:维护连接表并把入站发布转交事件管理器
308
284
  * @param {Object} broker aedes broker 实例
309
285
  */
310
286
  _bindBrokerEvents(broker) {
311
- broker.on('client', (client) => {
312
- this.#clients.delById(client.id);
313
- this.#clients.add({
314
- id: client.id,
315
- name: client.id,
316
- address: this._remoteAddress(client),
317
- client
318
- });
319
- this._safeEmit('connection', this._clientInfo(client));
320
- });
321
- broker.on('clientDisconnect', (client) => {
322
- const existed = this.#clients.delById(client.id);
323
- if (existed) this._safeEmit('close', client.id);
324
- });
287
+ broker.on('client', (client) => this._registerClient(client));
288
+ broker.on('clientDisconnect', (client) => this.#clients.delete(client.id));
325
289
  broker.on('publish', (packet, client) => {
326
290
  if (!client) return;
327
- this._handlePublish(client, packet).catch((err) => this._emitError(err));
328
- });
329
- broker.on('subscribe', (subscriptions, client) => {
330
- const topics = subscriptions.map((item) => item.topic);
331
- this._safeEmit('subscribe', client.id, topics);
332
- });
333
- broker.on('unsubscribe', (topics, client) => {
334
- this._safeEmit('unsubscribe', client.id, topics);
291
+ this._handlePublish(client, packet);
335
292
  });
336
293
  broker.on('clientError', (client, err) => this._emitError(err));
337
294
  broker.on('connectionError', (client, err) => this._emitError(err));
338
295
  }
339
296
 
340
297
  /**
341
- * 入站发布消息经管道处理:错误上抛,产出 { 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
+ * 处理入站发布消息:交给事件管理器,返回发布描述时由代理重发
342
312
  * @param {Object} client aedes client 实例
343
313
  * @param {Object} packet aedes 发布报文
344
314
  * @returns {Promise<void>} 无返回值
@@ -351,17 +321,31 @@ class MqttServer extends Mod {
351
321
  qos: packet.qos,
352
322
  retain: packet.retain
353
323
  };
354
- const ctx = { client_id: client.id };
355
- const done = await this.#handle.run(ctx, params, this._clientOptions(client.id));
356
- if (done.error) this._emitError(done.error);
357
- if (this._isRepublish(done.result)) {
358
- 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);
359
343
  }
360
344
  }
361
345
 
362
346
  /**
363
- * 判断管道产出是否为待重发的发布描述(须含 topic 与 payload)
364
- * @param {*} result 管道产出的结果
347
+ * 判断事件结果是否为待重发的发布描述(须含 topic 与 payload)
348
+ * @param {*} result 事件结果
365
349
  * @returns {boolean} 是否为发布描述
366
350
  */
367
351
  _isRepublish(result) {
@@ -398,8 +382,7 @@ class MqttServer extends Mod {
398
382
  * @returns {boolean} 是否放行
399
383
  */
400
384
  _checkAnonymous() {
401
- const anonymous = this.config.allow_anonymous && !this.config.username;
402
- return anonymous;
385
+ return !!(this.config.allow_anonymous && !this.config.username);
403
386
  }
404
387
 
405
388
  /**
@@ -437,24 +420,12 @@ class MqttServer extends Mod {
437
420
  return {
438
421
  cmd: 'publish',
439
422
  topic,
440
- payload: Buffer.from(String(payload)),
423
+ payload: Buffer.isBuffer(payload) ? payload : Buffer.from(String(payload)),
441
424
  qos: opts.qos || 0,
442
425
  retain: opts.retain || false
443
426
  };
444
427
  }
445
428
 
446
- /**
447
- * 提取客户端信息
448
- * @param {Object} client aedes client 实例
449
- * @returns {Object} 含 id 与 address 的信息
450
- */
451
- _clientInfo(client) {
452
- return {
453
- id: client.id,
454
- address: this._remoteAddress(client)
455
- };
456
- }
457
-
458
429
  /**
459
430
  * 获取远端地址字符串
460
431
  * @param {Object} client aedes client 实例
@@ -485,11 +456,11 @@ class MqttServer extends Mod {
485
456
  }
486
457
 
487
458
  /**
488
- * 断开全部客户端连接并清空注册表
459
+ * 断开全部客户端连接并清空连接表
489
460
  */
490
461
  _closeAllClients() {
491
- for (const entry of this.#clients.list()) {
492
- entry.client.close();
462
+ for (const client of this.#clients.values()) {
463
+ client.client.close();
493
464
  }
494
465
  this.#clients.clear();
495
466
  }
@@ -503,7 +474,7 @@ class MqttServer extends Mod {
503
474
  this.#server = null;
504
475
  if (!server) return Promise.resolve();
505
476
  return new Promise((resolve) => {
506
- server.close(resolve);
477
+ server.close(() => resolve());
507
478
  });
508
479
  }
509
480
 
@@ -516,36 +487,11 @@ class MqttServer extends Mod {
516
487
  this.#broker = null;
517
488
  if (!broker) return Promise.resolve();
518
489
  return new Promise((resolve) => {
519
- broker.close(resolve);
490
+ broker.close(() => resolve());
520
491
  });
521
492
  }
522
493
 
523
- // ---------- 消息管道 ----------
524
- /**
525
- * 装配内置管道钩子:main 发 message 事件保持向后兼容
526
- */
527
- _initHandle() {
528
- this.#handle.on('main', (ctx, params) => {
529
- this._safeEmit('message', params.client_id, {
530
- topic: params.topic,
531
- payload: params.payload,
532
- qos: params.qos,
533
- retain: params.retain
534
- });
535
- }, 1000, 'emit_message');
536
- }
537
-
538
494
  // ---------- 校验辅助 ----------
539
- /**
540
- * 校验顶层 config 中的服务器设置
541
- * @returns {Promise<void>} 无返回值
542
- * @throws {Error} 校验不通过时
543
- */
544
- async _validateConfig() {
545
- const tip = await this._validate(this.config, SERVER_SCHEMA);
546
- if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
547
- }
548
-
549
495
  /**
550
496
  * 断言调用参数含 client_id
551
497
  * @param {Object} params 调用参数
package/package.json CHANGED
@@ -1,25 +1,29 @@
1
1
  {
2
2
  "name": "mm_mqtt_server",
3
- "version": "1.0.1",
4
- "description": "MQTT 代理服务器入站端口模块,基于 aedes,继承 mm_corejs 的 Mod 基类,支持独立运行。",
3
+ "version": "2.0.0",
4
+ "description": "最薄的 MQTT 代理服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
5
  "keywords": [
6
6
  "超级美眉",
7
- "入站端口",
7
+ "入站适配器",
8
8
  "mm_mod",
9
- "mm_in_port",
9
+ "mm_adapter",
10
+ "mm_in_adapter",
10
11
  "mm_receiver",
11
12
  "mqtt",
12
13
  "broker",
13
14
  "aedes",
14
15
  "server"
15
16
  ],
17
+ "layer": "adapter",
18
+ "group": "in_adapter",
16
19
  "license": "ISC",
17
20
  "author": "qww",
18
21
  "type": "commonjs",
19
22
  "main": "index.js",
20
23
  "files": [
21
24
  "index.js",
22
- "lib"
25
+ "lib",
26
+ "example"
23
27
  ],
24
28
  "engines": {
25
29
  "node": ">=22.5.0"
@@ -35,13 +39,15 @@
35
39
  "scripts": {
36
40
  "start": "node example/demo.js",
37
41
  "demo": "node example/demo.js",
38
- "test": "jest --coverage"
42
+ "test": "node --test \"test/*.test.js\""
39
43
  },
40
44
  "dependencies": {
41
45
  "aedes": "^1.2.0",
42
- "mm_corejs": "^1.1.0"
46
+ "mm_corejs": "^1.1.4",
47
+ "mm_event": "^2.2.2",
48
+ "mm_mod": "^1.0.1"
43
49
  },
44
50
  "devDependencies": {
45
- "jest": "^30.5.2"
51
+ "mqtt-packet": "^9.0.2"
46
52
  }
47
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', (ctx, params, options) => {
89
- return this._execute(ctx, params, 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} ctx 执行上下文
168
- * @param {Object} params 消息参数,含 client_id / topic / payload
169
- * @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
170
- * @returns {Promise<Object|undefined>} 重发描述 { topic, payload, qos, retain };反馈、透传与无结果通知时为 undefined
171
- */
172
- async _execute(ctx, params, 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;