开发 Nemu 的第四天:真实 Beta 如何把一帧判断改造成状态机

开发 Nemu 的第四天:真实 Beta 如何把一帧判断改造成状态机

第三天,Nemu 已经能把钉钉单聊的本地画面变化变成受控候选。但真实 Beta 很快指出一个问题:桌面行为不是一组彼此独立的截图,而是一段有前因后果的连续状态。

前三篇分别记录了 8 月 26、27、28 日。8 月 29--30 日是周末,main 没有新提交,所以第四篇继续按"有效开发日"编号,记录下一个有实现进入可信主线的工作日:2026 年 8 月 31 日

这一天只有两个合并提交,但它们都来自真实验收的反例:

  • B1 暴露了"重置计数"和"重新绑定"并没有形成一条原子边界;
  • B3 暴露了"当前帧没有输入变化"不等于"用户已经结束输入"。

最终,原本偏瞬时的判断被改造成了两套明确的状态机制:Beta EpochStateful Input Guard

B1:界面上的"清零",为什么不一定代表新一轮测试

第三天已经有一组 Session-only Beta 指标:本地采样、Monitor 事件、ASK、AUTO、Vision 尝试,以及人工标记的正常命中、误触、重复和漏检。

最初的"重置计数"只是把 Main 进程里的显示数字变成零。但 Core 仍然可能保留:

  • 已累计的 local_sample_count
  • 有界事件队列里的旧事件;
  • 尚未成熟的候选;
  • 已准备但未执行的 Vision Candidate;
  • 正在进行中的异步 Vision 请求。

这样得到的"新一轮"其实混入了上一轮状态。即使界面显示为零,旧候选也可能随后返回,旧采样总数也会让测试人员误以为新一轮已经发生了大量采样。

问题的本质不是 UI 计数器,而是缺少一个跨 Core 与 Electron Main 的测试轮次边界。

Beta Epoch:清理瞬态工作,但保留用户确认的绑定

第四天为此增加了 reset-beta-epoch。一次重置不再是本地变量赋零,而是一条明确的策略边界:

这里有一个刻意保留的状态:当前不可变 Direct Binding 不会因为重置测试数据而丢失。

重置 Beta 轮次和重新授权一个聊天不是同一件事。前者只清理瞬态测试状态,后者才改变用户批准的会话边界。如果每次重置都强制重新绑定,测试流程会变得笨重;如果重置保留旧候选,又会污染新一轮数据。

图中蓝色锚定关系表示仍然有效的 Direct Binding;越过重置边界的旧候选、旧事件和旧 Vision 结果则全部失效。这里的"清除"只指本地待处理状态,已经发送到云端的数据仍然无法撤回。

为什么要同时有 Core Generation 和 Main Epoch

Core 与 Main 各自拥有不同的异步工作:

  • Core 的 generation 用于让重置期间已经开始的采样结果失效;
  • Main 的 betaEpoch 用于忽略重置前已经开始、重置后才返回的 Vision 成功或失败回调。
text 复制代码
executionEpoch != current betaEpoch
        ↓
ignore stale completion
        ↓
do not increment new-round metrics
do not reopen old Portal activity

只有两端都切断旧异步结果,新一轮计数才真正干净。

为什么事件计数必须由 Main 单调持有

Core 的事件列表是有界诊断队列,最多只保留有限数量的近期事件。它适合读取最新候选,不适合反推整轮测试到底发生过多少事件。

如果用"当前队列长度"充当累计计数,就会出现:

text 复制代码
产生 40 个事件
Core 队列最多保留 32 个
界面错误显示:32

第四天把 Beta 事件计数改为 Main 拥有的单调 Session Counter。Core 事件被 Main 首次消费时计数一次,之后即使 Core 队列滚动淘汰旧事件,累计值也不会倒退。

本地采样则使用另一种语义:重置时读取一份 fresh Core Status,把当时的 local_sample_count 记录为 local_sample_baseline,界面只显示:

text 复制代码
本轮采样 = 当前 Core 累计采样 - 本轮基线

这既不要求 Core 破坏自己的运行累计值,也能让测试人员看到真正从零开始的一轮。

重新绑定:旧 Binding 不能掩盖一次新失败

B1 还暴露了一个更隐蔽的问题:用户点击"重新绑定"后,新绑定可能因为前台焦点没有及时回到钉钉而失败,但旧 binding_ref 仍然存在。

旧逻辑主要通过"是否存在 Binding"计算 needs_direct_binding,因此可能得到一个矛盾状态:

text 复制代码
旧 binding_ref 仍存在
新绑定返回 FOREGROUND_WINDOW_MUST_BE_DINGTALK
        ↓
needs_direct_binding = false
        ↓
重试循环误以为已经成功

修复后,只要当前存在未解决的 bindFailureCode,状态就继续要求绑定。只有一次新的绑定真正成功,失败码才会被清除。旧绑定不能替新操作"冒充成功"。

六秒截止时间必须覆盖整条调用链

第三天已经为焦点交接设计了"最多等待 6 秒,每 500 ms 重试"。但仅在外层写一个重试循环还不够。

一次绑定前可能先后执行:

  1. 获取 Core 状态;
  2. 必要时同步 Consent;
  3. 读取旧事件,避免回放;
  4. 请求绑定当前 Direct 会话。

如果其中任意一个 HTTP 请求仍使用自己的 8 秒超时,所谓"6 秒截止"就只是表面约束。用户可能等到 8 秒甚至更久,外层才得到控制权。

第四天改为传递同一个绝对 deadlineMs。每个 Loopback 请求都会计算剩余时间,并用 AbortController 在截止点真实终止:

状态、授权同步或绑定中任何一段卡住,都会得到有界的 DINGTALK_DIRECT_BIND_DEADLINE_EXCEEDED,而不是无限延长交接窗口。

B3:为什么"这一帧没在输入"还会误触发

输入抑制的第一版逻辑看起来合理:如果输入区发生变化,就把当前帧标记为 user_is_typing,不产生候选。

问题在于,真实编辑会影响不止一帧:

  • 输入文字让输入框变高,聊天区域发生重排;
  • 删除草稿让输入框恢复,消息列表再次位移;
  • 光标、候选框或微小渲染变化可能短暂出现;
  • 输入区先稳定,聊天区可能晚一帧才稳定。

因此会出现这样的序列:

text 复制代码
Frame A:输入区变化 → 正确抑制
Frame B:输入区暂时不变,但聊天区仍在回流
Frame C:旧逻辑把回流误认为新消息

单帧布尔值无法表达"用户刚刚输入过,界面还在恢复"。

Stateful Input Guard:输入结束后仍有隔离期

第四天加入了有状态的输入 Guard。只要检测到输入活动,它会:

  1. 立即清除尚未成熟的消息候选;
  2. 进入 input_guard_active
  3. 记录最后输入时间;
  4. 同时收集聊天区和输入区的恢复帧;
  5. 把输入引起的布局变化刷新为 Guard Baseline。

输入变化消失后,Monitor 也不会立刻恢复候选判断。它必须同时满足:

  • 至少 2 秒没有新的输入活动;
  • 聊天区域连续稳定;
  • 输入区域连续稳定。

恢复成功的那一帧仍然只返回 USER_TYPING_RECOVERED,不会顺便成熟候选。只有之后真正出现的新变化,才重新进入消息判定链。

这相当于把输入及其布局余震放进一个短暂隔离区。

Fail closed 也不能变成"永远锁死"

状态化 Guard 的另一个风险,是一次细小的输入区渲染噪声可能让 Monitor 永久停在恢复状态。

因此测试不仅覆盖"输入不能误触发",也覆盖相反方向:一个微小像素噪声触发 Guard 后,只要经过安静、稳定恢复,之后的真实对方消息仍然必须恰好产生一个 ASK 事件。

这体现了 fail closed 的准确含义:不确定时暂缓,而不是发生一次不确定后永久失能。

重置测试轮次,也必须重置状态机

B1 与 B3 最终在 reset_beta_epoch 汇合。

新一轮场景开始时,Core 会:

  • 增加 Generation,使正在采集的旧帧失效;
  • 清空旧事件和 Candidate Plan;
  • 清空输入 Guard 及恢复帧;
  • 在仍可信的 Live Tail 上刷新基线;
  • 保留当前不可变 Direct Binding;
  • 返回 BETA_SCENARIO_EPOCH_RESET

同时,正在执行的 Vision 会被 best-effort 取消,Main 也会通过 Epoch 拒绝旧回调。这样测试人员点击"重置"后,看到的才是一条真正干净的场景边界。

这一天增加了哪些回归证据

新增测试专门覆盖过去容易被"看起来没问题"掩盖的序列:

回归场景 预期结果
草稿输入、聊天重排、清空草稿 整段都不成熟候选
已有候选后开始输入 旧候选立即隔离并清除
输入结束后的恢复帧 必须安静且双区域稳定,仍不触发
恢复后出现新的对方消息 恰好产生一次 ASK
微小输入区噪声 可以恢复,不永久锁死 Monitor
Reset 发生在采样进行中 旧采样结果失效,Binding 保留
Reset 后出现新的真实消息 新一轮仍能正常检测
Reset 前已有 Candidate / Vision 新一轮不可观察到旧结果
状态、配置或绑定请求挂起 在统一截止时间内被真实 Abort
旧 Binding 存在但重新绑定失败 继续显示需要绑定并继续有界重试

合并提交中还保留了两份 Windows CI 证据:B1 修复对应 CI #287 SUCCESS,B3 修复对应 Windows Nemu CI #298 SUCCESS。它们证明提交当时通过了项目 CI,但仍不等于 B1--B10 的完整真实钉钉验收。

第四天的代码变化

从第三天日终 fee425d 到第四天日终 28a7364

  • 2 个合并提交;
  • 13 个文件变化;
  • 966 行新增;
  • 105 行删除;
  • 日终快照仍有 214 个受 Git 跟踪文件;
  • Python 测试文件 45 个;
  • Python test_* 函数从 311 增至 318。

两个提交分别是:

text 复制代码
d5ba42f  fix: 修复钉钉 B1 诊断计数与重新绑定边界 (#80)
28a7364  fix: 加固 B3 输入抑制与 Beta 测试轮次重置 (#83)

这一天没有增加新的 Screenshot 范围、Cloud Egress 类型或自动操作权限。变化集中在已有 Direct Beta 内部的状态一致性和诊断可信度。

在第四天日终快照上重新验证

本文的 fresh 验证固定在 2026 年 8 月 31 日最后一个 main 提交:

text 复制代码
28a73646f64903dc5c9f9d6f07328ed2c56b4ab9
fix: 加固 B3 输入抑制与 Beta 测试轮次重置 (#83)

独立 Worktree 的结果:

text 复制代码
Python / pytest
344 collected
343 passed
1 skipped
1 warning

Desktop
Renderer TypeScript typecheck: PASS
Electron Main TypeScript typecheck: PASS
Vite production build: PASS

DingTalk Vision spike
6 tests
6 passed
0 failed

与第三天日终相比,Python 测试集合从 335 项增加到 344 项。唯一跳过项仍来自既有 Windows 文件系统能力测试;唯一 warning 仍是测试依赖中的 Starlette / httpx 弃用提醒。

第四天最重要的四个结论

1. 桌面感知必须理解时间,而不只是理解当前帧

输入、布局回流和稳定恢复是一段过程。只把 user_is_typing 当成当前帧布尔值,会在下一帧重新制造误判。

2. "重置"是跨进程策略边界,不是 UI 赋零

Core Candidate、采样 Generation、Main Counter 和异步 Vision 回调必须一起切换 Epoch,新一轮数据才可信。

3. 有界截止时间必须传到最内层 I/O

外层重试 6 秒、内层请求却能挂 8 秒,不是真正的 6 秒限制。Deadline 必须沿整条调用链传播,并能真实 Abort。

4. Fail closed 的另一面是可恢复

不确定时不触发,但安静稳定后必须恢复观察。安全机制如果永久锁死产品,也不能算正确实现。

验收状态仍然没有改变

第四天显著收紧了 B1 和 B3,但不能据此把钉钉能力标记为已支持。正式状态仍是:

BETA GATE / NOT SUPPORTED

B1 与 B3 的修复证明真实测试正在产生价值,却也证明单一视觉通道仍然容易受到窗口布局、输入重排和时间连续性的影响。只有 B1--B10 在真实 Windows 环境全部满足零误触、零重复和受控场景零漏检,才能讨论 SUPPORTED

下一篇:B5 为什么漏掉了真正的新回复

解决"不要把用户输入当成来信"之后,下一轮真实测试会走向相反的问题:用户没有输入,对方确实发来了一条消息,为什么 Nemu 仍可能没有产生 B5 事件?

这会把问题继续推进到帧连续性、变化方向和安全诊断轨迹:不仅要知道"没有触发",还要知道候选在哪一道本地 Gate 消失了。

相关推荐
空想兔1 小时前
AgentPulse:实时监控所有的 OpenCode Agent 跑到哪一步了
react.js·ai编程
弈栈录1 小时前
上下文管理:如何让 AI 记住用户之前说过的话
aigc
大刚测试开发实战2 小时前
用例写不完、回归跑不动?我把最磨人的活交给 TestHub,KPI 反而稳了
ai编程·测试·claude
LadiesAndGentlemen2 小时前
环球GeoAI-遥感VLM|2026-08-31|More with Less:通用 VLM 不换架构,也能在遥感基准上打平专用模型
人工智能·神经网络·目标检测·自然语言处理·aigc
杨杨杨大侠2 小时前
一次大模型 API 请求是怎么跑起来的:从 Harness 到 GPU、并发与 KV Cache
aigc·openai·ai编程
AI工具测评家2 小时前
2026知网维普AIGC检测原理拆解:快降重、笔过AI、快将AI三款降AI工具底层技术对比
人工智能·aigc·降重·ai检测·查重·降ai
Behavior4 小时前
刚刚,Claude 5.1 发布!全球最强模型来了?
aigc·claude·vibecoding
wangruofeng5 小时前
2000+ 小时实战后,我的 Agentic Engineering 全套装备「精译」
aigc·agent·ai编程
露天赏雪5 小时前
掘金小册样章
java·开发语言·人工智能·ai编程