一句话根因:每一层存的都是「上次已知值」,端出去时却按「当前值」的形状呈现 —— 「没读到设备」和「设备就是这个值」在数据上不可区分。
1. 现象
一条 cron:「主卧床头灯还开着就用音箱播报睡眠提醒」。2026-08-02 凌晨每 30 分钟播报一次,把睡着的人吵醒。同一夜、同一台设备,写路径响亮失败,读路径静默成功 :02:00 起音箱自己也离线,写路径立刻 -704042011 / success=0 落台账,读路径全程 code:0。
8 次触发里 6 次 trajectory 可解析,6/6 都先跑了 device list、拿到 offline、复述了 offline,然后照样相信读数 :
ASSISTANT: "The light is showing as \"offline\". Let me still try to check its status..."
RESULT: {"code":0,"message":"Device status retrieved successfully","data":
{"properties":[{"iid":"prop.2.1","value":true,"code":0}]}}
ASSISTANT: "The light is ON (value=true). So I need to play a reminder..."
不是没判断,是判断了、判反了 —— 响应体自我描述为权威 。而它是在遵守本仓库文档:SKILL.md:193/:179/:137 全是写路径语义 ,异常表没有一行提到 properties[]。
2. 复现
33 台设备、6 台离线,全程只读。云端原始行带 updateTime,后端丢掉了;同一时刻直连:
设备
online
ds=1(miloco 走的)
ds=2(读真机)
加湿器
false
code:0,182.4 天前
-704042011
净饮机
true
code:0,25.1 小时
code:0 updateTime=now
在线设备也会给出一天前的读数 —— 「在线/离线」不是判据轴,「多老」才是。 离线设备也不是 一律 code:0:电饭煲同一次查询里 10 条缓存命中 code:0、5 条云端自己报 -704042011。
3. 根因(文件:行)
backend/miloco/src/miloco/miot/service.py:1015-1026 —— 逐属性只取 value/code,同一行 r 里的 updateTime 被丢弃 。这是全后端唯一返回属性值的 return。
backend/miot/src/miot/cloud.py:849 —— 全仓唯一一处硬编码 {"datasource": 1},无注释。实测 ds=1=云端缓存,ds=2=读真机(不可达设备由云端自己 返回 -704042011)。纯透传,miloco 侧无本地缓存。
plugins/skills/miloco-devices/SKILL.md:193 —— 文档只覆盖写路径,等于在训练 agent 相信「没报错 ⇒ 值是活的」。
4. 影响面
全仓读设备属性只有 3 处:
miot/service.py:1015 → CLI device props → agent:事故路径 。CLI 输出没有任何新鲜度字段,而 device list 有 online。
rule/runner.py:1336 幂等预检:后果更重 —— 陈旧值 == 目标值 → 报 result=True 跳过,而这个 early return 在所有 _write_action_ledger 之前 → 动作永久丢失、台账无痕。断电后缓存停在 on,规则「开灯」永不重试。
miot/client.py:1376 read_cameras_awake:已正确处理(code != 0 → None 三态),现成样板。
WebUI 靠 real.ts:850 的 if (!d.online) 躲过(#441 );#290 疑似第三个受害者;静态规则 condition 不读属性,不受影响。
5. 正面回应「这不该在 miloco 修」
「code 是云端的执行码,本地不该伪造」 —— 成立,正是 fix(cli): treat only negative MIoT result codes as device failure #394 的立意。所以修法一个字节都不伪造 :ds=2 下的 -704042011 是云端自己返回的。
「那就加个在线态闸门」 —— 当场抓到反例,接受为设计约束 :33 台两次逐台比对漂移 1 台,恰好是床头灯(cached=False / cloud=True,真机读它是活的、亮着的)。非相机设备既无周期刷新也不订阅上下线(client.py:884)。危害不对称:今天的假阳吵 ,online 闸门换来的假阴静默 。所以只能 additive 加信息,不能拿 online 当判决。
「该在 openclaw 那条 cron 里改」 —— cron 已停,但 rule/runner.py:1336 在本仓库内 ,同一个 bug。
6. 建议改法
透出云端的 updateTime —— 逐属性 updated_at(缺失给 null),CLI 换算 age_s;它是 as-of 注解,不是 「设备还活着吗」。
datasource 提成参数 ,默认仍 1;传了 iid 的定点查询走 ds=2,全量冷查询走 ds=1 (复用已有的 user_specified;WebUI 不传 iid,留在便宜的缓存读上)。事故 cron 传了 iid,零改动即判据为假。
rule/runner.py:1336 单独传 ds=2 —— /status 的修复不会惠及它;已有的 code == 0 判据无需改动。
SKILL.md 收窄离线口径到控制路径、补查询侧规则 (文档是近因)。
⚠️ ds=2 有批量悬崖,必须分批。 约 4.2s 服务端截止时间且全或无 :属性数超过该设备能在时限内答完的数量,整批 返回 -704220043、不带 updateTime。悬崖位置因设备而异、对同一台设备确定:
空调 14 个 ok(369ms) → 15 个整批失败(4374ms)
净化器 16 个 ok(857ms) → 20 个整批失败(4739ms)
洗衣机 16 个 ok(368ms) → 20 个整批失败(4179ms)
而端点收逗号分隔的多个 iid(router.py:316),33 台里 11 台 可读属性 ≥15 个 —— agent 一次点名就会把一台健康在线的设备整台报成「读不到」,把响亮的假阳换成静默的假阴 。按 8 分批后 42/42 全 code:0,还比不分批更快 (1758ms vs 4198ms 后整批失败)。
成本(实测): ds=2 在线 15 属性 167249ms(ds=1 为 61ms)、离线 6790ms(云端秒拒,不等超时);30 台并发 290ms vs ds=1 的 356ms,未触发限频。
被否掉的: 离线时改写 properties[].code —— 伪造云端没返回的码、判据是不可信的缓存 online、还会覆盖云端逐属性给出的真实结果。
7. 诚实的开放问题
ds=2 会不会唤醒电池 / 休眠设备并耗电,没测 (本机 6 台温湿度计全在线,无可测样本)—— 这是推荐方案最可能在别的部署里出问题的地方。
updateTime 的精确云端语义无文档 ,我的解读(= 该值上次被写入的时刻)是从三个观察反推的,所以只拿它做 as-of 注解。
事故当晚灯在不在线无法回溯,「灯当晚真开着、判据本就该为真」的可能我没排除 ;online 漂移也只有两个时刻的快照。但 ds=1 的读数在两种情形下都无法自证。
last_online 只有 WiFi 设备有 —— cloud.py:716-731 只取了 isOnline,而云端对蓝牙 / mesh 设备不提供 last_online 。可用 max(逐属性 updated_at) 替代:实测与 WiFi 设备的 last_online 逐台吻合(加湿器 182.9 天、空调 5.5 天、电饭煲 3.7 天),并且能给出 mesh 驱蚊器的 21.6 天。
未重新检索上游是否已有同类 issue。
一句话根因:每一层存的都是「上次已知值」,端出去时却按「当前值」的形状呈现 —— 「没读到设备」和「设备就是这个值」在数据上不可区分。
1. 现象
一条 cron:「主卧床头灯还开着就用音箱播报睡眠提醒」。2026-08-02 凌晨每 30 分钟播报一次,把睡着的人吵醒。同一夜、同一台设备,写路径响亮失败,读路径静默成功:02:00 起音箱自己也离线,写路径立刻
-704042011 / success=0落台账,读路径全程code:0。8 次触发里 6 次 trajectory 可解析,6/6 都先跑了
device list、拿到 offline、复述了 offline,然后照样相信读数:不是没判断,是判断了、判反了 —— 响应体自我描述为权威。而它是在遵守本仓库文档:
SKILL.md:193/:179/:137全是写路径语义,异常表没有一行提到properties[]。2. 复现
33 台设备、6 台离线,全程只读。云端原始行带
updateTime,后端丢掉了;同一时刻直连:code:0,182.4 天前-704042011code:0,25.1 小时code:0 updateTime=now在线设备也会给出一天前的读数 —— 「在线/离线」不是判据轴,「多老」才是。 离线设备也不是一律
code:0:电饭煲同一次查询里 10 条缓存命中code:0、5 条云端自己报-704042011。3. 根因(文件:行)
backend/miloco/src/miloco/miot/service.py:1015-1026—— 逐属性只取value/code,同一行r里的updateTime被丢弃。这是全后端唯一返回属性值的 return。backend/miot/src/miot/cloud.py:849—— 全仓唯一一处硬编码{"datasource": 1},无注释。实测ds=1=云端缓存,ds=2=读真机(不可达设备由云端自己返回-704042011)。纯透传,miloco 侧无本地缓存。plugins/skills/miloco-devices/SKILL.md:193—— 文档只覆盖写路径,等于在训练 agent 相信「没报错 ⇒ 值是活的」。4. 影响面
全仓读设备属性只有 3 处:
miot/service.py:1015→ CLIdevice props→ agent:事故路径。CLI 输出没有任何新鲜度字段,而device list有 online。rule/runner.py:1336幂等预检:后果更重 —— 陈旧值 == 目标值 → 报result=True跳过,而这个 early return 在所有_write_action_ledger之前 → 动作永久丢失、台账无痕。断电后缓存停在on,规则「开灯」永不重试。miot/client.py:1376read_cameras_awake:已正确处理(code != 0 → None三态),现成样板。WebUI 靠
real.ts:850的if (!d.online)躲过(#441);#290 疑似第三个受害者;静态规则 condition 不读属性,不受影响。5. 正面回应「这不该在 miloco 修」
code是云端的执行码,本地不该伪造」 —— 成立,正是 fix(cli): treat only negative MIoT result codes as device failure #394 的立意。所以修法一个字节都不伪造:ds=2下的-704042011是云端自己返回的。client.py:884)。危害不对称:今天的假阳吵,online 闸门换来的假阴静默。所以只能 additive 加信息,不能拿 online 当判决。rule/runner.py:1336在本仓库内,同一个 bug。6. 建议改法
updateTime—— 逐属性updated_at(缺失给null),CLI 换算age_s;它是 as-of 注解,不是「设备还活着吗」。datasource提成参数,默认仍 1;传了iid的定点查询走 ds=2,全量冷查询走 ds=1(复用已有的user_specified;WebUI 不传 iid,留在便宜的缓存读上)。事故 cron 传了 iid,零改动即判据为假。rule/runner.py:1336单独传ds=2——/status的修复不会惠及它;已有的code == 0判据无需改动。SKILL.md收窄离线口径到控制路径、补查询侧规则(文档是近因)。ds=2有批量悬崖,必须分批。 约 4.2s 服务端截止时间且全或无:属性数超过该设备能在时限内答完的数量,整批返回-704220043、不带updateTime。悬崖位置因设备而异、对同一台设备确定:而端点收逗号分隔的多个 iid(
router.py:316),33 台里 11 台可读属性 ≥15 个 —— agent 一次点名就会把一台健康在线的设备整台报成「读不到」,把响亮的假阳换成静默的假阴。按 8 分批后 42/42 全code:0,还比不分批更快(1758ms vs 4198ms 后整批失败)。成本(实测): ds=2 在线 15 属性 167
249ms(ds=1 为 61ms)、离线 6790ms(云端秒拒,不等超时);30 台并发 290ms vs ds=1 的 356ms,未触发限频。被否掉的: 离线时改写
properties[].code—— 伪造云端没返回的码、判据是不可信的缓存 online、还会覆盖云端逐属性给出的真实结果。7. 诚实的开放问题
ds=2会不会唤醒电池 / 休眠设备并耗电,没测(本机 6 台温湿度计全在线,无可测样本)—— 这是推荐方案最可能在别的部署里出问题的地方。updateTime的精确云端语义无文档,我的解读(= 该值上次被写入的时刻)是从三个观察反推的,所以只拿它做 as-of 注解。last_online只有 WiFi 设备有 ——cloud.py:716-731只取了isOnline,而云端对蓝牙 / mesh 设备不提供last_online。可用max(逐属性 updated_at)替代:实测与 WiFi 设备的last_online逐台吻合(加湿器 182.9 天、空调 5.5 天、电饭煲 3.7 天),并且能给出 mesh 驱蚊器的 21.6 天。