@microi.net/cli 4.9.4 → 4.9.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.
Files changed (84) hide show
  1. package/.codebuddy-plugin/marketplace.json +2 -2
  2. package/.codebuddy-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.workbuddy-plugin/marketplace.json +2 -2
  5. package/.workbuddy-plugin/plugin.json +1 -1
  6. package/README.md +7 -12
  7. package/assets/build-meta.json +5 -5
  8. package/package.json +1 -1
  9. package/scripts/mcp-server.js +92 -92
  10. package/scripts/microi-cli.js +78 -60
  11. package/scripts/microi-codex-router.js +30 -0
  12. package/scripts/microi-skills.meta.json +196 -190
  13. package/skills/.microi-skills-version.json +2 -2
  14. package/skills/README.md +1 -1
  15. package/skills/ai-engine/SKILL.md +1 -1
  16. package/skills/ai-platform-governance/SKILL.md +1 -1
  17. package/skills/app-store/SKILL.md +1 -1
  18. package/skills/business-blueprint/SKILL.md +1 -1
  19. package/skills/datasource-engine/SKILL.md +1 -1
  20. package/skills/dos-orm/SKILL.md +1 -1
  21. package/skills/job-engine/SKILL.md +1 -1
  22. package/skills/message-notification/SKILL.md +1 -1
  23. package/skills/microi-ai-application/SKILL.md +1 -1
  24. package/skills/microi-client-frontend/SKILL.md +1 -1
  25. package/skills/microi-codex/SKILL.md +4 -4
  26. package/skills/microi-codex-installer/SKILL.md +25 -36
  27. package/skills/microi-datasource-mapping/SKILL.md +1 -1
  28. package/skills/microi-db-schema/SKILL.md +1 -1
  29. package/skills/microi-deployment/SKILL.md +1 -1
  30. package/skills/microi-docs-coverage/SKILL.md +1 -1
  31. package/skills/microi-docs-coverage/references/capability-map.md +3 -2
  32. package/skills/microi-form-engine/SKILL.md +7 -4
  33. package/skills/microi-form-layout/SKILL.md +19 -6
  34. package/skills/microi-frontend-sdk/SKILL.md +1 -1
  35. package/skills/microi-left-right-layout/SKILL.md +1 -1
  36. package/skills/microi-microservice/SKILL.md +8 -1
  37. package/skills/microi-microservice/references/runtime-delivery.md +4 -0
  38. package/skills/microi-mobile-app-quality/SKILL.md +1 -1
  39. package/skills/microi-solution-quotation/SKILL.md +1 -1
  40. package/skills/microi-system-delivery/SKILL.md +6 -3
  41. package/skills/microi-ui/SKILL.md +1 -1
  42. package/skills/microi-uniapp-frontend/SKILL.md +1 -1
  43. package/skills/module-engine/SKILL.md +1 -1
  44. package/skills/ocr-engine/SKILL.md +1 -1
  45. package/skills/page-engine/SKILL.md +1 -1
  46. package/skills/performance-testing/SKILL.md +1 -1
  47. package/skills/playwright-e2e/SKILL.md +1 -1
  48. package/skills/print-engine/SKILL.md +6 -5
  49. package/skills/production-readonly-audit/SKILL.md +1 -1
  50. package/skills/report-engine/SKILL.md +1 -1
  51. package/skills/search-engine/SKILL.md +1 -1
  52. package/skills/spider-engine/SKILL.md +1 -1
  53. package/skills/translate-engine/SKILL.md +1 -1
  54. package/skills/ui-design/SKILL.md +1 -1
  55. package/skills/uniapp-mall-assets/SKILL.md +1 -1
  56. package/skills/unity-integration/SKILL.md +9 -1
  57. package/skills/unity-integration/references/ai-app-delivery.md +26 -6
  58. package/skills/v8-api-config/SKILL.md +1 -1
  59. package/skills/v8-cache-pattern/SKILL.md +1 -1
  60. package/skills/v8-crud-api/SKILL.md +1 -1
  61. package/skills/v8-debugging/SKILL.md +1 -1
  62. package/skills/v8-explorer-tree/SKILL.md +1 -1
  63. package/skills/v8-export-import/SKILL.md +1 -1
  64. package/skills/v8-file-upload/SKILL.md +1 -1
  65. package/skills/v8-formengine-http/SKILL.md +1 -1
  66. package/skills/v8-frontend-events/SKILL.md +12 -8
  67. package/skills/v8-frontend-events/references/bluetooth-print-api.md +31 -3
  68. package/skills/v8-frontend-events/references/bluetooth-print.md +54 -12
  69. package/skills/v8-http-integration/SKILL.md +1 -1
  70. package/skills/v8-image-processing/SKILL.md +1 -1
  71. package/skills/v8-menu-buttons/SKILL.md +1 -1
  72. package/skills/v8-mongodb/SKILL.md +1 -1
  73. package/skills/v8-mq-mqtt/SKILL.md +141 -53
  74. package/skills/v8-mq-mqtt/references/mqtt-production.md +341 -0
  75. package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +205 -0
  76. package/skills/v8-saas-multi-tenant/SKILL.md +1 -1
  77. package/skills/v8-security/SKILL.md +1 -1
  78. package/skills/v8-sql-query/SKILL.md +1 -1
  79. package/skills/v8-table-event/SKILL.md +1 -1
  80. package/skills/v8-template-engine/SKILL.md +1 -1
  81. package/skills/v8-utilities/SKILL.md +1 -1
  82. package/skills/v8-utilities/references/client-api-index.md +4 -2
  83. package/skills/v8-workflow/SKILL.md +1 -1
  84. package/skills/workspace-conventions/SKILL.md +8 -9
@@ -3,7 +3,7 @@ name: v8-menu-buttons
3
3
  description: Microi 菜单按钮与 Tab V8 指南。用于配置 sys_menu MoreBtns、FormBtns、BatchSelectMoreBtns、PageTabs、PageBtns、ExportMoreBtns、显隐代码和行操作。
4
4
  ---
5
5
 
6
- > **Codex 强制前置:** 当前宿主为 Codex 时,在使用本 Skill 前必须先完整读取 `../microi-codex-installer/SKILL.md`,完成“Codex 每任务最新版硬门禁”;门禁未通过不得继续本 Skill。非 Codex 宿主跳过此项。
6
+ > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
7
 
8
8
  # v8-menu-buttons — 菜单按钮 / Tab / 批量操作 V8 写法
9
9
 
@@ -3,7 +3,7 @@ name: v8-mongodb
3
3
  description: Microi V8 MongoDB 指南。用于使用 V8.MongoDb AddFormData、UptFormData、DelFormData、GetFormData、GetTableData、对象过滤和文档 Id。
4
4
  ---
5
5
 
6
- > **Codex 强制前置:** 当前宿主为 Codex 时,在使用本 Skill 前必须先完整读取 `../microi-codex-installer/SKILL.md`,完成“Codex 每任务最新版硬门禁”;门禁未通过不得继续本 Skill。非 Codex 宿主跳过此项。
6
+ > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
7
 
8
8
  # Microi V8 MongoDB 操作
9
9
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: v8-mq-mqtt
3
- description: Microi V8 消息队列与 MQTT 指南。用于使用 V8.MQ.SendMsg、RabbitMQ 队列、MQTT 事件处理、主题、载荷、客户端 Id 和异步集成。
3
+ description: Microi V8 消息队列与 MQTT 生产指南。用于 V8.MQ.SendMsg、RabbitMQ 消费与幂等,以及内嵌 MQTT Broker、SaaS 租户认证、Topic ACL、TLS、QoS/Retain、七类 V8.MQTT 事件、设备级接口引擎、服务端下行、IoT 数据分层和多节点部署验收。
4
4
  ---
5
5
 
6
- > **Codex 强制前置:** 当前宿主为 Codex 时,在使用本 Skill 前必须先完整读取 `../microi-codex-installer/SKILL.md`,完成“Codex 每任务最新版硬门禁”;门禁未通过不得继续本 Skill。非 Codex 宿主跳过此项。
6
+ > **Codex 非阻塞自动更新:** 当前宿主为 Codex 时,吾码 CLI、Codex 插件与工作区 AI/MCP 由后台自动更新;需要诊断时读取 `../microi-codex-installer/SKILL.md`。更新失败、等待空闲或尚未重载均不得阻断当前、正在进行或新建任务。非 Codex 宿主跳过此项。
7
7
 
8
8
  # Microi V8 消息队列与 MQTT
9
9
 
@@ -129,29 +129,59 @@ try {
129
129
 
130
130
  ---
131
131
 
132
- ## V8.MQTT — IoT 物联网
133
-
134
- ### MQTT 事件类型
132
+ ## V8.MQTT — IoT 物联网
133
+
134
+ 先区分两个边界:MQTT 是设备实时双向通道,RabbitMQ 是服务端异步队列;
135
+ RS-485、ZigBee、BLE、Modbus 等现场协议需先由网关转换为 MQTT。涉及 MQTT
136
+ 配置、安全、设备级路由、生产部署或故障排查时,必须继续读取
137
+ [MQTT 生产参考](references/mqtt-production.md),不要只凭下面的快速示例上线。
138
+
139
+ ### 快速实施顺序
140
+
141
+ 1. 先确定拓扑:单节点/独立 MQTT 节点可用内嵌 Broker;多 API 节点不要把
142
+ 各节点的会话、订阅和 Retained Message 误认为一个集群。
143
+ 2. 在 SaaS 引擎为主租户启用监听,并为每个接入租户配置独立完整的
144
+ `MqttAccount`、`MqttPwd` 与 `MqttApiEngine`。
145
+ 3. 让设备携带 MQTT v5 `OsClient`,或使用 `<OsClient>:<账号>` 用户名、
146
+ `<OsClient>:<设备Id>` ClientId;多个来源同时存在时必须指向同一租户。
147
+ 4. 在接口引擎按 `V8.EventName` 路由,并只读取 Broker 已校验的 `V8.MQTT`;
148
+ 不要从 Payload 重新信任租户、Topic 或设备身份。
149
+ 5. 用真实客户端验证 TCP/TLS、错误凭据、跨租户 Topic、QoS、Retain、快速重连、
150
+ V8 拒绝、重复消息和节点重启,不能用静态检查代替 Broker/硬件验收。
151
+
152
+ 主租户运行时读取 `MqttPort`(默认 `1883`)以及可选 TLS 配置;子租户自己的
153
+ 端口不会再启动一套 Broker。`MqttWsPort` 是保留元数据,当前内嵌 Broker 没有
154
+ 启用 WebSocket 监听。
155
+
156
+ ### MQTT 事件类型
135
157
 
136
158
  MQTT 通过一个接口引擎处理所有事件,通过 `V8.EventName` 判断当前事件类型:
137
159
 
138
160
  | V8.EventName | 说明 |
139
161
  |---|---|
140
- | `StartServer` | MQTT 服务启动 |
141
- | `Connected` | 客户端连接 |
142
- | `Disconnected` | 客户端断开连接 |
143
- | `MessageReceived` | 收到客户端消息 |
144
- | `StopServer` | MQTT 服务停止 |
162
+ | `StartServer` | MQTT 服务启动 |
163
+ | `Connected` | 客户端连接 |
164
+ | `Disconnected` | 客户端断开连接 |
165
+ | `Subscribing` | Topic 通过 Broker ACL 后发生订阅;用于观察与审计,不承担拒绝语义 |
166
+ | `MessageReceived` | 收到客户端消息 |
167
+ | `MessageChanged` | Retained Message 发生变化 |
168
+ | `StopServer` | MQTT 服务停止 |
145
169
 
146
170
  ### V8.MQTT 上下文
147
171
 
148
- | 属性 | 说明 |
149
- |---|---|
150
- | `V8.MQTT.ClientId` | 客户端 Id |
151
- | `V8.MQTT.Topic` | 消息主题 |
152
- | `V8.MQTT.Payload` | 消息内容(在 MessageReceived 事件中) |
172
+ | 属性 | 说明 |
173
+ |---|---|
174
+ | `V8.MQTT.ClientId` | 客户端 Id |
175
+ | `V8.MQTT.OsClient` | Broker 已校验的租户标识 |
176
+ | `V8.MQTT.Topic` | 规范化后的完整 Topic |
177
+ | `V8.MQTT.Payload` | JSON 自动解析后的对象,或解析失败时的字符串 |
178
+ | `V8.MQTT.PayloadRaw` | 原始 UTF-8 Payload 文本 |
179
+ | `V8.MQTT.UserName` | 连接事件中的客户端用户名 |
180
+ | `V8.MQTT.Qos` | QoS:`0`、`1` 或 `2` |
181
+ | `V8.MQTT.Retain` | 是否为 Retained Message |
182
+ | `V8.MQTT.UserProperties` | MQTT v5 User Properties;没有时为空 |
153
183
 
154
- 子租户必须 `MqttEnable=1` 并配置独立 `MqttAccount/MqttPwd`。子租户不能通过 `MqttAllowAnonymous=1` 或 `MqttTopicIsolation=0` 关闭边界;缺少完整凭据时拒绝连接。Topic 统一为 `tenant/{lowerOsClient}/{businessTopic}`,服务端 publish、subscribe、retained、ResponseTopic 都会校验并拒绝其它租户、系统 Topic 和共享订阅绕过。
184
+ 子租户必须 `MqttEnable=1` 并配置独立 `MqttAccount/MqttPwd`。子租户不能通过 `MqttAllowAnonymous=1` 或 `MqttTopicIsolation=0` 关闭边界;缺少完整凭据时拒绝连接。Topic 统一为 `tenant/{lowerOsClient}/{businessTopic}`,服务端 publish、subscribe、retained、ResponseTopic 都会校验并拒绝其它租户、`$SYS` 系统 Topic 和 `$share` 共享订阅绕过。
155
185
 
156
186
  ### 完整示例
157
187
 
@@ -170,60 +200,118 @@ if (eventName === 'StartServer') {
170
200
  LastOnlineTime: DateNow('yyyy-MM-dd HH:mm:ss')
171
201
  });
172
202
 
173
- } else if (eventName === 'Disconnected') {
174
- console.log('设备已断开: ' + V8.MQTT.ClientId);
203
+ } else if (eventName === 'Disconnected') {
204
+ console.log('设备已断开: ' + V8.MQTT.ClientId);
175
205
  V8.FormEngine.UptFormDataByWhere('Device', {
176
206
  _Where: [['DeviceCode', '=', V8.MQTT.ClientId]],
177
207
  OnlineStatus: 0,
178
- LastOfflineTime: DateNow('yyyy-MM-dd HH:mm:ss')
179
- });
180
-
181
- } else if (eventName === 'MessageReceived') {
182
- // 处理设备上报的数据
183
- var clientId = V8.MQTT.ClientId;
184
- var topic = V8.MQTT.Topic;
185
- var payload = V8.MQTT.Payload;
186
-
187
- console.log('收到消息: ' + clientId + ' - ' + topic);
208
+ LastOfflineTime: DateNow('yyyy-MM-dd HH:mm:ss')
209
+ });
210
+
211
+ } else if (eventName === 'Subscribing') {
212
+ console.log('设备订阅: ' + V8.MQTT.ClientId + ' -> ' + V8.MQTT.Topic);
213
+
214
+ } else if (eventName === 'MessageReceived') {
215
+ // 处理设备上报的数据
216
+ var clientId = V8.MQTT.ClientId;
217
+ var topic = V8.MQTT.Topic;
218
+ var payload = V8.MQTT.Payload;
219
+
220
+ if (typeof payload === 'string') {
221
+ try {
222
+ payload = JSON.parse(payload);
223
+ } catch (ex) {
224
+ return { Code: 0, Msg: 'Payload 必须是合法 JSON。' };
225
+ }
226
+ }
227
+ if (!payload || !payload.eventId) {
228
+ return { Code: 0, Msg: '缺少稳定的 eventId。' };
229
+ }
230
+
231
+ console.log('收到消息: ' + clientId + ' - ' + topic);
188
232
 
189
233
  // 存储到 MongoDB(适合海量数据)
190
234
  V8.MongoDb.AddFormData({
191
235
  DbName: 'iot_data',
192
236
  TableName: 'device_msg_' + DateNow('yyyy_MM'),
193
237
  _FormData: {
194
- DeviceId: clientId,
195
- Topic: topic,
196
- Payload: payload,
197
- CreateTime: DateNow('yyyy-MM-dd HH:mm:ss')
198
- }
238
+ DeviceId: clientId,
239
+ EventId: payload.eventId,
240
+ Topic: topic,
241
+ Payload: payload,
242
+ PayloadRaw: V8.MQTT.PayloadRaw,
243
+ Qos: V8.MQTT.Qos,
244
+ Retain: V8.MQTT.Retain,
245
+ CreateTime: DateNow('yyyy-MM-dd HH:mm:ss')
246
+ }
199
247
  });
200
248
 
201
249
  // 解析特定主题的数据
202
250
  var temperatureTopic = 'tenant/' + V8.OsClient.toLowerCase() + '/sensor/temperature';
203
251
  if (topic === temperatureTopic) {
204
- var temp = parseFloat(payload);
205
- if (temp > 80) {
206
- // 温度报警
207
- V8.ApiEngine.Run('send-alarm', {
208
- deviceId: clientId,
209
- type: 'temperature',
210
- value: temp
211
- });
212
- }
213
- }
214
-
215
- } else if (eventName === 'StopServer') {
216
- console.log('MQTT 服务已停止');
217
- }
218
- ```
219
-
220
- ## 注意事项
252
+ var temp = Number(payload.temperature);
253
+ if (isNaN(temp)) return { Code: 0, Msg: 'temperature 必须是数字。' };
254
+ if (temp > 80) {
255
+ // 温度报警
256
+ V8.ApiEngine.Run('send-alarm', {
257
+ eventId: payload.eventId,
258
+ deviceId: clientId,
259
+ type: 'temperature',
260
+ value: temp
261
+ });
262
+ }
263
+ }
264
+
265
+ return { Code: 1 };
266
+
267
+ } else if (eventName === 'MessageChanged') {
268
+ console.log('Retained Message 已变化: ' + V8.MQTT.Topic);
269
+
270
+ } else if (eventName === 'StopServer') {
271
+ console.log('MQTT 服务已停止');
272
+ }
273
+ ```
274
+
275
+ `MessageReceived` 中只有显式 `Code != 1` 会阻止向订阅者广播;无返回值、普通
276
+ 字符串/数字、没有 `Code` 的对象和 `Code: 1` 保持兼容放行。已配置事件引擎但
277
+ 执行异常时失败关闭。其它事件的返回值不改变连接、订阅或生命周期结果。
278
+
279
+ 平台在 V8 前写入接收日志,因此被规则拒绝的消息仍可审计。业务副作用仍必须以
280
+ 设备提供的稳定 `EventId` 配合唯一约束、inbox/outbox 或条件更新实现幂等;
281
+ MQTT QoS、Retain 和连接锁都不等于业务“恰好一次”。
282
+
283
+ ### 设备、下行与部署边界
284
+
285
+ - 平台自动维护 `mci_mqtt_client` 与 `mci_mqtt_log`;前者支持设备级
286
+ `ApiEngineId` 覆盖租户 `MqttApiEngine`,修改后让设备重新连接刷新当前节点缓存。
287
+ - 可信 C# 后端只使用 `IMicroiMQTT.PublishAsync(osClient, ...)` 下行;缺少租户
288
+ 上下文的旧重载会拒绝。`V8.MQTT` 当前是事件上下文,不是通用 V8 发布函数。
289
+ - 同一 ClientId 快速重连时,旧会话的延迟断开会记录为 `StaleDisconnectIgnored`,
290
+ 不会把已接管的新会话误标为离线。
291
+ - `ConnectedClients` / `GetConnectedClients(osClient)` 和管理状态接口只表示当前
292
+ MQTT 节点快照,不能作为集群全局在线事实。
293
+ - 内嵌 Broker 的会话、订阅、Retained Message 不跨 API 节点共享。多节点生产
294
+ 使用独立 MQTT 节点,或外部集群 Broker + 租户感知适配器,并保持业务幂等。
295
+
296
+ ### 变更后的覆盖检查
297
+
298
+ 修改 MQTT 运行时、官网文档或本 Skill 后运行:
299
+
300
+ ```powershell
301
+ node microi.skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs
302
+ ```
303
+
304
+ 该检查只证明源码中的事件/上下文字段和关键安全能力已进入文档与 Skill;它不能
305
+ 证明 Broker、网络、证书、外部集群、真实设备或吞吐已经通过验收。
306
+
307
+ ## 注意事项
221
308
 
222
309
  - MQ 消费者接口引擎通过 `V8.Param.Message` 获取消息,包含 `EventId`、兼容 `Id`、`OsClient`、`Message`、`CurrentUserId`
223
310
  - MQ 适合异步解耦、削峰填谷、耗时操作异步化
224
- - MQTT 所有事件在同一个接口引擎中处理,通过 `V8.EventName` 区分
311
+ - MQTT 七类事件在同一个接口引擎中处理,通过 `V8.EventName` 区分
225
312
  - MQTT 适合 IoT 设备管理、实时数据采集
226
- - 海量 MQTT 数据建议存入 MongoDB 而非 MySQL
227
- - MQ MQTT 的租户凭据在 SaaS 引擎中管理,但必须先在真实 Broker 创建对应资源,不能只写数据库字段
313
+ - 设备/告警/工单等业务事实优先进入关系库,高频遥测可进入 MongoDB,大附件进入对象存储
314
+ - RabbitMQ 租户凭据在 SaaS 引擎登记前必须先在真实 RabbitMQ 创建 user/vhost/权限;内嵌 MQTT Broker 直接校验 SaaS 中的 MQTT 凭据,使用外部 MQTT Broker 时另行完成真实 Broker 账号、ACL 与适配器配置
228
315
  - `ConnectedClients` 只代表当前 MQTT 节点的诊断快照,不是集群全局在线事实
229
316
  - 内嵌 MQTT Broker 不具备跨节点共享会话/订阅/retained 的集群一致性;多 API 节点生产部署应使用支持集群的外部 Broker,或把内嵌 Broker 固定到独立节点并由负载入口路由,不能让每个 API 节点各自充当一套独立 Broker
317
+ - MQTT 生产配置、安全语义、事件字段可用性和上线清单以 [MQTT 生产参考](references/mqtt-production.md) 为准
@@ -0,0 +1,341 @@
1
+ # Microi MQTT 生产参考
2
+
3
+ 本参考用于设计或审查 Microi MQTT 配置、事件接口引擎、设备路由、服务端下行、
4
+ 安全边界和生产部署。优先以目标版本源码与实际租户元数据为准;旧部署可能尚未
5
+ 具备本文列出的全部字段或行为。
6
+
7
+ ## 目录
8
+
9
+ - [事实源与适用边界](#事实源与适用边界)
10
+ - [接入架构与协议边界](#接入架构与协议边界)
11
+ - [SaaS 配置矩阵](#saas-配置矩阵)
12
+ - [租户识别与连接认证](#租户识别与连接认证)
13
+ - [Topic ACL 与规范化](#topic-acl-与规范化)
14
+ - [事件与字段可用性](#事件与字段可用性)
15
+ - [V8 返回值和失败关闭](#v8-返回值和失败关闭)
16
+ - [设备级接口引擎](#设备级接口引擎)
17
+ - [安全的遥测处理模式](#安全的遥测处理模式)
18
+ - [服务端安全下行](#服务端安全下行)
19
+ - [数据分层与可观测性](#数据分层与可观测性)
20
+ - [单节点与多节点部署](#单节点与多节点部署)
21
+ - [上线验收清单](#上线验收清单)
22
+ - [自动覆盖检查的证据边界](#自动覆盖检查的证据边界)
23
+
24
+ ## 事实源与适用边界
25
+
26
+ 按以下顺序确认当前能力,不要仅凭示例或历史文档推断:
27
+
28
+ 1. `Microi.Server/Microi.Core/Model/MqttParam.cs`:`V8.MQTT` 可用字段。
29
+ 2. `Microi.Server/Microi.MQTT/MicroiMQTT.cs`:Broker、认证、Topic ACL、事件、
30
+ 返回值、设备缓存和日志的真实行为。
31
+ 3. `Microi.Server/Microi.Core/SaaSEngine/TenantConfigurationSecurity.cs`:共享监听
32
+ 配置、租户凭据、Topic 规范化和敏感字段边界。
33
+ 4. `Microi.Server/Microi.Core/Interface/IMicroiMQTT.cs`:
34
+ `IMicroiMQTT.PublishAsync(osClient, ...)` 可信后端发布和节点状态接口。
35
+ 5. `Microi.Server/Microi.net.Api/Controllers/MqttController.cs`:平台管理员、当前节点
36
+ 状态与示例下行入口的权限边界。
37
+ 6. `microi.doc/docs/doc/system-engine/mqtt-engine.md`:面向用户的完整能力说明。
38
+
39
+ 目标服务器可能落后于当前源码。编写事件代码前回读其 `sys_osclients` 字段和运行
40
+ 版本;缺少字段时走官方应用包/版本升级,不要在 V8 中伪造配置。
41
+
42
+ ## 接入架构与协议边界
43
+
44
+ Microi 当前直接处理 MQTT,不直接解析 RS-485、ZigBee、BLE 或 Modbus 帧:
45
+
46
+ ```text
47
+ 现场设备 -> 边缘网关/协议转换 -> MQTT TCP/TLS -> Microi Broker
48
+ -> 租户认证与 Topic ACL -> mci_mqtt_client / mci_mqtt_log
49
+ -> V8.EventName + V8.MQTT -> 业务表、MongoDB、HTTP、告警、工单
50
+ ```
51
+
52
+ 边缘网关负责现场总线、采样与协议解析;Microi 负责租户边界、实时业务规则、数据
53
+ 治理和应用联动。浏览器 MQTT 需要独立的 WebSocket MQTT 网关;当前隐藏元数据
54
+ `MqttWsPort` 不代表内嵌 Broker 已启用 WebSocket。
55
+
56
+ ## SaaS 配置矩阵
57
+
58
+ | 字段 | 作用域 | 当前行为与默认值 | 变更生效 |
59
+ | --- | --- | --- | --- |
60
+ | `MqttEnable` | 每租户 | 主租户为 `1` 才启动 Broker;子租户为 `1` 才允许其连接 | 启停监听需重启 MQTT 节点 |
61
+ | `MqttPort` | 主租户共享 | TCP 监听端口,默认 `1883` | 重启 MQTT 节点 |
62
+ | `MqttUseTls` | 主租户共享 | `1` 时尝试启用 TLS 端点 | 重启 MQTT 节点 |
63
+ | `MqttTlsPort` | 主租户共享 | TLS 端口,默认 `8883` | 重启 MQTT 节点 |
64
+ | `MqttCertPath` | 主租户共享 | 进程/容器内可读的 PFX 路径 | 挂载证书后重启 |
65
+ | `MqttCertPassword` | 主租户共享秘密 | PFX 密码,只允许可信后端读取 | 轮换后重启 |
66
+ | `MqttFallbackPort` | 主租户运行时 | Windows 主端口被拒绝时使用;无有效配置则 `21883` | 重启 MQTT 节点 |
67
+ | `MqttWsPort` | 保留元数据 | 当前未创建 WebSocket 监听 | 不得宣称已生效 |
68
+ | `MqttAccount` | 每租户凭据 | 子租户必须独立、完整,不能与其它租户账号重复 | 新连接使用新值 |
69
+ | `MqttPwd` | 每租户秘密 | 子租户必须独立、完整,不能与其它租户密码重复 | 新连接使用新值 |
70
+ | `MqttApiEngine` | 每租户 | 默认 MQTT 事件接口引擎;兼容历史 GUID/ULID Id 和当前 `ApiEngineKey` | 按接口引擎缓存规则 |
71
+ | `MqttAllowAnonymous` | 仅主租户兼容 | 只有主租户显式为 `1` 才可能匿名;生产不建议 | 新连接使用新值 |
72
+ | `MqttTopicIsolation` | 兼容元数据 | 不能关闭强制租户 Topic ACL,子租户设为 `0` 也不放宽 | 无放宽语义 |
73
+
74
+ 把 PFX 以只读 Secret/持久卷挂载,不把证书密码、MQTT 密码写入代码、URL、日志、
75
+ 前端或普通 V8。运行时读取了某字段不等于所有历史数据库都已经拥有该字段;部署前
76
+ 必须回读目标表结构。
77
+
78
+ 当前 `MqttUseTls=1` 是在默认明文 TCP 端点之外增加 TLS 1.2 端点,不是 TLS-only
79
+ 模式;证书路径无效时会写诊断,但默认 TCP Broker 仍可能启动。要求强制加密时,
80
+ 除验证 TLS 握手外,还要在防火墙/入口层关闭公网明文端口,不能只看 `IsRunning`。
81
+
82
+ ## 租户识别与连接认证
83
+
84
+ 连接验证按以下优先级解析租户:
85
+
86
+ 1. MQTT v5 User Property `OsClient`;
87
+ 2. Username 的 `<OsClient>:<MqttAccount>` 前缀;
88
+ 3. ClientId 的 `<OsClient>:<设备Id>` 前缀;
89
+ 4. 旧主租户客户端仅在 Username 精确等于主租户 `MqttAccount` 时兼容。
90
+
91
+ 同时提供多个来源时必须全部指向同一租户。显式未知租户、未启用租户、空/非法
92
+ ClientId、错误密码和跨租户 ClientId 冲突均失败关闭,不回退主租户。
93
+
94
+ 子租户还必须满足:
95
+
96
+ - 账号与密码都非空;
97
+ - 账号不能与任一其它租户账号相同;
98
+ - 密码不能与任一其它租户密码相同;
99
+ - 不能通过 `MqttAllowAnonymous=1` 或 `MqttTopicIsolation=0` 绕过边界。
100
+
101
+ 凭据使用常量时间字符串比较。不要在 Payload 中传一个 `OsClient` 后自行切换租户;
102
+ 业务代码只信任 `V8.MQTT.OsClient`。
103
+
104
+ 同一 ClientId 快速重连时,每次有效连接持有独立会话令牌。旧连接的延迟
105
+ `Disconnected` 会被记录为 `StaleDisconnectIgnored`,不会删除替代连接的租户映射
106
+ 或错误标记新会话下线。
107
+
108
+ ## Topic ACL 与规范化
109
+
110
+ 业务 Topic 会被收敛为:
111
+
112
+ ```text
113
+ tenant/{lowerOsClient}/{businessTopic}
114
+ ```
115
+
116
+ | 输入 | 结果 |
117
+ | --- | --- |
118
+ | `sensor/temperature` | 自动加当前租户前缀 |
119
+ | `tenant/<当前租户>/sensor/temperature` | 保留并规范化租户大小写 |
120
+ | `<当前租户>/sensor/temperature` | 兼容旧前缀并转为标准格式 |
121
+ | `tenant/<其它租户>/...` | 拒绝 |
122
+ | `$SYS/...`、`$share/...` | 拒绝 |
123
+ | 发布 Topic 包含 `+` 或 `#` | 拒绝 |
124
+ | 订阅 `sensor/+/state` 或 `sensor/#` | 允许合法完整段通配符并加租户前缀 |
125
+ | 包含控制字符、反斜杠、`//`、`.` 或 `..` 路径段 | 拒绝 |
126
+
127
+ 发布、订阅、Retained Message、可信后端下行和 MQTT v5 `ResponseTopic` 都执行同一
128
+ 租户边界。`#` 只能是订阅的最后一个完整段,`+` 只能作为完整段。
129
+
130
+ ## 事件与字段可用性
131
+
132
+ | `V8.EventName` | 触发时机 | 主要字段 | 返回值影响 |
133
+ | --- | --- | --- | --- |
134
+ | `StartServer` | Broker 成功启动后,逐个启用且配置引擎的租户 | `OsClient` | 不改变启动结果 |
135
+ | `Connected` | 认证成功、设备表/日志更新后 | `ClientId`、`OsClient`、`UserName`、`UserProperties` | 不能否决连接 |
136
+ | `Disconnected` | 当前有效会话断开、设备表/日志更新后 | `ClientId`、`OsClient`、`UserProperties` | 不能否决断开 |
137
+ | `Subscribing` | Topic ACL 已通过、订阅日志写入后 | `ClientId`、`OsClient`、`Topic`、`UserProperties` | 不能否决订阅 |
138
+ | `MessageReceived` | 发布 Topic 通过 ACL、接收日志写入后 | `ClientId`、`OsClient`、`Topic`、`Payload`、`PayloadRaw`、`Qos`、`Retain`、`UserProperties` | `Code != 1` 阻止广播 |
139
+ | `MessageChanged` | Retained Message 变化并通过 ACL 后 | `ClientId`、`OsClient`、`Topic`、`Payload`、`PayloadRaw`、`Qos`、`Retain` | 返回值不改变结果 |
140
+ | `StopServer` | Broker 正常停止后,逐个启用且配置引擎的租户 | `OsClient` | 不改变停止结果 |
141
+
142
+ `V8.MQTT` 完整模型:
143
+
144
+ | 字段 | 类型 | 说明 |
145
+ | --- | --- | --- |
146
+ | `ClientId` | `string` | 设备/客户端 Id |
147
+ | `Payload` | `object / string` | JSON 自动反序列化;失败时为原始字符串 |
148
+ | `PayloadRaw` | `string` | 原始 UTF-8 文本,适合验签与审计 |
149
+ | `Topic` | `string` | 已规范化的完整 Topic |
150
+ | `OsClient` | `string` | 已校验租户 |
151
+ | `UserName` | `string` | 连接用户名,仅部分事件有值 |
152
+ | `Qos` | `number` | `0`、`1`、`2` |
153
+ | `Retain` | `boolean` | Retain 标记 |
154
+ | `UserProperties` | `object` | MQTT v5 用户属性;没有时为空 |
155
+
156
+ 按事件读取字段,不要假设生命周期事件也有 ClientId/Payload,或连接事件已有 Topic。
157
+
158
+ ## V8 返回值和失败关闭
159
+
160
+ 只有 `MessageReceived` 把接口引擎返回值作为发布策略:
161
+
162
+ - `null`/无返回值:放行;
163
+ - 字符串、数字等普通历史返回值:放行;
164
+ - 没有 `Code` 的对象:放行;
165
+ - `Code: 1`:放行;
166
+ - 显式 `Code != 1`:阻止向订阅者广播并写系统诊断;
167
+ - 已配置的事件引擎执行异常:归一为 `Code: 0`,失败关闭。
168
+
169
+ 没有配置 `MqttApiEngine` 时不存在 V8 策略闸门,Broker 仍只执行内置认证与 Topic
170
+ ACL。接收日志在 V8 执行前进入 `mci_mqtt_log`,因此规则拒绝的消息仍可审计。
171
+
172
+ `Connected`、`Disconnected`、`Subscribing`、`MessageChanged` 和生命周期事件是业务
173
+ 观察/联动入口,当前返回值不会反向改变底层协议动作。不要误写“在 `Connected`
174
+ 返回 `Code: 0` 即可拒绝连接”或“在 `Subscribing` 返回失败即可拒绝订阅”。
175
+
176
+ ## 设备级接口引擎
177
+
178
+ `mci_mqtt_client.ApiEngineId` 可覆盖租户 `sys_osclients.MqttApiEngine`。连接时平台:
179
+
180
+ 1. 新增或更新 `ClientId`、`LastConnectTime`、`IsOnline`;
181
+ 2. 把已有设备 `ApiEngineId` 放进当前节点缓存;
182
+ 3. 优先使用设备引擎处理当前连接期间的 MQTT 事件;
183
+ 4. 缺少设备引擎时回退租户默认引擎。
184
+
185
+ 历史 JoinForm 配置可能保存 `sys_apiengine.Id`(GUID/ULID);运行时会解析为真实
186
+ `ApiEngineKey`。新配置优先保存 Key。
187
+
188
+ 设备级缓存是当前节点、当前连接的优化,不是共享事实。修改 `ApiEngineId` 后让设备
189
+ 重新连接。当前源码在有效断开时先移除连接与设备引擎缓存,因此
190
+ `Disconnected` 事件会走租户默认引擎;需要设备专属离线业务时,在租户默认引擎
191
+ 按 `ClientId` 查询设备配置,不要假设断开事件仍持有设备缓存。
192
+
193
+ ## 安全的遥测处理模式
194
+
195
+ ```javascript
196
+ var mqtt = V8.MQTT || {};
197
+
198
+ if (V8.EventName !== 'MessageReceived') {
199
+ return { Code: 1 };
200
+ }
201
+
202
+ var data = mqtt.Payload;
203
+ if (typeof data === 'string') {
204
+ try {
205
+ data = JSON.parse(data);
206
+ } catch (ex) {
207
+ return { Code: 0, Msg: 'Payload 必须是合法 JSON。' };
208
+ }
209
+ }
210
+
211
+ if (!data || !data.eventId) {
212
+ return { Code: 0, Msg: '缺少稳定的 eventId。' };
213
+ }
214
+
215
+ var temperature = Number(data.temperature);
216
+ if (isNaN(temperature) || temperature < -80 || temperature > 200) {
217
+ return { Code: 0, Msg: 'temperature 超出允许范围。' };
218
+ }
219
+
220
+ // iot_telemetry_ingest 必须按 mqtt.OsClient + data.eventId 做唯一约束/inbox 去重。
221
+ var result = V8.ApiEngine.Run('iot_telemetry_ingest', {
222
+ eventId: data.eventId,
223
+ osClient: mqtt.OsClient,
224
+ clientId: mqtt.ClientId,
225
+ topic: mqtt.Topic,
226
+ temperature: temperature,
227
+ qos: mqtt.Qos,
228
+ retain: mqtt.Retain,
229
+ payloadRaw: mqtt.PayloadRaw
230
+ });
231
+
232
+ return result && result.Code === 1
233
+ ? { Code: 1 }
234
+ : { Code: 0, Msg: (result && result.Msg) || '遥测处理失败。' };
235
+ ```
236
+
237
+ 不要在重试时用 `NewUlid()` 重新生成业务幂等键。稳定 `EventId` 应由设备/网关生成,
238
+ 或由网关基于设备序列号、消息序号和采样时间确定性构造。消费端仍需数据库唯一约束、
239
+ inbox/outbox、状态机或条件更新,QoS 2 也不能代替业务幂等。
240
+
241
+ ## 服务端安全下行
242
+
243
+ 可信后端只调用带租户上下文的接口:
244
+
245
+ ```csharp
246
+ await mqttService.PublishAsync(
247
+ osClient,
248
+ $"device/{deviceId}/command",
249
+ JsonConvert.SerializeObject(new { Action = "restart", EventId = eventId }),
250
+ qos: 1,
251
+ retain: false);
252
+ ```
253
+
254
+ 运行时会规范化 Topic、`ResponseTopic`,覆盖 User Property 中的 `OsClient`,并用内部
255
+ 租户 SenderClientId 注入消息。缺少 `osClient` 的旧原生重载会直接抛错拒绝。
256
+
257
+ `V8.MQTT` 是只读事件上下文,不是 `Publish` API。若需要 V8 下行,先在 C# 提供
258
+ 最小、租户隔离、不可覆盖基础设施秘密的原子能力,再由接口引擎做菜单/表/行权限、
259
+ 状态机、稳定 EventId、审计和业务编排;不要开放匿名通用发布 Controller。
260
+
261
+ ## 数据分层与可观测性
262
+
263
+ | 数据 | 推荐位置 | 原因 |
264
+ | --- | --- | --- |
265
+ | 设备档案、阈值、归属、工单、告警状态 | FormEngine/关系库 | 需要事务、权限和后台维护 |
266
+ | 高频遥测、采样序列 | MongoDB/专用时序存储 | 便于分区、保留和批量查询 |
267
+ | 图片、音频、固件、大文件 | 对象存储/文件服务 | MQTT 只传文件 Id、哈希和元数据 |
268
+ | Broker 运行审计 | `mci_mqtt_log` | 排障,不代替长期遥测仓 |
269
+ | 当前设备接入台账 | `mci_mqtt_client` | 最后连接、基础在线状态、设备引擎 |
270
+
271
+ 系统日志 `Type=MQTT` 记录端口占用、TLS、认证拒绝、Topic ACL、V8 异常和旧连接
272
+ 断开忽略等诊断。当前 `mci_mqtt_log` 的 `Receive` 审计会保存解析后的 Payload 与
273
+ `PayloadRaw`;不要在 MQTT Payload 携带密码、Token 等秘密,并为该表配置严格权限、
274
+ 脱敏、保留、归档和容量策略。日志/设备表写入失败只记录告警,不会停止消息主流程,
275
+ 因此它们不能单独作为“消息一定持久化”的证明。
276
+
277
+ `mci_mqtt_client.IsOnline` 只能反映运行时最后写入的基础状态。节点崩溃不会保证
278
+ 产生 `Disconnected`;业务在线判断应结合共享数据库中的心跳、最后活动时间、超时
279
+ 窗口和设备状态机。
280
+
281
+ ## 单节点与多节点部署
282
+
283
+ ### 单节点或独立 MQTT 节点
284
+
285
+ - 显式映射 `MqttPort` 和 `MqttTlsPort`,开放防火墙/负载入口。
286
+ - 把证书挂载为只读文件;轮换后重启 MQTT 节点。
287
+ - 把 MQTT TCP/TLS 流量稳定路由到该节点,不要求 HTTP 会话粘滞来保证业务正确。
288
+ - `GetConnectedClients(osClient)` 和管理状态接口只用于当前节点诊断。
289
+
290
+ ### 多 API 节点
291
+
292
+ 内嵌 Broker 的连接、会话、订阅、Retained Message 和内存缓存不会跨 API 节点共享。
293
+ 不要让负载均衡后的每个 API 节点各启动一套 Broker,再宣称它们组成一个集群。
294
+
295
+ 选择以下一种生产架构:
296
+
297
+ 1. 仅在独立 MQTT 节点启用内嵌 Broker,TCP/TLS 入口固定路由到该节点;
298
+ 2. 使用支持持久化与集群的外部 Broker,并通过租户感知网关/适配器进入 Microi
299
+ 事件链;外部 Broker 本身不会自动触发当前进程内 `V8.MQTT`;
300
+ 3. 所有业务副作用使用稳定 EventId、数据库唯一约束、inbox/outbox 和条件更新。
301
+
302
+ 若要求掉电或强杀窗口内零丢失,必须在返回成功前获得外部 Broker 持久化、共享
303
+ outbox 或同步 WAL 的确认;内存状态与异步稍后写库不能覆盖该窗口。
304
+
305
+ ## 上线验收清单
306
+
307
+ - [ ] 主租户启用后 TCP 端口真实监听;TLS 证书链、端口和客户端握手真实可用。
308
+ - [ ] 正确凭据连接成功;错误密码、未知/未启用租户、凭据碰撞均被拒绝。
309
+ - [ ] 多租户来源冲突、跨租户 ClientId、空或非法 ClientId 被拒绝。
310
+ - [ ] 普通 Topic 自动加租户前缀;其它租户、`$SYS`、`$share`、非法路径被拒绝。
311
+ - [ ] 合法订阅通配符、QoS 0/1/2、Retain、`ResponseTopic` 按预期工作。
312
+ - [ ] 七类事件按实际触发时机进入正确租户,字段可用性与本参考一致。
313
+ - [ ] `MessageReceived` 返回 `Code: 0` 或执行异常时订阅端收不到广播,审计仍存在。
314
+ - [ ] JSON 与 UTF-8 文本载荷都按策略处理,非法载荷不会写业务表。
315
+ - [ ] 设备级 `ApiEngineId` 重连后生效,历史 Id 能解析为 `ApiEngineKey`。
316
+ - [ ] 同一 ClientId 快速重连时,旧断开不会把新连接标记为离线。
317
+ - [ ] 可信后端下行被强制限制在当前租户 Topic,旧无租户重载被拒绝。
318
+ - [ ] 重复消息、接口超时、数据库短故障、节点重启后,业务副作用仍然至多一次。
319
+ - [ ] 多节点场景验证入口路由、故障转移与共享业务状态,不把节点快照当全局事实。
320
+ - [ ] 峰值压测记录 Broker、V8、关系库/MongoDB、日志的 CPU、内存、延迟与磁盘增长。
321
+
322
+ 静态源码、自动测试、本地 Broker、生产网络和真实硬件属于不同证据层。未执行某一层
323
+ 时必须明确说明,不能用另一层的成功替代。
324
+
325
+ ## 自动覆盖检查的证据边界
326
+
327
+ 运行:
328
+
329
+ ```powershell
330
+ node microi.skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs
331
+ ```
332
+
333
+ 脚本验证当前源码提取到的 MQTT 事件与 `MqttParam` 属性都出现在主 Skill、生产参考、
334
+ 官网 MQTT 文档和 V8 后端索引中,并检查配置、安全、设备路由、下行、节点状态与
335
+ 部署关键字。它不能证明:
336
+
337
+ - 字段已升级到某台目标服务器;
338
+ - Broker 已监听或证书有效;
339
+ - 外部 Broker/网关适配器已实现;
340
+ - V8 示例在目标表结构上运行成功;
341
+ - 真实设备、网络抖动、吞吐、掉电和多节点故障转移已经实测。