$npx -y skills add allocnode/oh-my-sage --skill mijia-automation米家自动化极客版规则与变量管理指南。当用户想要创建智能场景、设备联动、定时任务、条件触发,或创建、读取、修改、删除自动化变量时使用此skill。
| 1 | # 米家自动化规则创建 |
| 2 | |
| 3 | ## 变量生命周期能力 |
| 4 | |
| 5 | 变量管理分为三层,不能把其中一层的限制误判成网关不支持: |
| 6 | |
| 7 | | 层级 | 能力与限制 | |
| 8 | |------|------------| |
| 9 | | 网关 API | 支持 `createVar`、`deleteVar`、`getVarValue`、`getVarConfig`、`setVarValue` | |
| 10 | | 专用 MCP 工具 | 使用 `mijia_create_variable`、`mijia_delete_variable`、`mijia_get_variable_value`、`mijia_get_variable_config`、`mijia_set_variable` | |
| 11 | | 通用原始 API 工具 | `mijia_call_api` 故意只允许只读方法;写方法被拒绝不代表网关没有写能力 | |
| 12 | |
| 13 | 关键规则: |
| 14 | |
| 15 | - 新建变量必须调用 `mijia_create_variable`;`mijia_set_variable` 只修改已存在变量,不会自动创建。 |
| 16 | - `varSetNumber`、`varSetString`、`deviceInputSetVar` 和 `deviceGetSetVar` 只写已存在变量,不能用作变量创建器。 |
| 17 | - 变量 ID 必须匹配 `^[a-zA-Z0-9]+$`,不能含下划线、连字符或中文;显示名称 `name` 可以包含中文。 |
| 18 | - `type` 只能是 `number` 或 `string`,初始值和后续值必须与类型一致。 |
| 19 | - `createVar` 的显示名称必须放在 `userData: { name }`,不能传顶层 `name`。缺少 `userData.name` 时变量虽可按 ID 读取,但极客版 UI 的变量选择器不会显示它。 |
| 20 | - 删除变量前检查所有规则引用;删除是不可恢复操作。 |
| 21 | |
| 22 | ### 工具缺失或写入失败时 |
| 23 | |
| 24 | 按以下顺序判断,不要直接宣布“网关无法读写变量”: |
| 25 | |
| 26 | 1. 查看当前 MCP 工具列表是否包含上述专用变量工具。 |
| 27 | 2. 工具缺失时检查运行中的 MCP 是否为旧构建;源码新增工具后必须重新构建并重启 MCP,当前进程不会动态注册新工具。 |
| 28 | 3. 检查 `src/core/tools/variable.ts`、`src/mcp/tools/variable.ts` 和实际运行的 `dist`,确认功能是未实现、未构建还是未加载。 |
| 29 | 4. `mijia_set_variable` 返回变量不存在时,改用 `mijia_create_variable`,不要尝试用规则节点自动创建。 |
| 30 | 5. `mijia_call_api` 拒绝 `createVar` 等写方法时,改用专用工具,不要放宽通用工具的只读白名单。 |
| 31 | 6. 若怀疑功能曾存在但被回归删除,检查 Git 历史或会话中的真实工具调用记录,再下结论。 |
| 32 | |
| 33 | 实机验证过的生命周期:创建临时变量 -> 读取配置和值 -> 修改 -> 回读 -> 删除 -> 再次读取确认不存在。创建或恢复变量工具后应完整执行一次该流程,并清理临时变量。 |
| 34 | |
| 35 | 发现网关尚未封装的新能力时,读取 [网关能力发现方法](references/gateway-capability-discovery.md)。不要靠猜测 API 名称,也不要因为当前 MCP 没有工具就判定网关不支持。 |
| 36 | |
| 37 | ## 规则结构 |
| 38 | |
| 39 | ```json |
| 40 | { |
| 41 | "id": "graph_时间戳", |
| 42 | "nodes": [节点1, 节点2, ...], |
| 43 | "cfg": { |
| 44 | "id": "graph_时间戳", |
| 45 | "enable": true, |
| 46 | "uiType": "graph", |
| 47 | "userData": { |
| 48 | "name": "规则名称", |
| 49 | "lastUpdateTime": 1710000000000, |
| 50 | "transform": {"x": 0, "y": 0, "scale": 1, "rotate": 0} |
| 51 | } |
| 52 | } |
| 53 | } |
| 54 | ``` |
| 55 | |
| 56 | **节点位置自动布局**:create_graph 会根据节点连接关系自动计算位置,无需手动设置 `cfg.pos`。布局规则: |
| 57 | - 从左到右表示流程方向 |
| 58 | - 分支节点上下排列 |
| 59 | - 节点尺寸:528×164 |
| 60 | |
| 61 | ## 关键校验规则 |
| 62 | |
| 63 | 1. **节点 id**:只允许 `[0-9a-zA-Z]`,不能用下划线、连字符 |
| 64 | 2. **outputs 连接格式**:`"portName": ["nodeId.inputPort"]`(必须是点分隔,如 `"cond1.trigger"`) |
| 65 | - ❌ 错误:`"output": ["range1"]`(缺少 `.inputPort`) |
| 66 | - ✅ 正确:`"output": ["range1.trigger"]` |
| 67 | 3. **outputs 值必须是数组**:`"output": []` ✓,`"output": null` ✗ |
| 68 | 4. **所有节点必须声明 outputs 端口**:即使没有输出连接,也要声明端口(如 `"outputs": {"output": []}`) |
| 69 | 5. **deviceGet**:必须有 `outputs.output` 和 `outputs.output2` |
| 70 | 6. **inputs 命名**: |
| 71 | - `deviceGet`, `varGet`, `statusLast`, **`delay`** 用 `input` |
| 72 | - `deviceOutput`, `condition` 等用 `trigger` |
| 73 | - `timeRange`, `alarmClock` 等**状态节点没有输入端口**(`inputs: {}`) |
| 74 | 7. **dtype 映射**:`bool`→`boolean`,`uint8`/`int32`→`int`,`float`→`float` |
| 75 | 8. **props 必须存在**:`"props": {}` 不能省略 |
| 76 | 9. **cfg.name**:值为节点类型名(如 `"deviceInput"`) |
| 77 | 10. **cfg.unit/value**:delay 节点的 `cfg.unit` 和 `cfg.value` 是可选的(UI 显示用) |
| 78 | |
| 79 | ## inputs/outputs 工作机制 |
| 80 | |
| 81 | ### inputs 中的 null 是什么? |
| 82 | |
| 83 | `"inputs": {"trigger": null}` 中的 `null` **不是"未连接"**,而是**声明端口存在**。信号实际由上游节点的 `outputs` 数组传来。 |
| 84 | |
| 85 | ``` |
| 86 | 上游 outputs 数组 ──→ 决定 → 下游 inputs 端口 |
| 87 | "output": ["cond1.trigger"] "inputs": {"trigger": null} ← 端口声明,null 是正确的 |
| 88 | ``` |
| 89 | |
| 90 | ### outputs 数组决定连接关系 |
| 91 | |
| 92 | 整个规则的**连接关系完全由每个节点的 outputs 数组决定**,inputs 只是端口声明。生成规则后必须逐个检查每个节点的 outputs 数组,确保: |
| 93 | 1. 引用的目标节点 ID 存在 |
| 94 | 2. 引用的端口名是目标节点 inputs 中声明的端口名 |
| 95 | 3. 点分隔格式正确:`"目标节点ID.目标端口名"` |
| 96 | |
| 97 | ### state 节点 vs event 节点 |
| 98 | |
| 99 | | 类型 | 特征 | inputs | 代表节点 | |
| 100 | |------|------|--------|----------| |
| 101 | | **event 节点** | 可被触发 | 有 trigger/input 端口 | deviceOutput, condition, delay | |
| 102 | | **state 节点** | 不能被触发 | `inputs: {}`(空) | timeRange, alarmClock, deviceInputSetVar | |
| 103 | |
| 104 | **⚠️ state 条件节点通常连接 `condition.condition`,也可以先进入 `logicOr` / `logicAnd` / `logicNot`,再连接 `condition.condition`。state 节点没有输入端口,不能接收事件触发。** |
| 105 | |
| 106 | ### ⚠️ 网关不检查连接完整性(已验证) |
| 107 | |
| 108 | 网关 `setGraph` **只校验节点级别结构**(字段类型、必填字段),**不校验连接级别的逻辑完整性**。以下错误都能通过网关校验但在运行时不工作: |
| 109 | - condition 的 condition 端口未连接 → 网关允许保存,但实测不会执行 met 分支 |
| 110 | - deviceGet.output2 连到 state 节点 → 通过校验,但语义错误 |
| 111 | |
| 112 | **因此,生成规则后必须自行验证连接完整性**,不能依赖网关报错。 |
| 113 | |
| 114 | ## 工作流程(必须遵循) |
| 115 | |
| 116 | 当用户要求创建/修改自动化规则时,按以下步骤执行: |
| 117 | |
| 118 | 1. **理解需求**:分析用户的自动化逻辑,确定需要哪些节点 |
| 119 | 2. **生成节点列表**:按照节点模板和连接规则构建 nodes 数组 |
| 120 | 3. **调用 validate_graph 校验**:使用 `validate_graph` 工具传入完整的 graph JSON(nodes + cfg),检查连接完整性 |
| 121 | 4. **修复错误**:如果校验器报告 error 级别的问题,修复节点连接后重新校验,直到全部通过 |
| 122 | 5. **调用 create_graph 或 update_graph**:校验通过后调用创建/更新工具(注意:create_graph 和 update_graph 内部也会自动校验,如有错误会拒绝执行) |
| 123 | 6. **确认结果**:向用户确认规则创建成功 |
| 124 | |
| 125 | **⚠️ create_graph / update_graph 内置了校验逻辑。如果节点连接有 error 级别的问题,工具会返回错误并拒绝调用 setGraph。修复后重新调用即可。** |
| 126 | |
| 127 | ## 节点模板(直接复制使用) |
| 128 | |
| 129 | ### deviceInput - 设备触发(属性变化) |
| 130 | ```json |
| 131 | {"id":"$ID","type":"deviceInput","cfg":{"urn":"$URN","name":"deviceInput","version":1},"props":{"did":"$DID","siid":$SIID,"piid":$PIID,"preload":false,"dtype":"$DTYPE","operator":"=","v1":$V1},"inputs":{},"outputs":{"output":["$NEXT.trigger"]}} |
| 132 | ``` |
| 133 | |
| 134 | ### deviceInput - 设备触发(事件) |
| 135 | ```json |
| 136 | {"id":"$ID","type":"deviceInput","cfg":{"urn":"$URN","name":"deviceInput","version":1},"props":{"did":"$DID","siid":$SIID,"eiid":$EIID,"preload":false},"inputs":{},"outputs":{"output":["$NEXT.trigger"]}} |
| 137 | ``` |
| 138 | |
| 139 | ### deviceOutput - 控制设备(设置属性) |
| 140 | ```json |
| 141 | {"id":"$ID","type":"deviceOutput", |