开箱即得的协议行为
以下这些全部由投影推导得出。它们一行代码都不用写——也正因如此,最容易被忽略。
tools/list 里到底有什么
不是所有已打开的窗口——只有可操作的那些。这个列表是两层过滤的乘积:
- 哪些窗口可操作 —— 模态会阻塞它下面的窗口(见下文)。
- 该窗口的哪些动作可用 —— 用
set_available(false)隐藏的动作不会出现。
也就是说,一个工具出现的条件是:它所属的窗口可操作且该动作本身可用。其余一概不出现;任何一层发生变化都会重新计算列表并通知客户端。
可操作性与模态链
打开一个模态会改变 Agent 可以操作哪些窗口。Purview 会重新计算 operableWindowIds,并把被阻塞窗口的工具从 tools/list 中移除:这是"该窗口当前不可用"最明确的信号。
let confirm = win.open_modal("Confirm"); // window-modal: blocks its owner
let alert = app.open_app_modal("Alert"); // app-modal: collapses to its own tree
confirm.close(); // operability is restored| 打开方式 | 对可操作集合的影响 |
|---|---|
app.open(..) | 一个普通的顶层窗口 |
win.open_modal(..) | 模态打开期间,其所属窗口变为不可操作 |
app.open_app_modal(..) | 只有该模态自身所在的窗口树保持可操作 |
这是交接,不是叠加
打开模态并不会把它的工具添加到父窗口的工具之上。父窗口的工具会消失,取而代之的是模态的工具。一个正在父窗口上做事的 Agent 会发现那些工具就这么不见了。
有三点值得记住:
只有链尾可操作。 对于 B → D1 → D2,B 和 D1 都被阻塞——D1 自身是模态,但它同时也是 D2 的所属窗口。只有 D2 会贡献工具。
无关窗口不受影响。 模态阻塞的是它自己的所属窗口,而不是整个应用。如果 A 打开了模态,而 C 就在旁边,那么 C 依然可操作、工具照旧。唯一的例外是应用级模态,它会把自身窗口树之外的一切都收缩掉。
服务端并没有"冻结"任何东西。 可操作性决定的是"提供什么",它不是一把锁。对一个刚被阻塞的工具发起的过期调用,仍会在运行时被校验并以 blocked_by_modal 拒绝——参见错误与校验。
设计准则
模态必须自带能关闭它的工具。如果父窗口的工具都消失了,而模态又不提供任何工具,Agent 就彻底卡死了——它连这个对话框都关不掉。
乐观并发
每个窗口都带有一个 version。工具调用可以携带 expectedVersion;一旦不再匹配,Purview 会在你的处理器运行之前用 stale_state(连同当前版本号)拒绝该调用。两个 Agent 不可能悄无声息地互相覆盖,而这一切你不用写任何代码。
其他
openedWindowIds。 处理器执行期间打开的窗口会被记录下来,并在该次调用的结果中报告,因此 Agent 能立刻知道它们的 id。
通知。 版本号递增会触发 resource-updated 通知,经过合并后广播给所有会话。
脱敏。 被标记为敏感的字段永远不会出现在 app://windows 中。
动态 tools/list。 只有当前可操作的窗口才会贡献工具,且每次变化都会重新计算。