@microi.net/cli 4.9.3 → 4.9.5

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 (34) 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/assets/build-meta.json +5 -5
  7. package/package.json +1 -1
  8. package/scripts/mcp-server.js +92 -92
  9. package/scripts/microi-cli.js +25 -25
  10. package/scripts/microi-skills.meta.json +157 -130
  11. package/skills/.microi-skills-version.json +2 -2
  12. package/skills/README.md +3 -1
  13. package/skills/app-store/SKILL.md +10 -2
  14. package/skills/microi-ai-application/SKILL.md +1 -1
  15. package/skills/microi-docs-coverage/references/capability-map.md +4 -1
  16. package/skills/microi-form-engine/SKILL.md +6 -3
  17. package/skills/microi-form-layout/SKILL.md +18 -5
  18. package/skills/microi-microservice/SKILL.md +1 -1
  19. package/skills/microi-system-delivery/SKILL.md +5 -2
  20. package/skills/print-engine/SKILL.md +5 -4
  21. package/skills/unity-integration/SKILL.md +151 -0
  22. package/skills/unity-integration/agents/openai.yaml +4 -0
  23. package/skills/unity-integration/references/ai-app-delivery.md +98 -0
  24. package/skills/unity-integration/references/sdk-api.md +82 -0
  25. package/skills/unity-integration/references/toolbox-migration.md +66 -0
  26. package/skills/unity-integration/references/webgl-hosting.md +57 -0
  27. package/skills/v8-frontend-events/SKILL.md +11 -7
  28. package/skills/v8-frontend-events/references/bluetooth-print-api.md +31 -3
  29. package/skills/v8-frontend-events/references/bluetooth-print.md +54 -12
  30. package/skills/v8-mq-mqtt/SKILL.md +140 -52
  31. package/skills/v8-mq-mqtt/references/mqtt-production.md +341 -0
  32. package/skills/v8-mq-mqtt/scripts/check-mqtt-skill-coverage.mjs +205 -0
  33. package/skills/v8-utilities/references/client-api-index.md +4 -2
  34. package/skills/workspace-conventions/SKILL.md +3 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: v8-frontend-events
3
- description: Microi 前端 V8 事件与客户端能力指南。用于编写浏览器端字段、按钮、列表事件,或使用 V8.EventName、V8.Form、V8.Print 蓝牙打印、扫码、弹窗、表单联动和界面交互。
3
+ description: Microi 前端 V8 事件与客户端能力指南。用于编写浏览器端字段、按钮、列表事件,或使用 V8.EventName、V8.Form、V8.Print 蓝牙打印(佳博 GP-M322、ZICOX CC4、TSPL、CPCL、ESC/POS、BLE、SPP)、扫码、弹窗、表单联动和界面交互。
4
4
  ---
5
5
 
6
6
  > **Codex 强制前置:** 当前宿主为 Codex 时,在使用本 Skill 前必须先完整读取 `../microi-codex-installer/SKILL.md`,完成“Codex 每任务最新版硬门禁”;门禁未通过不得继续本 Skill。非 Codex 宿主跳过此项。
@@ -16,7 +16,7 @@ description: Microi 前端 V8 事件与客户端能力指南。用于编写浏
16
16
  ## 能力路由
17
17
 
18
18
  - 查询前端 V8 全部上下文、导航、表单、列表、网络、引擎与工具入口时,读取 `../v8-utilities/references/client-api-index.md`。
19
- - 需求包含“蓝牙打印、标签打印、TSC/TSPL、ESC/POS、小票打印、佳博打印机”时,必须先读取 `references/bluetooth-print.md`;需要完整指令签名、编码或位图参数时,再读取 `references/bluetooth-print-api.md`。
19
+ - 需求包含“蓝牙打印、标签打印、TSC/TSPL、CPCL、ESC/POS、小票打印、佳博/GP-M322、ZICOX/芝柯/CC4、BLE/SPP”时,必须先读取 `references/bluetooth-print.md`;需要完整指令签名、型号转换范围、编码或位图参数时,再读取 `references/bluetooth-print-api.md`。
20
20
  - 浏览器模板打印、PDF/纸张模板、`mic_print`、`PageObj`、`PrintObj` 使用 `print-engine/SKILL.md`,不要与直接蓝牙指令混为一套 API。
21
21
  - 扫码使用 `V8.Method.ScanCode`,结果从 Promise/回调取得;`V8.ScanCodeRes` 只作兼容结果槽,详见客户端 API 索引。
22
22
  - 登录后的敏感操作使用 `V8.Identity.Verify` 完成 Passkey/严格人脸交互;前端只取得一次性 Ticket,后端接口引擎必须重算 `ActionHash` 并原子消费,不能把前端成功当作授权。
@@ -206,12 +206,14 @@ V8.RefreshTable({ _PageIndex: 1 });
206
206
  | `V8.FormEngine.GetTableData(name, params, cb)` | 前端查列表(参数对象、回调或 await) |
207
207
  | `V8.Post(url, data, cb, errCb, headers, contentType)` | 通用 POST |
208
208
  | `V8.Method.ScanCode({...})` | 调用当前终端支持的扫码能力 |
209
- | `V8.Print.isConnected()` | 检查当前蓝牙写特征是否仍可用 |
209
+ | `V8.Print.isConnected()` | 检查当前 BLE 写特征或 Android SPP Socket 是否仍可用 |
210
210
  | `V8.Print.OpenBluetoothPage()` | 在用户手势中打开蓝牙连接页,返回 Promise |
211
211
  | `V8.Print.reconnect()` | 使用已记住的设备授权或设备 ID 尝试重连 |
212
- | `V8.Print.getConnectionState()` | 获取连接、设备、记忆和错误状态快照 |
212
+ | `V8.Print.getConnectionState()` | 获取连接、设备、传输、型号、指令、记忆和错误状态快照 |
213
213
  | `V8.Print.subscribeConnection(listener)` | 订阅应用级共享连接状态,返回取消订阅函数 |
214
- | `V8.Print.prepareSend(bytes)` | 串行分包发送 TSC 或 ESC/POS 字节,必须 `await` |
214
+ | `V8.Print.getPrinterProfile()` | 查看自动识别或手工选择后的型号配置 |
215
+ | `V8.Print.setPrinterProfile(mode)` | 广播名无法识别时选 `zicox-cc4` 等型号;旧业务通常不调用 |
216
+ | `V8.Print.prepareSend(bytes)` | 按型号适配后串行分包发送 TSPL、CPCL 或 ESC/POS,必须 `await` |
215
217
 
216
218
  `V8.OpenAnyForm` 只发起打开动作,不返回“用户关闭后的 Promise”。需要替换
217
219
  子表单保存时,通过 `EventReplace.Submit(v8, param, callback)` 注册提交替换;
@@ -231,7 +233,7 @@ if (!V8.Print.isConnected()) {
231
233
  if (!connected || !V8.Print.isConnected()) return;
232
234
  }
233
235
 
234
- var command = V8.Print.createNew(); // TSC/TSPL 标签
236
+ var command = V8.Print.createNew(); // 同一 TSC 调用:GP-M322 原 TSPL,CC4 自动转 CPCL
235
237
  command.setSize(60, 40);
236
238
  command.setGap(2);
237
239
  command.setCls();
@@ -246,7 +248,9 @@ try {
246
248
  }
247
249
  ```
248
250
 
249
- PC/平板顶部导航与移动端【我的】页共用同一个应用级 `V8.Print` 实例,用户可先在全局入口连接,再进入任意模块打印。`prepareSend` 内部会把不同 V8 上下文排入同一发送队列;成功只证明字节已经写入蓝牙特征,不代表打印机已走纸、无缺纸或无硬件故障。批量打印仍应逐条 `await`,不得用固定 `setTimeout` 猜测完成时间,也不要用 `Promise.all` 表达同一设备的并行打印。完整挂载范围、连接语义、批量恢复、安全与硬件验收见 `references/bluetooth-print.md`;源码级 TSC/ESC 方法表见 `references/bluetooth-print-api.md`。
251
+ PC/平板顶部导航与移动端【我的】页共用同一个应用级 `V8.Print` 实例,用户可先在全局入口连接,再进入任意模块打印。佳博 GP-M322 路径必须保持原 TSPL 字节不变;ZICOX CC4 只转换有明确 CPCL 等价语义的高层 TSC 调用,不支持的命令必须在首包写入前失败,禁止盲目透传乱码。Android 5+App 的 CC4 可在 BLE 失败时使用已配对 SPP,Web 端不能使用经典蓝牙。
252
+
253
+ `prepareSend` 内部会把不同 V8 上下文排入同一发送队列;成功只证明字节已经写入 BLE 特征或 SPP 输出流,不代表打印机已走纸、无缺纸或无硬件故障。批量打印仍应逐条 `await`,不得用固定 `setTimeout` 猜测完成时间,也不要用 `Promise.all` 表达同一设备的并行打印。完整挂载范围、双型号连接、协议映射、批量恢复、安全与硬件验收见 `references/bluetooth-print.md`;源码级 TSC/ESC/CPCL 方法表见 `references/bluetooth-print-api.md`。
250
254
 
251
255
  ### 常用上下文差异
252
256
 
@@ -1,12 +1,14 @@
1
- # V8.Print TSC ESC/POS 源码 API
1
+ # V8.Print TSC、CPCL 适配与 ESC/POS 源码 API
2
2
 
3
- 本表直接按 `Microi.Client/src/utils/ble/tsc.js`、`esc.js` 与编码文件整理。方法名
3
+ 本表直接按 `Microi.Client/src/utils/ble/tsc.js`、`esc.js`、
4
+ `printer-compatibility.js` 与编码文件整理。方法名
4
5
  (包括历史拼写)必须与源码完全一致,不能根据打印机手册自行改名。
5
6
 
6
7
  ## 目录
7
8
 
8
9
  - [构建器与编码](#构建器与编码)
9
10
  - [TSC/TSPL 的 28 个方法](#tsctspl-的-28-个方法)
11
+ - [ZICOX CC4 的 CPCL 映射](#zicox-cc4-的-cpcl-映射)
10
12
  - [ESC/POS 的 25 个方法](#escpos-的-25-个方法)
11
13
  - [参数与组合规则](#参数与组合规则)
12
14
 
@@ -17,7 +19,9 @@ var tsc = V8.Print.createNew();
17
19
  var esc = V8.Print.createNewESC();
18
20
  ```
19
21
 
20
- 两个构建器都把指令累积到普通字节数组,`getData()` 返回该数组。TSC 的全部文本命令、
22
+ 两个构建器都把指令累积到普通字节数组,`getData()` 返回该数组。TSC 返回值还带不可枚举
23
+ 协议/操作元数据:普通遍历与 GP-M322 字节值不变,CC4 发送前据此生成 CPCL。不要复制或
24
+ 序列化 TSC 数组后再交给 `prepareSend`。TSC 的全部文本命令、
21
25
  ESC 的 `setText` 和二维码内容通过本地 `TextEncoder('gb18030', {
22
26
  NONSTANDARD_allowLegacyEncoding: true })` 编码;映射表来自同目录的
23
27
  `encoding-indexes.js`,不需要网络请求。
@@ -59,6 +63,30 @@ NONSTANDARD_allowLegacyEncoding: true })` 编码;映射表来自同目录的
59
63
  `setPagePrint` → `getData`。字体名、条码类型、纸张传感器、速度和浓度由打印机固件决定,
60
64
  构建器不验证范围。
61
65
 
66
+ ## ZICOX CC4 的 CPCL 映射
67
+
68
+ | TSC 方法 | CC4 CPCL 结果 |
69
+ |---|---|
70
+ | `setSize(w,h)` | 203 dpi 下按 `8 dots/mm` 生成页宽/页高 |
71
+ | `setSpeed` / `setDensity` | `SPEED` / `CONTRAST`,钳制到当前安全范围 |
72
+ | `setGap` / `setBline` | `GAP-SENSE` / `BAR-SENSE` |
73
+ | `setFeed` / `setBackFeed` | `POSTFEED` / `PREFEED` |
74
+ | `setDirection(0/1)` | `ZPROTATE` / `ZPROTATE180` |
75
+ | `setReference` | 转换坐标时叠加参考点 |
76
+ | `setBar` / `setBox` / `setReverse` | `LINE` / `BOX` / `INVERSE-LINE` |
77
+ | `setText` | `SETMAG` + `T`,字体名按 16/24/32 档映射 |
78
+ | `setBarCode` | `BARCODE`,可选 `BARCODE-TEXT` |
79
+ | `setQR` | 厂家 SDK 格式 `BARCODE QR ...` + 数据 + `ENDQR` |
80
+ | `setBitmap` | `CG` + 原始单色位图字节 |
81
+ | `setPagePrint` | 最终统一补 `FORM` + `PRINT`,每份必须恰好一次 |
82
+
83
+ `init`、`setCls` 不输出。`addCommand`、`setCountry`、`setCodepage`、`setFromfeed`、
84
+ `setHome`、`setSound`、`setLimitfeed`、`setErase` 与未知方法会抛错,并且适配发生在分包前,
85
+ 所以失败时不得写入任何一包。该白名单只表达当前有证据的等价转换,不代表 CC4 固件不具备
86
+ 其它 CPCL 能力。
87
+
88
+ `createNewESC()` 的数组标记为 ESC/POS,CC4 直接原样发送,不进入 CPCL 转换。
89
+
62
90
  ## ESC/POS 的 25 个方法
63
91
 
64
92
  | 方法 | 当前生成的指令/作用 |
@@ -1,13 +1,15 @@
1
- # V8.Print 蓝牙打印运行指南
1
+ # V8.Print 蓝牙打印运行指南(GP-M322 / ZICOX CC4)
2
2
 
3
- 本参考用于 Microi 前端 V8 的 BLE 标签和小票打印。运行时事实源为
4
- `Microi.Client/src/utils/v8-print.js`,指令方法事实源见
3
+ 本参考用于 Microi 前端 V8 的 BLE/SPP 标签和小票打印。运行时事实源为
4
+ `Microi.Client/src/utils/v8-print.js`,型号与协议适配事实源为
5
+ `Microi.Client/src/utils/ble/printer-compatibility.js`,指令方法事实源见
5
6
  [`bluetooth-print-api.md`](bluetooth-print-api.md)。官网旧业务示例不能作为当前连接语义。
6
7
 
7
8
  ## 目录
8
9
 
9
10
  - [前端挂载范围](#前端挂载范围)
10
11
  - [运行环境与能力判断](#运行环境与能力判断)
12
+ - [双型号兼容契约](#双型号兼容契约)
11
13
  - [连接与发送语义](#连接与发送语义)
12
14
  - [最小安全流程](#最小安全流程)
13
15
  - [批量打印与恢复](#批量打印与恢复)
@@ -40,7 +42,8 @@ PC/平板顶部导航和移动端【我的】页的蓝牙入口也使用该单
40
42
 
41
43
  | 运行环境 | 当前引擎 | 结论 |
42
44
  |---|---|---|
43
- | 5+App 打包的 APK/IPA | `plus.bluetooth` | 支持 BLE 扫描、连接和写特征 |
45
+ | Android 5+App | `plus.bluetooth` + `plus.android` | BLE;ZICOX CC4 可回退到已配对 RFCOMM/SPP;Android 12+ 需要附近设备权限 |
46
+ | iOS 5+App | `plus.bluetooth` | BLE,不使用 Android SPP |
44
47
  | 存在 `navigator.bluetooth.requestDevice` 的浏览器 | Web Bluetooth | 支持;通常要求安全上下文和用户手势 |
45
48
  | 其它普通 H5/浏览器 | 无 | `V8.Print` 仍可能存在,但连接页会提示能力不可用 |
46
49
  | 微信小程序原生 BLE | 不属于此模块 | 需要小程序/UniApp 侧专用实现 |
@@ -49,8 +52,36 @@ PC/平板顶部导航和移动端【我的】页的蓝牙入口也使用该单
49
52
  `BLEInformation.deviceId`。正确顺序是检查 `V8.Print`、调用 `isConnected()`,再在
50
53
  用户点击事件中 `await OpenBluetoothPage()`。
51
54
 
55
+ Android 12+ 宿主必须启用 DCloud Bluetooth 模块并声明 `BLUETOOTH_SCAN`、
56
+ `BLUETOOTH_CONNECT`。运行时只在用户主动点击“搜索”时申请,不得在页面初始化或自动重连
57
+ 时弹授权框;拒绝或永久拒绝要引导用户进入系统“附近的设备”权限设置。
58
+
52
59
  浏览器模板、PDF、A4 单据和 Print Engine JSON 属于 `print-engine`;TSC/TSPL 或
53
- ESC/POS 原生字节通过 BLE 写入才属于 `V8.Print`。
60
+ CPCL/ESC/POS 原生字节通过 BLE/SPP 写入才属于 `V8.Print`。
61
+
62
+ ## 双型号兼容契约
63
+
64
+ | 型号 | 标签协议 | 传输 | 兼容承诺 |
65
+ |---|---|---|---|
66
+ | 佳博 GP-M322 | TSPL | BLE | `createNew().getData()` 原字节发送,协议适配层不得改写 |
67
+ | ZICOX CC4 | CPCL | BLE 优先,Android SPP 兜底 | 同一份标准 TSC 高层调用在首包写入前转换为 CPCL |
68
+ | 其它 TSPL | TSPL | BLE | 保持原字节路径 |
69
+
70
+ `createNewESC()` 在 CC4 上也原样发送,因为厂家声明 CC4 支持 ESC/POS。不要根据厂家 Demo
71
+ 里存在测试字符串就擅自宣称其它协议;以产品页、准确手册和实机固件为准。
72
+
73
+ TSC 构建器给字节数组附加不可枚举的操作元数据,因此 GP 字节值、长度与数组枚举完全不变。
74
+ CC4 必须直接收到同一次 `getData()` 返回值;`Array.from`、展开、JSON 序列化等复制会丢失
75
+ 元数据并失败关闭。适配器先完成整份转换和校验,再开始分包;不支持的命令不得产生半张输出。
76
+
77
+ CC4 可转换:纸张尺寸、速度、浓度、间隙/黑标、前后走纸、方向 0/1、参考点、线/框/反相、
78
+ 文字、条码、二维码、位图及单次 `setPagePrint`。`init`/`setCls` 无需输出。原始 `addCommand`、
79
+ 国家/代码页、`setFromfeed`、`setHome`、蜂鸣、限位、擦除和未知方法没有足够等价语义,必须
80
+ 在首包写入前拒绝。扩展白名单前要同时增加协议单测和两台目标机回归。
81
+
82
+ Android SPP 与厂家 Demo 一致,优先 RFCOMM 通道 1,再以标准 UUID
83
+ `00001101-0000-1000-8000-00805F9B34FB` 兜底。自动模式只显示名称可识别为 CC4 的已配对
84
+ 经典设备;广播名不规范时,用户先手工选择 `zicox-cc4`。Web Bluetooth 不能访问 SPP。
54
85
 
55
86
  ## 连接与发送语义
56
87
 
@@ -63,12 +94,17 @@ ESC/POS 原生字节通过 BLE 写入才属于 `V8.Print`。
63
94
  | `reconnect()` | 使用已记住的设备 ID 或浏览器保留的设备授权重连,不弹选择框 |
64
95
  | `getConnectionState()` | 返回可展示的连接、记忆、设备、错误和重连状态快照 |
65
96
  | `subscribeConnection(listener)` | 立即回调当前快照并持续通知状态变化,返回取消订阅函数 |
66
- | `prepareSend(bytes)` | 先尝试恢复连接,再进入应用级队列按包串行写入;必须 `await` 并捕获失败 |
97
+ | `getPrinterProfile()` | 返回最终型号、标签指令和传输偏好;普通业务无需调用 |
98
+ | `setPrinterProfile(mode)` | 手工选 `gprinter-gp-m322`、`zicox-cc4`、`generic-tspl`,或恢复 `auto` |
99
+ | `prepareSend(bytes)` | 先恢复连接、完成型号协议适配,再进入应用级队列按包串行写入 |
67
100
  | `Send(bytes)` | 依赖 `prepareSend` 已设置的内部游标,属于内部状态机入口,业务代码不要直接调用 |
68
101
  | `setOneTimeData(bytes)` | 设置 BLE 包长;只接受 1–512 整数,连接页候选 20–190,默认 20 |
69
102
  | `setPrinterNum(num)` | 重复发送同一缓冲区;只接受 1–99 整数,连接页候选 1–9 |
70
103
  | `disconnect()` | 主动断开、停止自动重连并忘记当前设备 |
71
- | `BLEInformation` | 最近设备/服务/特征元数据,只用于诊断,不代表实时连接或打印回执 |
104
+ | `BLEInformation` | 最近设备/型号/通道/服务/特征元数据,只用于诊断,不代表实时连接或打印回执 |
105
+
106
+ `getConnectionState()` 额外含 `transport`、`profileMode`、`profileId`、`profileName`、
107
+ `commandLanguage`;前端展示可以使用,打印判断仍使用 `isConnected()`。
72
108
 
73
109
  `OpenBluetoothPage()` 不是“连接成功事件”;用户连上设备后仍要关闭弹窗,调用方才能继续。
74
110
  设备元数据会写入 `localStorage` 与兼容用 `sessionStorage`。应用初始化、页面恢复、重新获得
@@ -115,8 +151,9 @@ async function printLabel(order) {
115
151
  }
116
152
  ```
117
153
 
118
- ESC/POS 小票使用 `createNewESC()`,完整顺序和 25 个真实方法见
119
- [`bluetooth-print-api.md`](bluetooth-print-api.md)。发送成功只表示 BLE 写调用完成,不能
154
+ 同一段标签代码在 GP-M322 上保持 TSPL,在 CC4 上转换为 CPCL。ESC/POS 小票使用
155
+ `createNewESC()`,完整顺序和 25 个真实方法见
156
+ [`bluetooth-print-api.md`](bluetooth-print-api.md)。发送成功只表示 BLE/SPP 写调用完成,不能
120
157
  写成“打印机已走纸”或“物理打印成功”。当前源码虽发现 read/notify 特征,但没有订阅状态
121
158
  通知,也没有消费 ACK、缺纸或故障回执。
122
159
 
@@ -157,6 +194,8 @@ async function printBatch(rows, startIndex) {
157
194
  - Web Bluetooth 仅把四个常见服务 UUID 传入 `optionalServices`:`18f0`、`ff00`、
158
195
  `49535343-fe7d-4ae5-8fa9-9fafd205e455`、`e7810a71-73ae-499d-8c15-faa9aef0c3f2`。
159
196
  当前没有公开的自定义服务配置,并选择枚举到的第一个可写特征;其它型号可能需要扩展源码。
197
+ CC4 固件若只开放 SPP 或使用其它私有 UUID,Web 端不可连接;Android 5+App 使用已配对 SPP,
198
+ 或先取得厂家准确 BLE UUID 再扩展源码,禁止猜 UUID。
160
199
  - `prepareSend` 默认每包 20 字节、包间约 20ms;同一缓冲区多份打印间约 100ms。这只是
161
200
  BLE 写节奏,不是打印完成等待时间。包长必须是已实测的正整数,空缓冲区不得发送。
162
201
  - 当前分包公式使用 `Math.ceil(length / packetSize)`,长度恰好整除时不会产生 0 字节末包;
@@ -167,6 +206,8 @@ async function printBatch(rows, startIndex) {
167
206
  大图可能产生大缓冲区;先缩放、二值化并用小图测试。
168
207
  - `V8.Print` 使用应用级共享发送队列,跨 V8 上下文不会再并发覆盖 `currentTime`、`looptime`、
169
208
  `lastData` 等共享状态。队列只保证写入顺序,不提供打印机 ACK、业务事务或自动重打语义。
209
+ - CC4 遇到不支持的方法、复制后丢失元数据、缺少或重复 `setPagePrint()` 时应零写入失败;不要
210
+ 在业务层捕获后把原 TSPL 盲目重发给 CC4。
170
211
 
171
212
  ## 安全边界
172
213
 
@@ -182,10 +223,11 @@ async function printBatch(rows, startIndex) {
182
223
 
183
224
  至少记录:
184
225
 
185
- 1. 打印机品牌、型号、固件、纸张规格、服务/写特征 UUID 和指令集。
186
- 2. 5+App 或浏览器版本;首次授权、再次连接、主动断开、页面刷新和断线重连。
226
+ 1. GP-M322 与 CC4 的固件、纸张规格、服务/写特征 UUID 或 SPP、指令集。
227
+ 2. 5+App 或浏览器版本;首次授权/配对、自动/手工选型、再次连接、主动断开、页面刷新和断线重连。
187
228
  3. 中文、数字、特殊字符、二维码、条码、长文本、图片和边界金额。
188
229
  4. 默认 20 字节与目标包长;同时覆盖“长度恰好整除包长”。
189
230
  5. 连续 20 张严格串行发送,无乱序、丢包、重复或任务状态互相污染。
190
231
  6. 中途关机、缺纸、离开范围、权限撤销后的失败位置与恢复行为。
191
- 7. 页面只确认“数据已发送”;若业务要求确认物理结果,另接状态回读或人工确认。
232
+ 7. 两种设备交替连接,证明 GP 原 TSPL 不变、CC4 收到 CPCL/ESC-POS;CC4 分别记录 BLE 与 SPP。
233
+ 8. 页面只确认“数据已发送”;若业务要求确认物理结果,另接状态回读或人工确认。
@@ -1,6 +1,6 @@
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
6
  > **Codex 强制前置:** 当前宿主为 Codex 时,在使用本 Skill 前必须先完整读取 `../microi-codex-installer/SKILL.md`,完成“Codex 每任务最新版硬门禁”;门禁未通过不得继续本 Skill。非 Codex 宿主跳过此项。
@@ -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) 为准