Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions .agents/skills/mijia-automation/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
---
name: mijia-automation
description: 米家自动化极客版规则创建指南。当用户想要创建智能场景、设备联动、定时任务、条件触发等自动化规则时使用此skill
description: 米家自动化极客版规则与变量管理指南。当用户想要创建智能场景、设备联动、定时任务、条件触发,或创建、读取、修改、删除自动化变量时使用此skill
metadata:
author: oh-my-sage
version: "3.4"
version: "3.5"
---

# 米家自动化规则创建

## 变量生命周期能力

变量管理分为三层,不能把其中一层的限制误判成网关不支持:

| 层级 | 能力与限制 |
|------|------------|
| 网关 API | 支持 `createVar`、`deleteVar`、`getVarValue`、`getVarConfig`、`setVarValue` |
| 专用 MCP 工具 | 使用 `mijia_create_variable`、`mijia_delete_variable`、`mijia_get_variable_value`、`mijia_get_variable_config`、`mijia_set_variable` |
| 通用原始 API 工具 | `mijia_call_api` 故意只允许只读方法;写方法被拒绝不代表网关没有写能力 |

关键规则:

- 新建变量必须调用 `mijia_create_variable`;`mijia_set_variable` 只修改已存在变量,不会自动创建。
- `varSetNumber`、`varSetString`、`deviceInputSetVar` 和 `deviceGetSetVar` 只写已存在变量,不能用作变量创建器。
- 变量 ID 必须匹配 `^[a-zA-Z0-9]+$`,不能含下划线、连字符或中文;显示名称 `name` 可以包含中文。
- `type` 只能是 `number` 或 `string`,初始值和后续值必须与类型一致。
- `createVar` 的显示名称必须放在 `userData: { name }`,不能传顶层 `name`。缺少 `userData.name` 时变量虽可按 ID 读取,但极客版 UI 的变量选择器不会显示它。
- 删除变量前检查所有规则引用;删除是不可恢复操作。

### 工具缺失或写入失败时

按以下顺序判断,不要直接宣布“网关无法读写变量”:

1. 查看当前 MCP 工具列表是否包含上述专用变量工具。
2. 工具缺失时检查运行中的 MCP 是否为旧构建;源码新增工具后必须重新构建并重启 MCP,当前进程不会动态注册新工具。
3. 检查 `src/core/tools/variable.ts`、`src/mcp/tools/variable.ts` 和实际运行的 `dist`,确认功能是未实现、未构建还是未加载。
4. `mijia_set_variable` 返回变量不存在时,改用 `mijia_create_variable`,不要尝试用规则节点自动创建。
5. `mijia_call_api` 拒绝 `createVar` 等写方法时,改用专用工具,不要放宽通用工具的只读白名单。
6. 若怀疑功能曾存在但被回归删除,检查 Git 历史或会话中的真实工具调用记录,再下结论。

实机验证过的生命周期:创建临时变量 -> 读取配置和值 -> 修改 -> 回读 -> 删除 -> 再次读取确认不存在。创建或恢复变量工具后应完整执行一次该流程,并清理临时变量。

发现网关尚未封装的新能力时,读取 [网关能力发现方法](references/gateway-capability-discovery.md)。不要靠猜测 API 名称,也不要因为当前 MCP 没有工具就判定网关不支持。

## 规则结构

```json
Expand Down Expand Up @@ -233,9 +267,9 @@ metadata:

### deviceGetSetVar - 查询设备赋值
```json
{"id":"$ID","type":"deviceGetSetVar","cfg":{"urn":"$URN","name":"deviceGetSetVar","version":1},"props":{"did":"$DID","siid":$SIID,"piid":$PIID,"dtype":"number","scope":"global","id":"$VAR_ID"},"inputs":{"input":null},"outputs":{"output":["$NEXT1.trigger"],"output2":["$NEXT2.trigger"]}}
{"id":"$ID","type":"deviceGetSetVar","cfg":{"urn":"$URN","name":"deviceGetSetVar","version":1},"props":{"did":"$DID","siid":$SIID,"piid":$PIID,"dtype":"number","scope":"global","id":"$VAR_ID"},"inputs":{"input":null},"outputs":{"output":["$NEXT.trigger"]}}
```
⚠️ 同 deviceGet,`inputs` 用 `input`,outputs 必须有 `output``output2`。
⚠️ `inputs` 用 `input`。极客版 UI 实测只生成 `outputs.output`,没有 `output2`;不要套用 `deviceGet` 的双输出结构

### varChange - 变量值更新时触发(state 节点)
```json
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 网关能力发现方法

这套流程用于发现网关已经支持、但当前 Core/MCP 尚未封装的能力。目标是形成可复核的证据链,而不是试猜接口名。

## 证据优先级

1. **米家前端参考代码**:搜索仓库 `ref/` 中前端 bundle 实际调用的方法名、参数和响应处理。
2. **项目现有封装**:搜索 `src/core/` 和 `src/mcp/`,确认该方法是已封装、部分封装还是完全缺失。
3. **真实规则与设备能力**:读取现有规则和 `mijia_get_device`,从网关已保存对象反推节点结构、URN、字段和值域。
4. **安全的原始 API 验证**:只读方法可通过 `mijia_call_api` 验证。写入、删除和未知方法不能通过放宽白名单试探。
5. **专用工具的最小可逆实验**:确认写 API 后先实现类型化 Core 包装和专用 MCP 工具,再用临时对象验证完整生命周期并清理。

`getApiList` 在部分网关固件上可能报内部错误,不能把它当成唯一的能力清单。猜测的 API 返回“方法不存在”也只能否定该名称,不能否定整类能力。

## 从证据到实现

1. 在 `ref/` 搜索功能关键词及候选方法名,例如 `createVar`、`deleteVar`、`getVarConfig`、`getVarValue`。
2. 搜索所有 `gateway.callApi(...)` 和 `server.registerTool(...)`,列出现有 API 与 MCP 工具。
3. 对两份清单做差集,确认缺口,不要重复实现已有功能。
4. 从前端调用点提取真实参数结构,不凭方法名猜参数。
5. 在 `src/core/tools/` 添加最小包装,遵循 `{ success, data/error }` 返回约定。
6. 在 `src/mcp/tools/` 注册专用工具并做输入校验。高风险写操作不要加入通用 `mijia_call_api` 白名单。
7. 导出新函数、运行类型检查和 MCP 构建,然后重启 MCP。工具列表在进程启动时固定,旧进程看不到新注册工具。
8. 用临时对象执行创建、读取配置、读取值、修改、回读、删除、确认不存在的完整测试。
9. 把实测约束写入 Schema、测试和 skill,避免后续模型重复试错。

## 变量能力的发现实例

历史实现遵循了上述路径:

1. 在米家前端参考 JS 中找到 `createVar`、`deleteVar`、`getVarConfig`、`getVarValue`、`setVarConfig` 和 `setVarValue`。
2. 对比代码发现 MCP 当时只封装了变量列表和 `setVarValue`,因此确认是封装缺口,而不是网关缺少变量能力。
3. 新增 Core 包装和 `mijia_create_variable`、`mijia_delete_variable`、`mijia_get_variable_config`、`mijia_get_variable_value`。
4. 编译并重启 MCP 后,第一次使用带下划线的临时 ID 返回 `Invalid id format`。
5. 改用纯字母数字 ID 后创建成功,随后删除成功,由此确认变量 ID 必须匹配 `^[a-zA-Z0-9]+$`。
6. 后续完整生命周期测试再次确认创建、读取、修改和删除均可用。

这里最重要的结论不是某个变量 API,而是诊断边界:

- 当前工具列表只说明当前 MCP 进程暴露了什么。
- `mijia_call_api` 白名单只说明通用维护入口允许什么。
- 规则节点失败只说明节点运行语义,不代表底层配置 API 不存在。
- 网关能力需要由前端调用证据、源码实现和真实网关实验共同确认。

## 回归排查

如果某项能力以前成功、现在工具消失:

1. 查 Git 历史和当前分支,确认代码是否在整理其他 PR 时被误删。
2. 对比 `src` 与实际运行的 `dist`,确认是否只缺构建。
3. 重启 MCP,排除旧进程仍在使用旧工具清单。
4. 若普通日志不足以证明历史行为,可查询 OpenCode SQLite 会话中的原始 tool part;成功调用的输入和输出比文字总结更可靠。
5. 恢复后补自动测试和 skill 说明,不只恢复代码。
Original file line number Diff line number Diff line change
Expand Up @@ -1173,13 +1173,12 @@ deviceInput → condition1
"id": "var_brightness"
},
"inputs": {"input": null},
"outputs": {
"output": ["nextNode.trigger"],
"output2": ["fallbackNode.trigger"]
}
"outputs": {"output": ["nextNode.trigger"]}
}
```

极客版 UI 实测该卡片只有一个 `output`,查询成功并完成赋值后触发;它没有 `output2`,不要套用 `deviceGet` 的双分支结构。

---

### varChange — 变量值更新时触发
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type { GatewayManager } from './gateway/manager';
// 工具函数
export { getDevices, getDevice } from './tools/device';
export { getGraphs, getGraph, createGraph, updateGraph, deleteGraph, toggleGraph } from './tools/graph';
export { getVariables, setVariable } from './tools/variable';
export { getVariables, setVariable, createVariable, deleteVariable, getVariableValue, getVariableConfig } from './tools/variable';
export { callGatewayApi, READ_ONLY_GATEWAY_METHODS } from './tools/misc';
export { validateGraphCapabilitiesWithGateway } from './tools/capabilityValidation';
export { validateGraph, layoutNodes } from './tools/base';
Expand Down
6 changes: 5 additions & 1 deletion src/core/tools/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const STATE_NODE_TYPES = new Set([
'timeRange', 'alarmClock', 'onLoad', 'deviceInputSetVar', 'varChange',
]);

const DUAL_OUTPUT_TYPES = new Set(['deviceGet', 'varGet', 'deviceGetSetVar']);
const DUAL_OUTPUT_TYPES = new Set(['deviceGet', 'varGet']);

const STATE_CONDITION_INPUT_TYPES = new Set(['condition', 'logicOr', 'logicAnd', 'logicNot']);

Expand Down Expand Up @@ -149,6 +149,10 @@ export function validateGraph(graph: Graph): ValidationError[] {
if (!('output2' in o)) errors.push({ nodeId: node.id, type: 'missing_output2', level: 'error', message: `${node.type} 必须声明 outputs.output2` });
}

if (node.type === 'deviceGetSetVar' && !('output' in (node.outputs || {}))) {
errors.push({ nodeId: node.id, type: 'missing_output', level: 'error', message: 'deviceGetSetVar 必须声明 outputs.output' });
}

if (node.type === 'delay' && !('input' in (node.inputs || {}))) {
errors.push({ nodeId: node.id, type: 'delay_wrong_input', level: 'error', message: 'delay inputs 必须用 "input",不是 "trigger"' });
}
Expand Down
55 changes: 53 additions & 2 deletions src/core/tools/variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import type { ToolResponse } from '../types';

export async function getVariables(gateway: GatewayClient, scope: string = 'global'): Promise<ToolResponse<Variable[]>> {
try {
const variables = await gateway.callApi<Variable[]>('getVarList', { scope }, 10000);
return { success: true, data: Array.isArray(variables) ? variables : [] };
const variables = await gateway.callApi<Variable[] | Record<string, Variable>>('getVarList', { scope }, 10000);
const data = Array.isArray(variables)
? variables
: Object.entries(variables || {}).map(([id, variable]) => ({ ...variable, id, scope }));
return { success: true, data };
} catch (error) {
return { success: false, error: `获取变量列表失败: ${error}` };
}
Expand All @@ -23,3 +26,51 @@ export async function setVariable(gateway: GatewayClient, id: string, value: str
return { success: false, error: `设置变量值失败: ${error}` };
}
}

/**
* 创建变量。
* ⚠️ 网关要求 id 必须是纯字母数字(不能含下划线/连字符),否则返回 "Invalid id format"。
*/
export async function createVariable(
gateway: GatewayClient,
id: string,
type: 'number' | 'string',
value: number | string,
name?: string,
scope: string = 'global'
): Promise<ToolResponse> {
try {
const displayName = name?.trim() || id;
await gateway.callApi('createVar', { scope, id, type, value, userData: { name: displayName } }, 10000);
return { success: true, message: `变量 ${id} 创建成功` };
} catch (error) {
return { success: false, error: `创建变量失败: ${error}` };
}
}

export async function deleteVariable(gateway: GatewayClient, id: string, scope: string = 'global'): Promise<ToolResponse> {
try {
await gateway.callApi('deleteVar', { scope, id }, 10000);
return { success: true, message: `变量 ${id} 删除成功` };
} catch (error) {
return { success: false, error: `删除变量失败: ${error}` };
}
}

export async function getVariableValue(gateway: GatewayClient, id: string, scope: string = 'global'): Promise<ToolResponse> {
try {
const result = await gateway.callApi<{ value: string | number }>('getVarValue', { scope, id }, 10000);
return { success: true, data: { id, scope, value: result.value } };
} catch (error) {
return { success: false, error: `获取变量值失败: ${error}` };
}
}

export async function getVariableConfig(gateway: GatewayClient, id: string, scope: string = 'global'): Promise<ToolResponse> {
try {
const config = await gateway.callApi('getVarConfig', { scope, id }, 10000);
return { success: true, data: { id, scope, config } };
} catch (error) {
return { success: false, error: `获取变量配置失败: ${error}` };
}
}
2 changes: 2 additions & 0 deletions src/core/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export type ToolResponse<T = unknown> = ToolResult<T> | ToolError;

/** 变量 */
export interface Variable {
id?: string;
scope?: string;
type: 'number' | 'string';
value: number | string;
userData: {
Expand Down
Loading