高级治理阅读约 26 分钟最后核对 2026-09-20

电脑操作

Spark Work 电脑操作(Computer Use)完整指南:Agent 直接操作真实桌面的能力。先讲清两条架构完全不同的使用路径——原子动作(会话模型自己逐步操作,每个会话一个隐式会话、空闲 5 分钟释放)与委派的 start_task(独立决策循环,各自的任务契约预算不同),再展开权限模型:会话权限层里 fail-closed 的 computer_direct_action / computer_unknown 硬闸,与生产路径上更早生效的自动放行名单的关系,以及 Broker 层「策略只做风险分级、不做内容拦截」的真实边界(evaluate 在所有分支返回 allow,唯一 deny 是 focus_mismatch;任务契约里 allowedApps / allowedDomains / forbiddenActions 等字段没有任何读取点)。执行内核部分覆盖 L0–L4 风险计算与哪些升级规则在生产不可达、十个原子工具与 click 的语义改写、坐标按「模型看到的图」换算、type_text 带 into/submit 会拆成最多三个动作、五个控制工具的真实语义(pause 与 takeover 逐行等价、resume 只有 MCP 路径会重启算子、bind_target 必须先暂停)。底层部分覆盖原生宿主为什么必需、双向签名校验与本地信任旁路、5 字节帧头 stdio 协议与「没有请求级取消,超时即 SIGKILL 宿主」、心跳与每会话一次的重启预算、元素树渲染与 treeVersion 的 stale 语义、以及 macOS 与 Windows 的能力不对等。数据部分覆盖三套尺寸的截图、AES-256-GCM 加密快照库与保留策略(只有证据帧有 24 小时 TTL、整体无磁盘配额)、预览令牌的 5 分钟 TTL 与 turn 边界撤销、17 项凭据类应用黑名单的实际覆盖面。最后是界面可达性(设置页只有授权与诊断、那套设置没有界面、全新安装不会武装紧急停止键)与 10 个 V2 开关里哪 4 个没有任何消费者。文末附 30 行排查表。

电脑操作(Computer Use)是 Spark Work 里唯一允许 Agent直接操作你的真实桌面的能力: 看屏幕、读元素树、点击、输入、滚动、拖拽,以及把一整个多步目标委派给它自己跑。 它的实现方式和其他能力都不同——不是纯 Electron/JS,而是一个独立的原生宿主子进程(macOS 是 Swift 写的SparkComputerHost,Windows 是 Rust 写的 spark-computer-host),主进程只能通过 stdio 帧协议指挥它。

这一页按真实代码讲清六件事:两条使用路径(原子动作与委派任务)、权限为什么是两道独立闸门、 策略引擎的实际强制力边界、原生宿主的信任与自愈、快照的加密与隐私边界、 以及 10 个 V2 开关里哪 4 个其实没有任何消费者。

先给结论:这套能力在代码层面是「可观测 + 可接管 + 有界预算」的, 但它不是沙箱——策略引擎对动作只做风险分级、不做内容拦截, 多数「安全字段」是声明而非强制。下面逐条给出证据。

1. 定位与真实入口

电脑操作是「会话内的即时能力」,不是长期资源:它绑定在某个 Agent 会话与某一轮的上下文里, 没有独立的项目管理页。你能看到的界面只有三处:设置页、聊天流里的活动卡片、系统托盘。

1.1 入口与界面文案

  • 设置页:分组「系统」下的「电脑操作」, 项 id 为computer-use,图标Icons.Monitor, 搜索关键词是 ['辅助功能', '屏幕录制', '输入控制', '系统权限', 'computer use'](apps/desktop/src/renderer/design/views/SettingsView.tsx:449-457)。 渲染的是 ComputerUseSettingsSection(同文件:545)。
  • 该页顶部说明原文是:「系统授权集中在这里管理。授权完成后,Agent 发起的电脑操作任务默认直接执行,不再逐步审批。」(design/computer-use/ComputerUseSettingsSection.tsx:154-157)。 这句话就是整套设计的核心口径。
  • 聊天流:电脑操作过程以卡片形式出现在消息之间,由 ComputerActivityProvider/ComputerActivitySegmentsBridge/ ComputerActivitySegmentCard渲染 (design/views/ChatView.tsx:5147-5168)。
  • 系统托盘:ComputerControlTrayService,菜单里三条动作的文案是**「暂停 Agent」/「立即接管」/「停止控制」**(apps/desktop/src/main/index.ts:685/693/701)—— 注意这与聊天卡片里的「暂停 / 接管 / 停止」不是同一套文案,别混用。

一个容易踩的细节:设置页搜索框把关键词整串当子串匹配,而关键词里写的是带空格的 computer use,所以搜带连字符的computer-use搜不到这一项 (SettingsView.tsx:517-521)。

1.2 27 个工具的全貌

电脑操作给模型暴露27 个工具,名字都在mcp__spark_computer__ 命名空间下, 清单硬编码在COMPUTER_USE_AGENT_TOOL_NAMES(apps/desktop/src/main/services/computer-use/ComputerUseMcpProvider.ts:6-37), HTTP 桥侧还有一份完全相同的白名单ALLOWED_TOOLS(ComputerUseAgentBridge.ts:12-41)。按职责分四组:

分组工具数量
观测get_capabilities、diagnose_native_host、list_apps、list_windows、get_screen_state、get_app_state、capture_app_snapshot、screenshot8
原子动作click、type_text、paste、set_value、invoke_element、press_key、scroll、drag、select_text、perform_secondary_action10
委派任务start_task、get_status、wait_for_completion、open_app、bind_target5
控制pause、resume、stop、takeover4

工具一旦挂载,会话权限层不会再对它逐次弹审批(见第 3 节)。 另外要注意:MCP 服务器是无条件挂载给所有会话的,只判 computerUseMcpProvider != null,并没有「本会话是否开启电脑操作」这个开关 (packages/agent-runtime/src/services/session.service.ts:2926-2940)。 真正生效的门槛在每次调用时的能力预检。

1.3 两条使用路径

同一个工具面里有两种架构完全不同的用法,这是理解整篇文档的关键:

原子动作路径委派任务路径
入口工具click/type_text等 10 个start_task
谁做决策会话模型自己(它就是电脑操作 agent)独立的决策循环ComputerTaskOperator
决策模型当前会话的模型单独的 Provider,可按会话解析(resolveDecisionModel)
节奏一次工具调用 = 一个动作,返回新树 + 新截图后台循环自己跑,直到完成 / 失败 / 需要接管
会话对象隐式会话,每个 Agent 会话一个,空闲 5 分钟释放显式computerSession,带任务契约与预算
契约预算maxSteps: 2000/maxRuntimeMs: 12h/ maxConsecutiveNoops: 20maxSteps: 100/maxRuntimeMs: 20min/ maxConsecutiveNoops: 8

两条路径共用同一个执行内核(ComputerControlBroker→ 原生宿主), 但原子动作不经过ComputerTaskOperator。 契约常量分别在ComputerAtomicActionService.ts:52-69与 ComputerUseAgentController.ts:430-441。

2. 系统权限与能力

电脑操作依赖操作系统级授权,这是唯一需要你手动参与的环节。 代码里没有绕过系统授权的路径:拿不到权限就是能力不可用。

2.1 平台支持矩阵

协议里声明了 macOS / Windows / Linux 三种平台与多种后端技术 (packages/protocol/src/computer-use/native-wire.ts:38:screen_capture_kit/windows_graphics_capture/ xdg_portal/x11, 无障碍为axui_element/ uia/at_spi), 但仓库里只实现了 macOS 与 Windows 两个宿主:apps/desktop/native/ 下只有macos/与windows/, 且工厂函数把非 mac/win 或非 arm64/x64 直接降级为不可用后端 (NativeHostBackendFactory.ts:54-59)。Linux 目前没有任何可运行实现。

能力macOS(Swift)Windows(Rust)
截图ScreenCaptureKit(需 macOS 14+)Windows.Graphics.Capture
元素树AX,深度 48、元素上限2000UIA,深度 30、元素上限100000
后台输入AX 语义 +CGEventPostToPidPostMessageW+ UIA 语义
前台输入CGEventSendInput
视觉兜底✅ Vision OCR❌ 无(UIA 为空即报错)
虚拟光标指示✅MacVirtualCursor❌ 无
键盘布局自适应✅ 按当前布局解析❌ 硬编码 VK 表
锁屏检测错误码有screen_locked分支无对应概念
安全桌面检测无对应概念✅ 拒绝截图/输入

**不要把两平台当能力对等。**元素上限差 50 倍 (MacControlPolicy.swift:19vswindows_host.rs:337), mac 有 OCR 与虚拟光标兜底而 Windows 没有,Windows 有安全桌面概念而 mac 用锁屏错误码代替。 另外 macOS 的树渲染文本预算是 48k (NativeAXTreeRenderer.swift:62,注释明确要求「必须 ≤ TS 侧上限」), 而 Windows 用的是 90k(tree_render.rs:25), 但 TS 侧决策前只截 48k(ComputerDecisionAdapter.ts:23)—— 也就是说 Windows 侧超出的部分会被客户端静默截断,长界面可能丢尾部元素。

2.2 macOS 的三项授权

macOS 需要三项系统权限,代码里都是「先 Preflight 探测、按需 Request 弹窗」:

  • 屏幕录制:CGPreflightScreenCaptureAccess()/ CGRequestScreenCaptureAccess()(MacScreenCaptureProvider.swift:38-39、:64); 缺权时报 screen_permission_denied(可重试)。
  • 辅助功能:AXIsProcessTrusted()/ AXIsProcessTrustedWithOptions()(:70、:1130);缺权时报accessibility_permission_denied。
  • 事件投递:CGPreflightPostEventAccess()/ CGRequestPostEventAccess()(:71、:1135)。 注意这一项缺失也报accessibility_permission_denied, 没有独立错误码(MacScreenCaptureProvider.swift:789)。

界面上的两项授权行对应关系是:「屏幕录制」= screen、「辅助功能与输入控制」= accessibility + input 两项同时满足(ComputerUseSettingsSection.tsx:78-91)。 这里有个组合态容易困惑:状态文字优先显示 input 的状态,而「已授权」要求两个都 granted—— 于是「辅助功能已授权但输入控制未授权」时,会显示「待授权」但按钮变成「打开系统设置」 (因为 state 已不是not_determined)。

点「请求授权」时,只有not_determined才会真正调系统弹窗 (app-snapshot:request-permissions);其他状态直接跳系统设置页 (computer-use:open-system-settings)。 深链文案是:「请前往“系统设置 → 隐私与安全性 → 屏幕录制/辅助功能”允许 Spark Work(开发模式下可能显示为 Electron)。」(:264-265)。

2.3 Windows 的能力探测

Windows没有授权弹窗,也没有denied/ not_determined状态: 它把三项能力按运行时探测结果直接映射成granted 或restricted(windows_host.rs:328-332)。request_permissions请求在 Windows 上只是重新探测一次 (windows_host.rs:131-132),不会弹任何系统界面。 UIA 不可用时执行动作报 environment_unavailable(windows_host.rs:367-372)。

协议层的权限状态枚举是五值:granted/denied/ not_determined/restricted/unsupported(native-wire.ts:19),macOS 会用到前三者,Windows 只用前四者中的两个。

2.4 能力探测的关键常量

客户端对get_capabilities结果做了缓存,刷新间隔是1 秒(NativeHostClient.ts:27、:185), 所以你在设置页改完权限、切回应用窗口时,界面会在下一个焦点事件后自动重测 (ComputerUseSettingsSection.tsx:59-70监听focus与 visibilitychange)。

「已就绪」的判定是四项同时成立:available === true且 screen / accessibility / input 三项都是 granted(:248-255)。任一项不满足就显示「电脑操作尚未就绪」, 并展示 unavailableReason原文,或兜底文案「请完成下方系统授权并重新检测。」

3. 权限与审批:两道闸门的真实关系

这是整篇最容易写错的地方。代码里存在两套与电脑操作有关的放行逻辑, 但它们当前的实际效果是「都不逐动作拦」——原因却完全不同。先把结论摆出来:

  • 会话权限层里确实有一条 fail-closed 硬闸:computer_direct_action与computer_unknown永不批准, 且连测试都锁死了这条语义。
  • 但生产路径上,电脑操作工具在到达这条硬闸之前就被引擎的自动放行名单放行了, 所以它不会真的被这条规则拦到。
  • **执行层(Broker)**不做逐动作审批: 策略引擎只做风险分级,动作以 approvalTicketId = null直接执行。

3.1 第一道:会话权限层

动作名由resolveComputerPermissionAction映射 (packages/agent-runtime/src/computer-use/computer-permission-action.ts:38-44), 规则表在permission.service.ts:41-50,三个内置 profile 共用同一份:

动作scopemode覆盖的工具
computer_observesessionallow10 个观测 / 查询类
computer_task_startsessionallowstart_task
computer_pausesessionallowpause
computer_resumesessionallowresume、bind_target
computer_stopsessionallowstop
computer_takeoversessionallowtakeover
computer_direct_actionanydeny13 个低层动作名(click、type_text、set_value等)
computer_unknownanydeny其余任何mcp__spark_computer__*

requestApproval开头有两条短路,位置在一切 profile 查询、会话记忆与 forcePrompt之前(permission.service.ts:389-390):

if (isUnapprovableComputerAction(action)) return false
if (isGovernedComputerTaskAction(action)) return true

这个顺序很关键:mode === 'deny'在更靠后的代码里是可以被forcePrompt: true绕过的,但上面这两条短路在它之前就返回了, 所以电脑操作的两条 deny 规则无法被任何配置绕开。 回归测试把这条语义写死:即使把 computer_direct_action配成allow,mcp__spark_computer__click依然被拒且不弹卡(packages/agent-runtime/src/__tests__/services/permission.service.test.ts:365-380)。

注释把这个意图写得很直白(permission.service.ts:96-101): 受管 Computer Use 的高层工具*「在 requestApproval 入口直接放行, 确保旧库、自定义 profile、会话记忆和 forcePrompt 都不会重新引入应用内审批」*; 内置规则版本号也为此升到3(:105)。

3.2 但生产路径其实在更早处就放行了

这里有一个必须讲清、否则会误导的层次问题。工具在到达 PermissionService之前,会先经过执行引擎的canUseTool回调, 而该回调里有一条「自动放行名单」的短路,位置在审批回调之前。

Claude SDK 路径的实现(packages/agent-runtime/src/sdk/claude-sdk-executor.ts): 自动放行在:895-897,而宿主审批回调在:922:

if (callbackAllowedTools.has(toolName)) {          // :895
  return allowTool(input, callbackOptions.toolUseID, 'user_temporary')
}
...
const approvalCallback = config.approvalCallback      // :922
if (approvalCallback == null) return denyTool('Permission check failed', ...)

而电脑操作的全部 27 个工具名恰好就在这份名单里: MCP provider 把它们放进 allowedTools(ComputerUseMcpProvider.ts:66-68),并在紧邻的注释里写明了设计意图——「Desktop task authorization is task-scoped and identical in every permission mode.」(桌面任务授权是任务级的,在每个权限模式下都完全一致)。

也就是说:会话权限层里那条 fail-closed 硬闸是一条兜底网,而不是生产路径上真正生效的闸门。 它保证「任何绕过引擎自动放行名单的路径(自定义 profile、旧数据库、会话记忆、 强制弹卡)都无法批准一个低层电脑动作」,但正常使用时,mcp__spark_computer__click并不会走到它。 Codex 路径同理:其交互审批只覆盖命令执行与文件补丁两类,mcpToolCall只是一条通知,不产生审批 (codex-app-server-executor.ts:1473-1491)。

怎么理解这条设计:电脑操作的控制点被有意集中在执行层—— 即「用户同意开始一次桌面任务」这个动作本身就是授权, 之后不再逐步打断。设置页那句「授权完成后默认直接执行,不再逐步审批」正是这个意思。

动作风险等级另有一张映射表(permission.service.ts:186-194):computer_observe/computer_pause/computer_stop/ computer_takeover为low,computer_task_start/ computer_resume/computer_direct_action/ computer_unknown为high。 由于上面两条短路的存在,这些等级在当前实现里不参与拦截决策。

3.3 执行层:Broker 做了什么、没做什么

真正执行动作的是ComputerControlBroker。它在每次动作前做三件事:

  1. 新鲜度校验:动作携带的frameId与treeVersion 必须与当前观测一致,否则抛stale_frame/stale_tree(ComputerControlBroker.ts:344-351)。 这里有一个刻意的例外:不校验前台应用/窗口, 注释写明理由是「绑定任务必须在用户切到别的应用时继续操作它的目标窗口」 (:352-355)——这正是「不抢焦点、后台操作」的实现基础。
  2. 策略评估:调ComputerPolicyService.evaluate 得到风险等级与决策(见第 4 节)。
  3. 动作落库:写入computer_actions, 其中 approvalTicketId 硬编码为null(:389)。deny分支之后没有任何审批分支, 直接进入 startExecuting(..., null)(:211)。

有一个名字容易误读的错误码:approval_mismatch(:374-378)。 它与审批无关——触发条件是「同一个actionId被复用, 但动作参数 / 风险等级 / 策略决策不同」,本质是幂等保护。

3.4 审批链为什么是停用的

仓库里有一整套完整的审批实现(ComputerApprovalService、ComputerActionApprovalPresenter、NativeComputerActionApprovalPrompt), 包含票证摘要、过期语义、远端只能批 L2 等细节,但在当前模型下整条链没有接到任何生产路径上。证据有四条:

  • 接口上直接标了废弃:{/** @deprecated Persisted approval UI compatibility; direct execution never creates this. */}(ComputerTaskOperator.ts:90-91);构造参数同样标注 「never read by direct execution」(:128-129), 且 takeApprovedTicket在算子体内从未被调用。
  • createComputerActionApprovalPresenter与 createNativeComputerActionApprovalPrompt 只有定义、零生产调用点,引用只出现在各自的测试文件里。
  • requiresUserPresence在策略决策里恒为false(ComputerPolicyService.ts:66-76两处decision(...) 的最后一个实参都是false), 而任务契约里的userPresence恒被写成 'required'——两者从未被比较。
  • 策略层从不产出require_approval/require_handoff: 这两个值只出现在协议 schema、数据库 CHECK 约束与校验器里,没有任何decision(...)调用传过它们。

渲染端还留着一个故意封死的通道:computer-use:approve-action 永远抛错,文案是*「Computer actions can be approved only through the native one-time confirmation surface」*(registerComputerUseIpc.ts:294-302)。 也就是说,即使未来要做逐动作审批,也不打算走渲染端界面。

唯一与「完全放行」相关的判断是isFullAccess: 它只认claude-bypass 与codex-full-access两个模式 (ComputerActionApprovalPresenter.ts:51-53,注意不含任何spark-*模式)。但既然这条链已停用,它当前不产生实际差异。

4. 策略引擎与它的真实边界

所有动作的风险判定都来自ComputerPolicyService.evaluate(apps/desktop/src/main/services/computer-use/ComputerPolicyService.ts:43-82), 它唯一的调用点是 Broker(ComputerControlBroker.ts:181)。 这个类只有 129 行,但它的边界必须讲清楚,否则很容易写出「策略引擎会拦截危险动作」这种错误结论。

4.1 L0–L4 是怎么算出来的

风险等级由两个来源取较大值:动作基线风险,与策略上下文里的 effect 风险。

动作基线表(ComputerPolicyService.ts:26-41):

基线动作
L0observe、move、scroll、wait_for
L1focus_window、select_text、set_value、type_text、paste_text、invoke_element、click、drag、keypress、app_command

effect 风险表(:10-16):read_only→L0、reversible_local→L1、external_write→L2、high_impact→L3、restricted→L4。

三条升级规则(:59-73):

  • policyContext.target.kind === 'unknown'→ 至少 L2。
  • 敏感文本写入 → 若数据类含credential则L4,否则 L2。
  • 本地文本写入且数据类含非public→ 至少 L2。

但要注意这些升级规则里有一部分在生产路径不可达, 因为 policyContext的唯一生产者是policyContextFor(ComputerActionPolicyContext.ts:9-59,被算子与原子路径共用),而它:

  • 只产出 3 种 effect:read_only、reversible_local、external_write(:45-52)。 因此high_impact(L3)与 restricted(L4)无法从生产代码产生—— 它们只出现在测试里。
  • 只产出element或window 两种 target kind (:53-55),所以unknown→ L2 那条规则也不可达。
  • 只产出 3 种数据类组合:[]、['public']、['credential'](:56)。 所谓 personal / financial / health 等分类在生产路径不会出现。

还有一个设计上值得注意的点:policyContextFor用一个正则匹配模型自己写的intent文本来决定 effect 是否升级为external_write(:26-29,命中 send|submit|publish|post|purchase|buy|pay|delete|remove|confirm|book|order 或对应中文词即升级)。也就是说,风险等级有一部分取决于模型怎么描述自己的意图, 而不是纯粹取决于动作本身。

顺带一个反直觉的结论:invoke_element在普通语义调用时 (action为空或为'invoke')既不算 read_only、也不算 reversible_local, 会落到external_write→L2。 也就是说最常用的「点按钮」动作拿到的是 L2,而带select/expand 等显式子动作时才是 L1(:35-40与policyContextFor:35-43的差异)。

4.2 唯一的硬拒绝是 focus_mismatch

这是本节最重要的结论:evaluate 在所有分支上都返回'allow',唯一的'deny'出现在开头—— 观测到的应用 id 与动作的目标应用 id 不一致时 (ComputerPolicyService.ts:52-54):

if (observedApp.id !== envelope.targetAppId) {
  return decision(envelope.actionId, 'L1', 'deny', 'focus_mismatch', false)
}

其余所有情况都走到底部的allow(:75-81),reason 只有两种: L0 时是 read_only_action,其余是within_task_scope。 连 L2 的「外部写入」也一样放行——测试标题直接写着*「allows external writes and high-impact actions without per-action approval」*(ComputerPolicyService.test.ts:87)。

换个说法:这个策略引擎的产出是「风险标签」,不是「拦截器」。 它会告诉你某个动作多危险(写进computer_actions.risk_level, 并在活动卡片里展示),但不因此阻止它。 真正的刹车是预算、新鲜度校验、用户接管与kill switch,而不是内容策略。

另外focus_mismatch在原子路径上几乎不会出现, 因为 envelope 的 targetAppId就是observation.foreground.app.id(ComputerAtomicActionService.ts:208),必然自洽。 实际抛这个码的是原生宿主与快照服务,不是策略层。

4.3 任务契约里哪些字段真的生效

start_task会构造一个ComputerTaskContract(ComputerUseAgentController.ts:430-441)。 这个契约有 10 个字段,但策略引擎完全不用它——evaluate的形参名是_taskContract(下划线表示未使用,ComputerPolicyService.ts:46)。实测各字段的强制力:

字段生产值是否真的生效
objective用户目标✅ 进决策提示词与日志
successCriteria由目标/验收条件推导✅ 验收引擎实际使用
maxSteps100✅ 超限抛task_step_limit_exceeded
maxRuntimeMs1200000(20 分钟)✅ 超限抛task_runtime_exceeded
maxConsecutiveNoops8✅ 连续无变化即中止
allowedApps恒[]❌无任何读取点
allowedDomains恒[]❌无任何读取点
allowedDataClasses恒['public','internal','personal']❌无任何读取点
forbiddenActions恒[]❌无任何读取点
userPresence恒'required'❌ 从不与requiresUserPresence比较

后 5 个字段的声明本身就在协议里写了理由——它们标注为桌面执行不使用应用白名单 (packages/protocol/src/computer-use/session.ts:55-57的注释)。 所以文档里不能写「任务契约限制了可操作的应用/域名/动作」, 当前实现里没有这个约束。生产者只有两处,且写的都是空值/常量:ComputerAtomicActionService.ts:61-68与 ComputerUseAgentController.ts:433-440。

同理,bind_target的工具描述里写着*「bind to a new window whose strong application identity is already allowed by the task contract」*(ComputerUseAgentBridge.ts:516-517),但代码从不校验allowedApps。照工具描述写文档会出错。

5. 原子动作链路

这条路径让会话模型自己成为电脑操作 agent:一次工具调用只下发一个动作, 返回新的元素树与截图,模型据此决定下一步。这是 Codex 风格的决策架构, 与 start_task的委派模型并存 (ComputerAtomicActionService.ts:74-80 的类注释)。

5.1 十个原子工具

原子工具名记录在ATOMIC_TOOL_NAMES,控制器据此分流 (ComputerUseAgentController.ts:190-192→:487-501):click、type_text、paste、set_value、 invoke_element、press_key、scroll、drag、 select_text、perform_secondary_action。 每次调用都会重做一次能力预检(:493-496), 所以中途撤销系统权限会立刻生效。

工具优先级在系统提示词里有明确排序(computer-use-system-prompt.ts的 ## Tool priority段):语义元素动作 > 元素定向点击 > 原始截图坐标 > 键盘。 坐标是最后手段,且是最新截图里的像素坐标、左上角原点。

5.2 click 的语义改写与坐标换算

两个容易踩的实现细节:

  • 元素左键单击会被改写成语义调用,不是合成鼠标事件:当at是 elementId、按钮为左键(默认)、点击次数为 1 时, 实际下发的是 invoke_element(ComputerAtomicToolHandlers.ts:250-268)。 这也是「不抢焦点」能成立的原因——走的是无障碍语义,不是全局鼠标事件。
  • 坐标要按模型看到的图换算:模型收到的截图可能已被降采样, 所以坐标换算用的是「返回给模型那张图的尺寸」而非原始截图尺寸, 并且会被clamp01 夹到 [0,1] 的归一化窗口坐标 (:532-545、:584)。

另一个反直觉的点:type_text/paste带into时, 会先额外下发一次invoke_element(action: 'focus'); 若再带submit: true,还会追加一次keypress ['Enter'](:411-443、:445-477)。 也就是说一次「工具调用」在时间线与动作表里可能是三条记录。 排查时按工具名数动作会数错。

5.3 隐式会话与空闲释放

原子路径为每个 Agent 会话维护一个隐式电脑会话, 复用的状态是 preflighting/observing/planning/ waiting_approval/acting(ComputerAtomicActionService.ts:27-33)。 空闲5 分钟(IDLE_RELEASE_MS = 300_000,:50)后释放, 下次调用再透明重建。

空闲计时器有两个细节:用unref()不阻塞进程退出(:125), 且 observe/dispatch 进入时先清掉计时器、finally 再重新装(:147、:186-191)——注释解释了原因: 计时器若在动作中途触发,会伪造出一个 session_canceled。

为什么释放要这么久?源码注释给了解释:推理模型在两次工具调用之间常常思考数分钟, 太短的计时器会让任务卡片在过程中反复消失又出现(原文用了「断断续续」这个词), 并强制每次调用都重启截图流。macOS 的持久捕获另有 90 秒帧空闲停放, 用来在不打断会话的前提下熄灭系统的「屏幕共享」指示。

陈旧错误会自动自愈一次:stale_frame/stale_tree/ focus_mismatch命中后, 会自动重新观测、重建动作并重试一次(:19、:173-185)。 所以模型看到stale_tree时更可能应该换一个新元素 id, 而不是重复同一个调用。

6. 委派任务链路

start_task让 Agent 把一整个目标交出去,由独立的决策循环在后台跑到结束。 它走的不是渲染端 IPC,而是主进程内置的 HTTP MCP 服务器。

6.1 从 MCP 调用到算子循环

完整链路(每一步都有对应的失败收敛):

  1. MCP 桥:ComputerUseAgentBridge校验 bearer 令牌后, 把 tools/call转成controller.invoke(sessionId, toolName, args)(ComputerUseAgentBridge.ts:677-683)。 令牌是 32 随机字节的 base64url,TTL 30 分钟且每次鉴权滑动续期(:551-559、:708-722)。
  2. 能力预检:start_task先取getCapabilities(), 不支持执行就直接抛(ComputerUseAgentController.ts:409-412), 并检查本轮上下文是否存在(:413-414)。
  3. 环境校验:只接受my_desktop,其余抛*「This build currently provides governed execution only on My Desktop」*(:416-418)。工具入参的 Zod 也把environment收成 z.literal('my_desktop')(:659)。
  4. 目标解析:给了targetApp就按应用标识解析并直接前台化; 否则用 targetWindowId,都没有则报错(:419-424)。
  5. 决策模型解析:resolveDecisionModel(sessionId)(:425)。
  6. 建会话:写computer_sessions行并记 computer_session_started事件(ComputerSessionManager.ts:123-147)。
  7. 占用桌面通道:coordinator.claim(computerSession.id, sessionId)(:459)。
  8. 激活并起循环:sessions.activate()把状态置 observing,然后launchOperator在后台跑 ComputerTaskOperator.run()(:460-463)。

失败会回滚:除session_canceled外的异常都会broker.stop+ coordinator.release(:468-476)。

6.2 桌面独占通道

同一时刻只能有一个任务真正驱动桌面,由ComputerDesktopExecutionCoordinator 串行化(ComputerDesktopExecutionCoordinator.ts:30-69)。规则是非对称的:

  • 同一个 Agent 会话再次 claim:只是转移通道, 不杀兄弟会话——这样start_task与原子截图/点击可以并存(:38-41)。
  • 不同的 Agent 会话claim:会停掉前一个持有者及其全部同名会话(:46-63)。

值得注意的是sessions.activate()不创建持久化 actuator lease(ComputerSessionManager.ts:231-255,注释说明 lease 由主进程的 coordinator 接管)。 所以computer_actuator_leases表与actuator_lease_conflict 错误码在这条路径上主要作为兜底存在。

6.3 状态与事件枚举

会话状态共11 个(packages/protocol/src/computer-use/session.ts:16-28, 数据库侧是同一份):

preflighting | observing | planning | waiting_approval | acting
| verifying | paused | handoff_required | completed | failed | canceled

其中要区分三组不同用途的子集:

  • 后端认的执行态只有 4 个:observing/planning/ waiting_approval/acting(EXECUTABLE_STATUSES,ComputerSessionManager.ts:26-31)—— 注意不含 preflighting与verifying。
  • 主进程可写入的 phase 有 7 个(:33-40)。
  • 对 Agent 可见的算子运行态是另一层:running/completed/failed/ handoff_required/not_running(ComputerUseAgentController.ts:34-37、:575)。

waiting_approval这个状态虽然被列进可执行集合与 phase 类型,但没有任何生产代码把会话置为该状态,与第 3.4 节「审批链停用」是同一个事实。

时间线事件共14 种(packages/protocol/src/computer-use/events.ts:24-142):session_started、observation_created、action_requested、action_blocked、action_executed、action_failed、approval_requested、approval_resolved、verification_started、verification_completed、handoff_required、session_completed、session_failed、session_canceled。 其中approval_requested同样没有任何生产发出点。

还有一个排查陷阱:暂停不产生时间线事件。

所以活动卡片的状态徽标只看最后一条事件, 非终态一律显示「进行中」(ComputerActivityBlock.tsx:381-396)—— 你按了暂停,卡片可能仍显示「进行中」。

7. 五个控制工具的真实语义

这五个工具的名字都很好懂,但代码行为与名字的暗示有两处明显偏差。 入口在 ComputerUseAgentController的switch。

7.1 pause 与 takeover 在主进程路径上逐行等价

两者都是「硬暂停」:调broker.pause、释放通道、作废本次运行记录。

case 'pause': {                                    // :320-326
  const paused = await services.broker.pause(computerSession.id)
  services.coordinator.release(computerSession.id)
  this.invalidateRun(services, computerSession.id)
  return { computerSession: paused }
}
case 'takeover': {                                 // :334-340
  const paused = await services.broker.pause(computerSession.id)
  services.coordinator.release(computerSession.id)
  this.invalidateRun(services, computerSession.id)
  return { computerSession: paused }
}

唯一的差别在渲染端 IPC 路径:computer-use:takeover会额外记录 owner 归属,供后续resume/bind-target做归属校验 (registerComputerUseIpc.ts:245-254)。 但无论哪条路径,它都不会把窗口或输入交给用户,也不激活任何窗口—— 「接管」的实际含义是「停下 Agent,让用户自己接手」。

底层broker.pause做三件事(ComputerControlBroker.ts:314-320): 清观测缓存、撤销待审批、executor.cancelSession()中断在途动作;随后会话状态置paused并 abort 掉当前 controller (ComputerSessionManager.ts:319-336)。 在途打字会被停在半途,拖拽会松开按键。

7.2 resume 只有一条路能真正恢复运行

MCP 路径的resume会重新拉起算子循环(:341-368):校验必须是paused→ 重新解析决策模型 → 重新 claim 通道 → broker.resume→launchOperator。 失败会把会话重新暂停并释放通道(:360-367)。

而渲染端 IPC 版的resume只调broker.resume,不重启算子(registerComputerUseIpc.ts:208-232)。 如果它被调用,会话会停在 observing而没有任何驱动。 好消息是渲染端当前根本没有调用它(见 12.2)。

7.3 wait_for_completion 是有界等待,超时不算失败

它的实现是「先看当前状态,若已是会返回的状态就直接返回, 否则挂到会话状态订阅上等,直到超时」(:591-629)。 会立即返回的状态集合是 6 个(:39-46):completed/failed/canceled/paused/ waiting_approval/handoff_required。

  • 超时参数默认120 秒,允许范围 100–300000 毫秒(:707-712)。
  • 超时返回体里带timedOut: true,不代表任务失败。
  • 命中终态时还会等算子收尾完成再返回(finishAfterCleanup,:609-615),所以返回值里的operator字段是可信的。
  • 任务失败时额外附带一条 continuation 提示,让 Agent 向用户报告失败并询问兜底方案 (:571-589)。

7.4 bind_target 的先决条件是「已暂停」

这是唯一能显式更换目标窗口的工具,而且必须在会话paused 状态下调用,否则抛错*「Pause the Computer Use task before changing its bound target window」*(ComputerUseAgentController.ts:303-319)。

绑定后的行为变化(NativeHostComputerUseBackend.ts:370-376):

  • 清掉该会话的观测缓存,强制下一步必须重新观测。
  • 后续观测不再跟随前台窗口变化,而是锁定绑定的窗口与所属应用。

这正好解释了系统提示词里那句「你的会话绑定在它自己的应用上: 用户中途切到别的应用也继续干」——绑定是默认行为(start_task 时建立),bind_target是中途改绑。 界面上对应的是卡片里的「更换窗口」按钮,它只在暂停态出现(ComputerActivityBlock.tsx:318-327),窗口列表会过滤掉最小化的窗口。

另外stop的请求体里有一个reason字段, schema 接受但在 handler 里未被解构使用(协议ipc.ts:88-93vsregisterComputerUseIpc.ts:234)——传了也没用。

8. 验收是怎么判定的

委派任务不会「跑完就算成功」:它必须通过ComputerVerificationEngine对 successCriteria的逐条判定(ComputerVerificationEngine.ts:19-30)。 判定结果只有 4 种 reason:assertion_passed、assertion_failed、unsupported_evidence、model_visual_assertion。 整体通过的条件是「至少一条判据且全部通过」。

8.1 三种 kind 的判定规则

kind支持的断言判定方式
accessibilityexists/visible/enabled/focused /value_equals/text_contains在元素树里筛匹配元素,只看第一个
visualtext_present/text_absent把树文本 + 无障碍文本拼成字符串做includes
application_statefrontmost/window_exists/running/ window_title_contains比对前台应用 id 或窗口清单

accessibility的 selector 是「填了的字段都需匹配」的 AND 关系, 且只检查第一个匹配元素(:136的elements[0])—— 如果界面里有同名的多个元素,判定只看第一个。visible的判定是「存在且宽高都大于 0」(:121-152)。

application_state里有一条非对称规则值得记住:running在expected: true时用「存在窗口」证明它在运行; 但在 expected: false时,没有窗口并不能证明它没在运行, 于是返回 null→ 归为unsupported_evidence(:85-91)。写判据时不要依赖「应用未运行」这种否定断言。

8.2 visual 判定有一条模型兜底

这是整套验收里最需要讲清楚的一条。当一条visual判据的字符串匹配没有通过,但同时满足另外两个条件时,引擎会判通过, reason 记为model_visual_assertion(:48-69):

  • modelVisualApproval === true——由决策模型主动声明「可以验收了」时传入;
  • 原生树没有可用证据——即树版本不是visual-开头、 元素为空、且树文本是空 /[]/ 空的 diff。

这条兜底的用意是:Canvas、Electron 自绘界面等场景可能完全没有无障碍元素, 此时不能因为「字符串匹配不到」就永远判失败。 但它也意味着视觉类验收在无树可读时依赖模型自述——modelVisualApproval由算子在模型说「ready_for_verification」时传 true(ComputerTaskOperator.ts:278), 而自动校验路径传 false(:193)。

判据的来源有三层兜底:显式successCriteria→ 文本形式的 acceptanceCriteria(会被翻译成 visual 的text_present)→ 从 goal 里抽引号内文本 → 都拿不到时退化为application_state的 frontmost(desktop)(ComputerUseAgentController.ts:770-816)。

8.3 验收的两个现存缺口

缺口一:三种 kind 声明了但引擎不支持。

协议里的VerificationSpec允许dom/file/ external_readback三种 kind (packages/protocol/src/computer-use/verification.ts:164-207), 但 ComputerVerificationEngine里没有任何分支处理它们:passed保持null→ 归为unsupported_evidence→ passed = false,任务将永远无法通过验收, 只会一直把失败反馈给模型、直到步数或时长预算耗尽。

好消息是 MCP 侧start_task的入参 schema只暴露了三种可用 kind(ComputerUseAgentBridge.ts:70-157),所以正常调用不会踩到。 但「schema 支持」不等于「能验收」这个区别要记住。

缺口二:验收结果不落库。

ComputerTaskOperator的verifications依赖在构造函数里赋值后全文再无读取(声明于:117、赋值于:144,仅此三处),ComputerVerificationRepository.create/complete也零生产调用。 结果是computer_verifications表在生产路径恒为空, 会话完成事件里的 verificationIds恒为[](ComputerSessionManager.ts:394-400), 而 computer-use:get-verification必然返回null(该通道在渲染端也零调用)。

也就是说:验收判定在本次运行内生效(决定任务是否算完成),但不留档。 事后要复现「为什么判通过」只能看活动时间线里的verification_completed事件。

9. 原生宿主

这是电脑操作与其他所有能力最大的架构差异:真正的观测与输入不在 Electron 进程里, 而在一个独立的原生子进程中完成。

9.1 为什么必须有原生宿主

代码里不存在纯 Electron/JS 的替代实现——后端只有两个类: 真实现 NativeHostComputerUseBackend(NativeHostComputerUseBackend.ts:171)与占位UnavailableComputerUseBackend(ComputerUseBackend.ts:58)。 平台/架构不匹配时直接降级为不可用,没有 JS 兜底 (NativeHostBackendFactory.ts:54-59)。

原因从协议枚举就能看出来——它把后端技术写死成原生 API 名: screen 只能是 screen_capture_kit/windows_graphics_capture/ xdg_portal/x11, accessibility 只能是axui_element/ uia/at_spi, input 只能是cg_event/ send_input/xdg_remote_desktop(packages/protocol/src/computer-use/native-wire.ts:38)。 macOS 宿主实际调用的 API 包括 ScreenCaptureKit(SCScreenshotManager、SCContentFilter)、CGEventPostToPid、Vision OCR, 以及一个用dlsym动态解析的私有 SPI _AXUIElementPostNotification(MacFocusForger.swift:16-18说明该符号 「存在于所有已发布的 macOS 的 HIServices 中,但不在公开 SDK 头文件里」)。 Windows 宿主用 Windows.Graphics.Capture、UIA COM 与PostMessageW。 这些都是 JS 拿不到的能力。

macOS 上还有一条实践证据解释了为什么必须做原生无障碍读取:Electron/Chromium 应用必须设置私有属性AXManualAccessibility才会暴露内容树, 否则只能读到一个几乎空的树 (MacAccessibilityController.swift:42-44,注释把这件事称作「竞争对手能读到 Electron 应用结构树而我们之前只看到空树」的最大原因)。

9.2 双向签名校验

主进程用spawn拉起宿主,shell: false、stdio 管道 (NativeHostClient.ts:147)。信任是双向的:

主进程 → 宿主(校验可执行文件):

  • macOS:要求宿主的 Team ID 与当前 App 自己的签名 Team ID 一致, 并逐项核对标识符、SHA-256、文件名、平台、架构 (NativeHostArtifact.ts:282-288、NativeHostBackendFactory.ts:163-171); 还会用 codesign --verify --strict并生成 requirement 复核(:473、:607)。
  • Windows:用 PowerShell 取签名者证书 SHA-256,同样与 App 自身指纹绑定 (NativeHostArtifact.ts:592-596、NativeHostBackendFactory.ts:111-118)。
  • 文件级约束:manifest ≤ 64 KiB、可执行 ≤ 256 MB、必须是常规文件且非符号链接、 POSIX 下不得 group/world 可写、版本不得低于0.1.0(NativeHostArtifact.ts:11-14、:412-425)。

宿主 → 父进程(校验谁拉起了自己):

  • macOS 的ParentProcessTrustPolicy要求:宿主标识符为 com.spark-agent.desktop.computer-host、父标识符为 com.spark-agent.desktop、双方都必须anchor apple generic、 且双方 Team ID 非空且相等(ParentProcessTrustPolicy.swift:49-74)。 校验在宿主启动最前面执行,失败即exit(EX_NOPERM)(=77) (HostMain.swift:10-13)。
  • Windows 同样先验自己、再验父进程签名者指纹,并要求父进程产品名为Spark Work 且镜像路径以sparkwork.exe结尾 (runtime_auth.rs:45-80、parent_auth.rs:91-102)。 它用WinVerifyTrust 且只接受缓存内的吊销检查(可离线), 并且只信任已验证的叶子证书——注释明确说明绝不扫描未认证的 PKCS#7 证书袋, 因为攻击者可以往里面追加东西(runtime_auth.rs:221-235)。
  • 两端都做了防 PID 复用:取父进程 id 前后各一次并比对进程创建时间 (ParentProcessAuthorizer.swift:99-109、parent_auth.rs:81-84)。

有一套**「本地信任」旁路需要你知道:当trustMode === 'local'时, 主进程会向宿主注入环境变量SPARK_COMPUTER_LOCAL_TRUST=1(NativeHostClient.ts:143-146),宿主读到后直接跳过全部校验**(ParentProcessAuthorizer.swift:15-19); Rust 侧只在debug_assertions 或local-trustfeature 下认它。 而且有一条保护:本地信任 + 已签名 App 会被拒绝(抛native_host_untrusted),只有未签名 App(开发态)才允许 (NativeHostBackendFactory.ts:130-139)。 所以不要把这个分支当成生产链路的常态。

两个退出码在两平台是一致的:77 = 认证失败、76 = 协议失败(HostMain.swift:13、:29;windows_host.rs:28、:39)。

9.3 通信协议

帧格式非常简单,三份实现完全一致:

  • 5 字节定长帧头:4 字节大端长度 + 1 字节 kind (NativeHostFrameCodec.ts:1-3、NativeFrameCodec.swift:40-42、 frame_codec.rs:4)。
  • kind 只有两种:1 = json、2 = binary。 宿主只接受 JSON 帧作为请求,收到二进制即致命错误 (HostMain.swift:47、windows_host.rs:53-54)。
  • 载荷上限 64 MiB(67_108_864), 三份实现各有一个常量名(MAX_NATIVE_HOST_FRAME_PAYLOAD_BYTES/ maxNativeHostFramePayloadBytes/MAX_FRAME_PAYLOAD_BYTES)。 注意帧头里的长度是载荷长度、不含那 5 字节头(frame_codec.rs:106),且空载荷被视为协议错误。
  • 二进制帧必须紧邻它的 JSON 描述符:客户端在收到二进制时若不在等待状态, 直接抛Native Host binary payload was not adjacent to its descriptor(NativeHostClient.ts:498-500)。

请求8 种(native-wire.ts:167-213,Swift / Rust 侧同名):get_capabilities、request_permissions、list_windows、capture_window、observe、execute_action、cancel_session、ping。 响应8 种:capabilities、windows、capture_result、observation、action_result、ack、pong、error。 协议版本1,三处常量一致(native-version.ts:3、 NativeHostProtocol.swift:4、protocol.rs:9)。

超时与取消是这个协议里最容易误解的部分:

机制行为证据
默认请求超时20 秒NativeHostClient.ts:26
超时上限180 秒:29
动作类超时按动作时长 + 宽限 + 6 秒 skyshot 预算动态计算:636-651
超时后果整个宿主进程被 SIGKILL:402-404
AbortSignal 触发同样是杀进程,不是取消单个请求:407-411
cancel_session请求只让宿主「忘记该会话状态」,不杀进程:298-300

进程内没有请求级取消——这是必须讲清的一点: 取消一个动作的代价是重启整个宿主进程。为此客户端做了一个兜底: 终止前先 stdin.end(),让宿主的输入守卫有机会补发鼠标/按键抬起事件, 避免按键卡住(:623-625,宽限 300 毫秒,:30)。

并发模型是单 FIFO 车道(operationTail), 但有容量上限:64 个在飞 + 64 个排队(:31-32)。超限返回 environment_unavailable并附诊断码 native_host_request_capacity_reached/ native_host_operation_capacity_reached(:352-357、:392-397)。

会话取消在宿主侧是粘性的:取消后再执行会被拒 (NativeHostRequestHandler.swift:203-204、:232)。 两个平台的饱和策略不同:macOS 用有界 FIFO 驱逐(上限 64,MacScreenCaptureProvider.swift:828-830), Windows 则在超过 10000条后清空并进入饱和态、全部拒绝(cancellation_policy.rs:3-25)。别把两者写成同一套语义。

另外 macOS 的请求解析比客户端的 Zod更严: 它用 requireKeys(..., exactly: [...])做逐字段白名单,多一个字段就报invalidRequestFields(NativeHostProtocol.swift:81-166)。

9.4 健康检查与自愈

宿主是长驻子进程,主进程用心跳保护它:每 5 秒用get_capabilities探活 (NativeHostHealthService.ts:31、NativeHostComputerUseBackend.ts:238-240),连续 3 次失败才判定不健康(:32、:96-104)。 注释解释了为什么是 3 次而不是 1 次——单次抖动不该触发重启。

「宿主死了」有两条不同路径,处理方式不同:

  • 心跳连续失败→ 立刻尝试重连(NativeHostSupervisor.ts:280-281)。
  • 某次操作报硬错误→ 记录终态失败但不同步重连, 等下一次 acquire()再说(:159-165)。

重启预算是每个会话 1 次(DEFAULT_MAX_RESTARTS_PER_SESSION = 1,:74), 预算耗尽后进入failed,之后所有获取都直接失败(:173-174)。 预算只在会话取消时重置(NativeHostComputerUseBackend.ts:586-603)—— 注释解释了原因:否则一次旧的崩溃会把电脑操作永久锁死。

重连后的状态恢复策略很讲究:

  • 清空观测缓存——新进程没有旧状态,强制调用方重新绑定并重新观测。
  • 保留目标绑定——targetBindings不被清除(:370-376)。

能让连接作废的错误只有5 个(:889-895):native_host_missing、native_host_untrusted、native_host_incompatible、action_timeout、session_canceled。 反过来说,stale_frame/stale_tree/focus_mismatch 都不会作废连接——它们只是「这次动作没成,重新看一眼」。

只有幂等操作会被透明重试,最多 3 次,且必须宿主标了retryable: 即observe/list_windows/get_capabilities。execute_action与capture_window首次失败即抛出(:45、:676-677)。

还有一个独立于运行时心跳的部署期自检: 用 --spark-verify-native-host参数启动,配合 SPARK_NATIVE_HOST_SMOKE_REPORT环境变量输出报告, 只读、不开窗、不发任何动作,报告以 0600 独占方式写入 (ComputerUsePackagedSmoke.ts:5、:51-53;接线在 apps/desktop/src/main/index.ts:1125-1131)。

10. 元素树与 stale 语义

模型「看到」的界面不是截图,而是一份 Markdown 大纲;截图是辅助。 理解这份树的 id 与版本语义,是排查stale_tree的前提。

10.1 树如何渲染成 Markdown

格式在两侧的注释里都给了示例(NativeAXTreeRenderer.swift:10-15、tree_render.rs:5-6):

- window "Settings" [1]
  - group "Sidebar" [2]
    - textField "Search" = "vpn" [focused] [5]

两条实现要点:元素 id 是渲染出来的行号(NativeAXTreeRenderer.swift:87、tree_render.rs:65), 所以跨帧不可复用;缩进用前序扁平列表 + depth 还原层级。

渲染时会做几类取舍:

  • 丢弃「噪声叶子」:无名称、无值、无可用动作、也未聚焦的叶子节点 (NativeAXTreeRenderer.swift:116-120)。
  • 超预算即截断并留标记:超出文本预算时输出 [truncated: N elements omitted](:94-107)。
  • 容器子元素过多时只显示前若干个并标注总数 (:153-154)。
  • 敏感值被抹掉:macOS 侧 secure 元素的 value 直接置空 (MacControlPolicy.swift:309); Windows 侧密码/安全字段的 value 清空、name 统一替换为Sensitive field(uia_policy.rs:213-215)。 两端都把每个元素的actions截断到 20 条(:222、 MacControlPolicy.swift:313)。

macOS 还额外做了两件 Windows 没有的事:强制打开 Chromium/Electron 的无障碍树(MacAccessibilityController.swift:45、:159), 以及合并已展开的菜单(:106、:142)。

10.2 treeVersion 与 stale_tree

treeVersion是渲染文本的内容哈希(不是单调计数器)—— 这意味着界面没变时版本号也不变,注释明确说明了这个设计 (MacControlPolicy.swift:230-231)。

stale_tree有两种触发条件:

  1. 动作携带的treeVersion≠ 宿主当前版本(MacControlPolicy.swift:286、uia_policy.rs:195-196)。
  2. 元素 id 在当前版本里解析不到,或元素已被销毁 / PID 不匹配(MacAccessibilityController.swift:519-523)。

客户端侧还有一层更外层的stale_frame: 校验观测身份的 frameId(NativeHostComputerUseBackend.ts:441-462)。 两者都被映射为可重试(NativeHostRequestHandler.swift:373-376、windows_host.rs:950-955)。

宿主在动作结束后会主动作废自己的观测绑定, 所以下一个动作必须先重新观测(MacScreenCaptureProvider.swift:299-301、windows_host.rs:789)。 还有一种兜底:当帧/树版本变了但窗口身份没变时, 宿主会尝试重建一次缓存树而不是直接失败 (MacScreenCaptureProvider.swift:556-567)。

正确的处理方式(也是系统提示词里写的):

  • 看到stale_tree/stale_frame:重新读取一次状态(screenshot或get_app_state),用新 id重试。
  • 不要原样重复同一个调用——旧 id 不会自己变有效。
  • 如果返回里说「树的帧没有变化」,说明这个动作没产生任何变化,应该换策略而不是重试。

10.3 两个平台的树差异

这段容易写错,逐条对照:

macOSWindows
文本预算48 000 UTF-16 单元90 000 UTF-16 单元
元素上限2000100000
树版本哈希输入渲染文本**+ 元素个数**仅渲染文本
超预算时截断 + 明确标记同样截断 + 标记

两个不一致值得记下来: 其一,Windows 的 90k 预算高于TS 侧决策前的 48k 截断 (ComputerDecisionAdapter.ts:23、:373), 而 macOS 的注释明确要求「必须保持在 TS 侧上限以下,否则尾部[n]会被切掉」—— 这意味着Windows 侧长界面确实可能丢尾部元素。 其二,两端的哈希输入不同,所以不要断言两端算法一致。

还有一个已被废弃但代码里还留着的机制:NativeHostTreeReconciler同时支持 full与diff两种模式 (NativeHostTreeReconciler.ts:9-11), 但两个宿主现在都只发 full: macOS 已在MacControlPolicy.publish里删掉了 wire diff (MacControlPolicy.swift:236-240明确写 diff mode gone), Rust 侧永远走 TreeMode::Full(uia_policy.rs:188)。 所以不要因为 V2 开关里有 incrementalTree就写「增量树已上线」, 真实状态是「diff 通道已废弃,full 无条件生效」。

11. 快照、隐私与留存

电脑操作会产生大量截图。这一节讲清它们怎么产生、存哪里、谁能看、什么时候删。

11.1 截图路径与三套尺寸

所有截图都走原生宿主。仓库里desktopCapturer 零命中——Electron 只提供nativeImage的解码与编码能力。

三种尺寸是三样不同的东西,不要混为一谈:

产物尺寸编码是否落盘
imageblob(capture_app_snapshot)原始分辨率,不缩放PNG✅ 加密落盘
previewblob宽 > 1200 才缩到 1200PNG✅ 加密落盘
模型帧(决策输入)长边 ≤ 1600JPEG 质量 85❌ 只在内存
证据帧(观测审计)宽 ≤ 1200,且已打码PNG✅ 加密落盘

常量与理由(ElectronSnapshotImageProcessor.ts:4-13):

const MAX_IMAGE_DIMENSION = 16_384
const MAX_PREVIEW_WIDTH = 1_200
const MAX_MODEL_IMAGE_LONG_EDGE = 1_600
const MODEL_IMAGE_JPEG_QUALITY = 85

模型帧那条注释把动机讲得很清楚:视觉模型内部会把超过约 2 MP 的图降采样, 所以传完整 Retina 截图只会白白增加上传量与首字延迟(一张 5120×2880 的 PNG 编码成 base64 后每步有 7–14 MB)。 1600 长边的 JPEG 既保留小控件可读性,又把载荷压掉约 20 倍。

另外两组硬限额:单张截图 ≤ 64 MiB、像素数 ≤ 50 000 000 (NativeApplicationSnapshotCaptureService.ts:19-20), 且宿主只允许返回 PNG(:282-301校验 payload.kind === 'image_png')。 两端截图都不包含物理光标——因为光标会破坏静态界面的 frameId 去重 (MacScreenCaptureProvider.swift:157-160、capture.rs:126)。

11.2 加密存储与清理

blob 落在userData/snapshot-vault/blobs/,文件名是 24 随机字节的 hex +.svb(48 位十六进制), 与 blobId / snapshotId无任何字面关联—— 对应关系只存在 SQLite 的computer_snapshot_blobs.storage_key里 (SnapshotVault.ts:12、:71)。

加密参数(SnapshotVault.ts:7-11、:61-70):

  • 算法AES-256-GCM,nonce 12 字节、认证标签 16 字节。
  • 文件头有魔数SPKSVLT与格式版本,AAD 绑定 blobId 与 kind (:268-273的 JSON.stringify({ domain: 'spark.snapshot-vault', formatVersion, blobId, kind }))。
  • 密钥是 32 字节安装级密钥,存在操作系统钥匙串里 (引用名 snapshot-vault-installation-key-v1,SnapshotVaultKeyProvider.ts:4、:43-50); 首次使用时随机生成并写入。密钥长度不是 256 位直接抛错(SnapshotVault.ts:200-204)。
  • 目录权限0700、文件0600, 写入走「临时文件 + fsync + rename」的原子替换(:206-226)。

保留与清理规则(这里有反直觉的地方):

  • 没有任何数量或总体积上限——SnapshotVault里不存在磁盘配额、blob 计数或 LRU 逻辑。磁盘占用没有兜底保护。
  • 只有retention_mode = 'ttl'的行会被过期扫描清掉 (application-snapshot.repository.ts:192-199)。
  • 证据帧的 TTL 是 24 小时(ComputerObservationEvidenceStore.ts:16、:121)。
  • capture_app_snapshot产出的快照没有 TTL: 带会话时是 session模式(靠外键级联删除), 不带会话时是manual模式,永不自动删除(NativeApplicationSnapshotCaptureService.ts:252)。
  • 清理任务启动时立即跑一次、之后每6 小时一次 (SnapshotVaultMaintenance.ts:12、:49-54), 单批 200 条(:13);孤儿文件有24 小时宽限期 (SnapshotVault.ts:13)。

所以「删了会话磁盘就会立刻干净」是错的:.svb文件最多会滞留到下一轮清理 (≤ 6 小时)。反过来,「快照会自动保留 N 天后删除」也是错的—— 只有证据帧有 24 小时 TTL,manual模式的快照不会自动消失。

11.3 预览令牌

界面里的截图通过自定义协议spark-snapshot://读取, 路径形如 spark-snapshot://snapshot/<id>/preview?cap=<token>(SnapshotPreviewCapability.ts:115-119)。

  • 令牌默认 5 分钟有效(DEFAULT_TTL_MS = 5 * 60_000,:3), 合法区间 1 秒到 60 分钟;令牌是 32 随机字节的 base64url,格式为 /^[A-Za-z0-9_-]{43,128}$/(:5、:36)。
  • 协议侧校验极其严格:URL 必须逐字符等于重新拼出的结果,cap只能出现一次,且不允许任何其他 query 参数 (SnapshotProtocol.ts:130-134)。
  • 只允许GET/HEAD(其他方法 405), 只允许读取image 与preview两类 blob (text类型拿不到), 并按魔数嗅探 PNG/JPEG/WebP/GIF,否则 415 (:51-65、:160-182)。
  • 失败一律返回 404 / 500,不泄漏路径或加密细节(:55、:74-77); 响应头带cache-control: private, no-store与 content-security-policy: default-src 'none'; sandbox(:18-23)。
  • 授权判定只看「令牌存在 + 未过期 + snapshotId 相等」(:66-75)。

撤销有两个维度:按会话撤销(:77-81)与按快照撤销(:83-87)。turn 边界确实会按会话撤销——会话收尾的三个分支都会调 revokeComputerUseSession(packages/agent-runtime/src/services/session.service.ts:4221、:4255、:4288), 最终落到ComputerUseMcpProvider.ts:73-76的 revokeSession。

两个容易理解错的点:

  • 撤销只废掉「已发出」的令牌,不等于预览永久失效—— 渲染端只要再调一次 app-snapshot:get就能拿到新令牌 (ApplicationSnapshotPreviewCard.tsx:43-50、:70 在图片加载失败时自动续签)。
  • sessionId为空的授权无法被 turn 边界撤销, 只能等 5 分钟 TTL 过期——manual模式的快照就属于这种。

11.4 隐私边界:实际做了什么

这里必须按代码写,不能写成理想设计。实际有两道独立的保护, 覆盖面比直觉窄得多。

第一道:凭据类应用黑名单(17 项)

名单硬编码在NativeApplicationSnapshotCaptureService.ts:21-39: macOS 侧是 com.apple.securityagent、com.apple.loginwindow、com.apple.passwords、com.apple.keychainaccess、 securityagent;Windows 侧是logonui.exe、 credentialuibroker.exe、consent.exe、lockapp.exe; 外加密码管理器1password/bitwarden/keepass 各自的可执行名(含.exe变体)。 命中时抛sensitive_input_blocked(:304-327)。

三条必须说清的边界:

  • 它是精确 identity 匹配(小写 + 反斜杠归一化 + 取 basename),不做子串匹配。
  • 名单只覆盖凭据与系统认证类应用——浏览器、聊天软件、 任何可能显示密码的普通窗口都不在名单里。
  • 这道检查只作用于capture_app_snapshot(主动截某个应用)这条路(:192),观测路径不经过它。

第二道:敏感区域打码(只作用于观测证据帧)

敏感区域来自原生宿主上报的 AX secure 元素边界 (MacControlPolicy.swift:281的 sensitiveRegions: bounded.filter(\.secure).map(\.bounds))。 TS 侧在落盘前把这些区域涂成灰色(RGBA128,128,128,255,ElectronSnapshotImageProcessor.ts:108-117、:191-194), 坐标要先按缩放比从窗口坐标换算到图像坐标(:105-116)。 打码情况会记进元数据(ComputerObservationEvidenceStore.ts:253-258)。

与之相对的一个反向事实:capture_app_snapshot的快照完全不打码——redaction字段被硬编码为 { applied: false, reasonCodes: [], regionCount: 0 }(NativeApplicationSnapshotCaptureService.ts:251、:277)。 所以「所有快照都打码」是错的:打码只发生在观测证据帧上。

另外,纯视觉兜底路径(OCR 回退)产出的树不带敏感区域信息, 两处都是 sensitiveRegions: [](MacScreenCaptureProvider.swift:439、:1044)。

最后一道工程约束在渲染端:CSP 只允许spark-snapshot:作为图片源 (apps/desktop/src/renderer/index.html:19), 且渲染前用正则强校验 URL 形状 (design/services/event-mapper.ts:2749-2752)。

12. 界面能做什么、不能做什么

这一节不讲功能清单,而是讲哪些能力在界面上真的可达—— 因为这个子系统的界面覆盖度远低于后端能力覆盖度。

12.1 设置页只有授权与诊断,没有开关

ComputerUseSettingsSection一共 280 行,渲染的内容只有四块: 状态摘要条、两行系统授权、一个诊断块,以及「重新检测 / 请求授权 / 打开系统设置 / 运行诊断 / 复制诊断」这几个按钮。**它没有任何一个开关或复选框。**文案是硬编码中文、不走 i18n(与聊天卡片相反,那张卡走t())。

授权行的原文与状态文案(ComputerUseSettingsSection.tsx:78-91、:198、:256-260):

界面文案说明
屏幕录制读取当前应用窗口画面,用于识别按钮、输入框和操作结果。
辅助功能与输入控制读取可访问性结构,并执行点击、键盘输入、滚动和窗口切换。
已授权 / 未授权 / 受系统限制 / 当前不可用 / 待授权分别对应 ready / denied / restricted / unsupported / not_determined
电脑操作已就绪 / 电脑操作尚未就绪四项能力探测全通过才算就绪

诊断块的文案是「Native Host 与核心链路」,说明写着*「检查运行时、权限、协议状态及观察/动作各阶段延迟,不包含截图或输入内容」*(:215-218)——这句话是准确的,指标收集器确实不含内容(见 13.2)。 诊断结果会显示 diagnosticCode与三个延迟数值: 观测、动作、动作后观测(:222-227、:243-253)。

关键落差:那套ComputerUseSettings设置没有任何界面。

代码里定义了一份完整的设置结构,存在app_settings表的 category computer-use/ keysettings下(registerComputerUseIpc.ts:34-35):

{
  enabled: true,
  environments: { safeBrowser: false, safeDesktop: false, myDesktop: true },
  allowedApps: [],
  redactSensitiveContent: true,
  fullRecordingEnabled: false,
  evidenceRetentionDays: 30,
  killSwitch: 'CommandOrControl+Shift+Esc',
  remote: { observe: false, approveL2: false, control: false },
}

但渲染端从不调用computer-use:get-settings与 computer-use:update-settings(这两个通道各注册了 handler, 但全仓渲染端零引用)。于是有四个字段除了默认值之外没有任何生产读取点:

  • redactSensitiveContent(默认true)
  • fullRecordingEnabled(默认false)
  • evidenceRetentionDays(默认30)
  • 整个remote对象,含observe/approveL2/ control

它们会被 schema 校验、会被持久化、会被读回界面——但没有任何代码根据它们改变行为。 这一点如果写成「可以配置敏感内容打码 / 保留天数 / 远程审批」就是错的。

由此还引出一个关于 kill switch 的重要事实。

紧急停止的快捷键确实是真实注册的——主进程启动时注入了 Electron 的globalShortcut(apps/desktop/src/main/index.ts:1119-1124的 shortcutRegistrar: globalShortcut,位于正常的数据库初始化路径上, 不是只在冒烟测试分支里)。未注入时用的是 fail-closed 实现:register恒返回false(ComputerUseServices.ts:326-329)。

但武装(arm)动作只有两条路径: (a) 启动时的 reconcilePersistedKillSwitch; (b)computer-use:update-settings handler 内的armKillSwitchBestEffort。 而 (a) 读的是原始持久化行,没有默认值兜底(registerComputerUseIpc.ts:446-454直接safeParse(store.get(...)), 与loadSettings的?: structuredClone(DEFAULT...)不同)。 由于没有任何界面写这一行,全新安装时该记录不存在 →safeParse失败 → 直接 return。

结论:默认值里的CommandOrControl+Shift+Esc在全新安装上不会被注册。不要写「默认快捷键 Cmd/Ctrl+Shift+Esc 可随时紧急停止」。 想启用它,需要让那一行设置被真正写入(例如通过平台工具的settings_set写入该 category/key, 它不校验 category/key,packages/agent-runtime/src/services/settings.service.ts:29-31), 之后下次启动才会武装。

一旦武装,它的行为是取消所有活跃会话,与界面上的「停止」走同一条代码路径 (ComputerUseServices.ts:224-241对每个活跃会话调broker.killSwitch, 后者即stop,ComputerControlBroker.ts:335-342)。 两个细节:重复按键会被合并(一次 kill 未完成时后续按键被吞掉, ComputerKillSwitchService.ts:21);被 kill 的会话不能 resume——canceled不是paused,恢复只能起一个新会话 (ComputerSessionManager.ts:338-343)。 另外代码里没有用任何标记把「kill switch 中止」与「用户点停止」区分开, 时间线上看不出是哪一种。

还要区分两个完全不同的「Esc」:kill switch是CommandOrControl+Shift+Esc,停掉所有会话;物理 Esc 键只是取消当前这一个动作, 让调用方收到 handoff_required(NativeInterruptionToken.swift:6-10 有完整语义说明)。 系统提示词特别强调:模型自己通过press_key发 Esc 不会触发它, 因为合成按键不算用户输入。

12.2 聊天里的活动卡片

卡片由ComputerActivitySegmentsBridge按消息切段渲染(ChatView.tsx:5147-5168)。 每段的构成:

  • 状态徽标:取该段最后一条事件推断(ComputerActivityBlock.tsx:381-396),文案有「已完成 / 失败 / 已停止 / 需要接管 / 等待确认 / 进行中」。
  • 行:事件标签 + 时间(HH:mm:ss,:297-298)。
  • 耗时:按整条会话时间线的首末事件差计算,小于 1 秒不显示(:501-510)。
  • 控制按钮:暂停 / 接管 / 停止 / 更换窗口,只在「本段是最新一段、有会话、且非终态」时出现(:305-374)。

两个显示上的取舍值得知道:

  • computer_observation_created事件被显式过滤掉不显示(:216),所以卡片不会列出每一次观测。
  • 同一actionId的requested事件在出现终态后会被折叠(:403-417),所以不是「每个动作两行」。

错误码在卡片里会做一次中文映射,但这张映射表只有 7 个键, 且其中三个键不是协议枚举里的码(:482-493用的permission_denied /native_host_not_found/target_lost, 而枚举里是 screen_permission_denied/native_host_missing/ focus_mismatch)。 结果是绝大多数真实错误会原样显示英文错误码(?? errorCode兜底)。 排查时直接按英文码查本页第 14 节更可靠。

托盘里的按钮失败只写日志、界面无提示(apps/desktop/src/main/index.ts:717),这也是一个排查盲区。

12.3 托盘与画中画(PIP)

  • 托盘:ComputerControlTrayService,在createTray() 里构造(main/index.ts:565-575),订阅会话状态刷新菜单。菜单文案是 「暂停 Agent / 立即接管 / 停止控制」——与卡片里的「暂停 / 接管 / 停止」不是同一套。
  • PIP 面板:ComputerUsePipService, 但它不是渲染端 React 组件——它是主进程创建的BrowserWindow,内容是一段内联的data:text/html页面 (ComputerUsePipService.ts:91-116、HTML 常量在:151-216)。 只有 pipPanel这个 V2 开关为真时才创建 (ComputerUseServices.ts:185-206),该开关默认为真。

渲染端实际调用的电脑操作通道只有 11 个(get-capabilities、 diagnose-native-host、open-system-settings、 list-windows、list-sessions、get-timeline、 bind-target、pause、stop、takeover、 resolve-app-command), 而注册的通道有 20 个。剩下 9 个没有任何渲染端调用者:

start  resume  get-status  get-settings  update-settings
list-apps  approve-action  deny-action  get-verification

它们的存在方式各有不同:start/resume/get-status/ list-apps是主进程 MCP 路径的 IPC 镜像,实际由 Agent 调用;get-settings/update-settings是那份「没有界面」的设置;approve-action是被故意封死的(永远抛错);get-verification则必然返回null(见 8.3)。

这带来一个实际的界面落差:聊天卡片上没有「继续」按钮。 会话暂停后,只有 Agent 通过 MCP 调resume才能真正恢复运行 (渲染端既不调用它,它的 IPC 版实现也不重启算子循环)。 同理,bind_target(更换窗口)之后要靠 Agent 调 resume继续。

13. V2 开关与自动回滚

computer-use目录下有一组SPARK_COMPUTER_USE_V2_* 环境变量控制的开关。 它们的语义与很多人的直觉不同:没有 V1/V2 总开关,10 个开关默认全开, 且只能被显式关闭、不能通过配置打开任何「旧版」。

13.1 十个开关的真实消费情况

定义在computerUseV2Flags.ts:1-12,默认值在:38-49(十个全为true)。源码注释写明了口径:「V2 is the shipped product path.」——V2 就是当前产品路径, 每个优化都能通过环境变量显式关闭或运行时独立回滚。

环境变量名统一前缀SPARK_COMPUTER_USE_V2_(:23-34), 解析规则是「只有0/false/no/off算关闭, 其他任何非空字符串都算开启」(:137-140)。 所以SPARK_COMPUTER_USE_V2_PIP_PANEL=anything是开,不是关。

逐个人工核实过的消费情况:

开关生产读取点作用
hostSupervisorNativeHostBackendFactory.ts:92启用原生宿主 supervisor(自愈)
incrementalTreeComputerTaskOperator.ts:42取反决定是否请求 full 决策树
actionBatchComputerTaskOperator.ts:219是否允许模型一次返回一批动作
persistentCaptureNativeHostComputerUseBackend.ts:227持久捕获流(而非每次截一张)
actionSkyshotNativeHostComputerUseBackend.ts:474动作回包附带后置观测
pipPanelComputerUseServices.ts:185是否创建画中画面板
installedArtifactDiagnostics无—
backgroundSemanticLane无—
activityTimeline无—
visibleControlIndicator无—

四个开关没有任何消费者:installedArtifactDiagnostics、backgroundSemanticLane、activityTimeline、visibleControlIndicator。 它们会出现在get_capabilities 返回的开关快照里 (ComputerUseAgentController.ts:200-201把快照塞进返回值), 但读取它们不会改变任何行为。连它们对应的四个包装函数也无人调用。

由此推出两个容易写错的结论: 其一,活动时间线不是由activityTimeline开关控制的——ComputerUseTimelineStore无条件在跑,卡片也是无条件渲染的; 其二,不要因为incrementalTree默认开启就写「增量树已上线」—— 两个宿主现在都只发 full(见 10.3),diff 通道已废弃。

另外persistentCapture虽然确实被消费, 但它对应的熔断计分没有被接上:recordPersistentCaptureBudget()零生产调用(只在自己的类里定义), 所以它不会被自动回滚,相关阈值形同虚设。

13.2 自动回滚阈值

ComputerUseV2RolloutController会按运行统计自动关闭某个开关。 语义是进程内、不落盘、下次启动自然重置,且只能关不能开。 阈值(ComputerUseV2RolloutController.ts:24-35):

指标阈值
宿主崩溃率0.005(样本 ≥ 200)
安装制品失败率0.001(样本 ≥ 1000)
动作错误率0.01(样本 ≥ 500)
接管停止 P99500毫秒(样本 ≥ 100)
持久捕获预算失败率0.01(样本 ≥ 200,但计分未被接上)

统计窗口上限2048个样本(:37)。 有两处命名与直觉不符,排查时会误导:

  • 接管耗时超限回滚的是actionBatch, 不是 supervisor,也不是可见指示器(:108的 rollback('actionBatch', 'takeover_stop_p99_exceeded', ...))。
  • 安装制品校验失败时被禁用的是hostSupervisor, 不是installedArtifactDiagnostics(:80-84)。 所以那个开关永远不会被回滚——它本来也没有消费者。

回滚的后果也不一样:只有hostSupervisor会真的拆掉 supervisor 并清空观测(NativeHostComputerUseBackend.ts:248-256), 其余开关回滚只是让读取点返回 false。

14. 排查表

按现象查。每条都给出了对应的判定点或错误码——错误码以协议枚举为准, 不要依赖聊天卡片里的中文映射(见 12.2)。

现象先看哪里说明
设置页显示「尚未就绪」两行授权状态 +unavailableReason原文四项能力探测需全部 granted;就绪判定见 2.4
macOS 授权后仍显示未授权screen_permission_denied/accessibility_permission_denied事件投递权限缺失也报 accessibility 码,没有独立码(2.2)
Windows 上没有任何授权弹窗get_capabilities的restrictedWindows 无弹窗、无 denied 状态,只有 granted/restricted(2.3)
报native_host_missing安装制品是否存在、平台/架构是否匹配诊断 stage 为discover,修复动作reinstall
报native_host_untrustedTeam ID / 发布者指纹是否与 App 一致本地信任 + 已签名 App 会故意被拒(9.2)
报host_handshake_failed/ 协议错误协议版本、帧格式宿主退出码 76 即协议失败、77 即认证失败(9.3)
动作报stale_frame/stale_tree重新读一次状态,用新元素 id 重试元素 id 是行号、跨帧不可复用;不要原样重复调用(10.2)
动作报action_noop界面是否真的变了无变化会被记为失败而非成功;应换策略
动作报focus_mismatch目标应用是否已切换/关闭策略层唯一的硬拒绝条件(4.2)
报action_timeout宿主是否被杀、是否会自动重启超时即 SIGKILL 整个宿主;每会话只有 1 次重启预算(9.4)
宿主反复重启后彻底不可用supervisor 状态是否failed预算耗尽;只有取消会话才重置(9.4)
报native_host_request_capacity_reached是否有并发调用堆积上限 64 在飞 + 64 排队(9.3)
报environment_unavailable环境开关、UIA 可用性、并发状态该码同时用于「环境关闭」「UIA 不可用」「并发未完成(可重试)」
报computer_disabledComputerUseSettings.enabled默认 true;但该设置无界面,落库值可能异常
报sensitive_input_blocked目标是否在 17 项凭据类黑名单里精确 identity 匹配;只作用于主动截图路径(11.4)
报decision_model_error会话 Provider 是否可用、是否本地 CLI本地 CLI Provider 被显式拒绝;失败会带诊断与修复建议
报task_step_limit_exceeded/task_runtime_exceeded步数 100 / 时长 20 分钟预算委派路径的默认预算;原子路径宽松得多(1.3)
任务一直跑不完、从不验收判据里是否用了dom/file/external_readback这三种 kind 引擎不支持,永远判失败(8.3)
任务明明完成了却判失败判据文本是否与界面实际文本一致visual 判定是字符串 includes;无树证据时才走模型兜底(8.2)
截图里少了元素 / 树被截断树文本预算mac 48k / Windows 90k,但 TS 侧只截 48k → Windows 可能丢尾部(10.3)
点不中元素 / 坐标偏移坐标是否按归一化窗口坐标传换算基于「模型看到那张图」的尺寸,可能已被降采样(5.2)
动作数比工具调用数多type_text/paste是否带into/ submit会自动追加 focus 与 Enter,一次调用最多 3 个动作(5.2)
磁盘上还有.svb文件下一轮清理(≤ 6 小时)且manual模式快照永不自动删除(11.2)
截图预览打不开 / 图片裂开令牌是否过期(5 分钟)渲染端会自动续签;sessionId为空的授权只能等 TTL(11.3)
界面里无法彻底关闭电脑操作设置页确实只有授权与诊断那套设置有 schema、有 IPC、无界面(12.1)
暂停后卡片仍显示「进行中」时间线最后一条事件暂停不产生事件;徽标只看末事件(6.3)
暂停后无法从界面继续卡片上没有「继续」按钮需要 Agent 调 MCPresume(12.3)
紧急停止快捷键没反应设置行是否已持久化全新安装不会武装,需要先写入该设置并重启(12.1)
卡片里显示英文错误码对照本表与协议错误码枚举中文映射表只有 7 个键且 3 个键名对不上(12.2)
托盘按钮点了没反应主进程日志托盘失败只写日志、界面无提示(12.2)

15. 事实边界说明

这一页的每条结论都来自仓库代码。有几处必须交代清楚,避免你把它们当成运行时验证结果:

  • 关于「会话权限层的硬闸是否真的会被触发」:代码顺序是可确认的 (自动放行在审批回调之前)。但canUseTool的具体调用策略属于外部 SDK (@anthropic-ai/claude-agent-sdk)的行为,不在本仓库内。 所以本页的表述是「该硬闸是 fail-closed 兜底网,生产路径存在更早的自动放行」, 而不是「它必然不会被触发」。
  • spark 引擎路径未核实:spark-*权限模式下 「自动放行名单是否先于 approver 生效」的判定在引擎内核里,不在本仓库中。
  • Windows 发布证书指纹无法从仓库确认:它是编译期环境变量注入的, 仓库内没有证书或指纹字面量。
  • 原生宿主内部是否存在未暴露的超时:Swift / Rust 源码里没有请求级超时,action_timeout完全由客户端产生;若宿主内部另有超时,本仓库看不到。
  • kill switch 与「用户点停止」在时间线上无法区分—— 代码里没有为此写任何标记事件,这是可确认的事实,不是未核实项。

最后一句实话:这套能力的工程完成度很高(双向签名、加密快照、心跳自愈、自动回滚、 有界预算),但它的策略层是「标注」而不是「拦截」, 多个安全字段是声明而非强制。使用前请按 4.2 与 4.3 的实际边界来评估风险, 不要依赖任务契约里的那些未生效字段。

速查表

项目内容
设置入口设置 → 系统 → 电脑操作(项 id computer-use)
工具数量27 个 mcp__spark_computer__* 工具
默认开关enabled=true;environments 只有 myDesktop=true
可用环境仅 my_desktop(工具入参是 z.literal)
平台支持macOS + Windows;Linux 无实现,直接降级为不可用
默认请求超时20 秒(上限 180 秒);超时即 SIGKILL 宿主进程
帧格式5 字节头(4 字节大端长度 + 1 字节 kind),载荷上限 64 MiB
协议版本1
心跳每 5 秒 get_capabilities,连续 3 次失败判定不健康
重启预算每会话 1 次,仅取消会话时重置
并发上限64 在飞 + 64 排队
宿主退出码76 = 协议失败,77 = 认证失败
委派任务预算maxSteps 100 / maxRuntimeMs 20 分钟 / maxConsecutiveNoops 8
原子会话预算maxSteps 2000 / maxRuntimeMs 12 小时 / maxConsecutiveNoops 20
原子会话空闲释放5 分钟(IDLE_RELEASE_MS = 300000)
预览令牌 TTL5 分钟(合法区间 1 秒 ~ 60 分钟)
快照加密AES-256-GCM,密钥存 OS 钥匙串,目录 0700 / 文件 0600
快照清理周期启动即跑一次,之后每 6 小时;孤儿文件宽限 24 小时
证据帧 TTL24 小时
凭据类应用黑名单17 项(仅作用于主动截图路径,精确 identity 匹配)
决策树文本上限TS 侧 48000 字符(mac 树 48000 / Windows 树 90000)
元素树上限mac 2000 个元素 / Windows 100000 个元素
批量动作范围MIN_BATCH_ACTIONS 2 ~ MAX_BATCH_ACTIONS 8
V2 开关10 个,默认全 true;其中 4 个无任何消费者
默认紧急停止键CommandOrControl+Shift+Esc(全新安装不会武装)
错误码总数32 个(packages/protocol/src/computer-use/errors.ts)

上手清单:启用电脑操作并让 Agent 操作某个应用

从系统授权到委派任务,再到验收与接管,走一遍完整流程。

  1. 打开「设置 → 系统 → 电脑操作」,先看顶部状态条。若显示「电脑操作尚未就绪」,先处理下面的系统授权。
  2. macOS 上逐项点「请求授权」:屏幕录制、辅助功能与输入控制。首次会弹系统对话框;如果之前拒绝过,按钮会变成「打开系统设置」,需要到「系统设置 → 隐私与安全性」里手动勾选,授权后回到本页点「重新检测」。
  3. Windows 上没有弹窗,权限状态直接由运行时探测给出 granted / restricted。若某项 restricted,先确认对应系统能力(图形捕获 / UI Automation / 输入注入)是否可用。
  4. 授权完成后点「运行诊断」,确认 diagnosticCode 正常、观测与动作延迟有数值。诊断只含耗时与状态,不含截图或输入内容,可以放心复制给别人看。
  5. 回到会话,直接描述目标即可,例如「把这份文档的内容填到编辑器里并保存」。涉及具体应用时把应用名说清楚,Agent 会据此解析目标窗口。
  6. 长任务建议让 Agent 用 start_task 委派,它会自己循环直到完成、失败或需要你接管;短任务则用原子动作一次一步更可控。
  7. 过程中可以在聊天卡片或系统托盘里操作:暂停(停下并中断在途动作)、接管(停止 Agent 让你自己接手)、停止(终态取消,不可恢复)。注意暂停后界面上没有「继续」按钮,需要让 Agent 通过 MCP 恢复。
  8. 想知道它做了什么,看聊天里的活动卡片:每行一个事件带时间,控制按钮只在最新一段且非终态时出现。卡片不显示每次观测,同一动作的请求与终态会折叠成一条。
  9. 任务判完成靠 successCriteria 的逐条验收。若反复跑不完,检查判据是否用了引擎不支持的 dom / file / external_readback——这三种永远判失败。
  10. 用完之后记住两件事:暂停的会话需要显式恢复或停止;电脑操作自身没有界面开关可以关闭,只能依赖会话结束与撤销系统授权。

常见问题

电脑操作需要我手动开启吗?默认是开着的吗?

默认是开启的:设置项 enabled 默认 true,environments 里只有 myDesktop 为 true(safeBrowser / safeDesktop 默认关闭,且工具入参只接受 my_desktop)。你唯一必须手动做的是在「设置 → 系统 → 电脑操作」里完成系统授权——macOS 需要屏幕录制、辅助功能、事件投递三项,Windows 只需运行时探测通过。设置页本身没有任何开关可以关掉这个能力。

Agent 操作我的电脑时,会抢走我的鼠标和焦点吗?

默认不会。动作优先走无障碍语义(例如元素左键单击会被改写为 invoke_element),鼠标键盘投递是后台优先的,不移动你的物理光标也不抢焦点。你切到别的应用时它会继续操作自己绑定的目标窗口。例外是少数必须前台的操作:Windows 的拖拽与 focus_window 需要先把目标窗口提到前台;macOS 的 move / drag / keypress 也只能走真实事件流。

每次点击都会弹审批框让我确认吗?

不会,当前版本没有逐动作审批。设置页原文就写着「授权完成后,Agent 发起的电脑操作任务默认直接执行,不再逐步审批」。仓库里那套审批实现(含票证与过期语义)已标注为废弃、没有接到生产路径上,computer-use:approve-action 这个 IPC 还会直接抛错。动作以 approvalTicketId = null 直接执行。你真正能用的刹车是暂停、接管、停止与紧急停止快捷键。

策略引擎会拦截危险动作吗?

不会拦截内容,只做风险分级。ComputerPolicyService.evaluate 在所有分支上都返回 allow,唯一的 deny 是「观测到的应用与动作目标应用不一致」(focus_mismatch)。风险等级 L0–L4 会算出来、写进动作记录并在卡片上展示,但不因此阻止执行——测试里连 L3 高风险动作都是直接放行的。任务契约里的 allowedApps / allowedDomains / forbiddenActions 等字段在生产路径没有任何读取点。

截图会保存到磁盘吗?会打码吗?我能控制保留多久吗?

会保存,且是加密保存:blob 落在 userData/snapshot-vault/blobs/ 下,文件名是 24 随机字节的十六进制 + .svb,用 AES-256-GCM 加密,密钥存在操作系统钥匙串里。打码只发生在「观测证据帧」上(涂灰 AX 上报的 secure 区域),capture_app_snapshot 产出的快照完全不打码。保留策略要分清:证据帧有 24 小时 TTL,而 capture_app_snapshot 的快照在无会话时是 manual 模式、永不自动删除,且整体没有任何磁盘配额或 LRU。设置里那几个看起来像保留策略的字段(evidenceRetentionDays、redactSensitiveContent、fullRecordingEnabled)除默认值外没有任何生产读取点。

紧急停止快捷键默认生效吗?

全新安装上不会。快捷键注册器确实是真实注入的 Electron globalShortcut,但武装(arm)只在启动时读取持久化设置行、或调用 computer-use:update-settings 时发生;而渲染端从不调用那个通道,所以设置行根本不存在,启动时 safeParse 失败就直接 return。想启用需要先让那一行设置被真正写入,然后重启。一旦武装,它的行为是取消所有活跃会话,且被它取消的会话不能 resume——只能重新开始。