computer-use 是 Qwen Code 的桌面操控能力:配置好之后,AI 可以直接读取屏幕内容、移动鼠标、点击界面、输入文字,代替人来操作本机应用。这项能力通过一个 MCP 服务器接入 Qwen Code。MCP(Model Context Protocol)是一种让 AI 工具调用外部能力的协议,这里的外部能力就是操控桌面。
官方文档给出的配置流程只有两步:
qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.7npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.11第一步注册 MCP 服务器,第二步安装 SDK。SDK 里包含一个原生驱动程序,负责实际的屏幕读取和鼠标键盘控制。
在 Windows 上执行第二步时,安装直接失败。下面记录完整的排查和解决过程。
安装失败:驱动没有签名,安装脚本却强制验证
执行安装命令:
PS> npm install --no-save --package-lock=false @qwen-code/cua-sdk@0.20.11...(依赖下载过程省略)npm error command C:\WINDOWS\system32\cmd.exe /d /s /c node scripts/install-native.mjsnpm error @qwen-code/cua-sdk native installation failed: Command failed: powershell -NoProfile -NonInteractive -Command $signature = Get-AuthenticodeSignature -LiteralPath 'C:\...\qwen-cua-driver-uia.exe'; if ($signature.Status -ne 'Valid') { throw "UIAccess worker Authenticode status is $($signature.Status)" }npm error UIAccess worker Authenticode status is NotSigned安装失败发生在 postinstall 阶段。postinstall 是 npm 的一个机制:包安装完成后,npm 会自动执行包作者预先定义的脚本,通常用来编译代码或下载附加文件。cua-sdk 的 postinstall 脚本负责下载并安装原生驱动,它在这一步终止。
错误信息本身就把线索交代得很清楚:安装脚本执行了一段 PowerShell 命令,命令里调用了 Get-AuthenticodeSignature,这是 PowerShell 用来查询文件数字签名状态的命令。打开包里的 scripts/install-native.mjs 查看完整逻辑:
async function requireValidAuthenticodeSignature(source) { const command = `$signature = Get-AuthenticodeSignature -LiteralPath ${quotePowerShell(source)}; ` + `if ($signature.Status -ne 'Valid') { ` + `throw \"UIAccess worker Authenticode status is $($signature.Status)\" }` await run("powershell", ["-NoProfile", "-NonInteractive", "-Command", command])}这段代码对驱动文件执行签名检查,状态不是 Valid 就抛出异常终止安装。这个函数被无条件调用,没有任何配置项或环境变量能跳过它。
而它检查的驱动文件 qwen-cua-driver-uia.exe,实际查询签名状态的结果是:
PS> (Get-AuthenticodeSignature 'qwen-cua-driver-uia.exe').StatusNotSigned结论:官方发布的驱动文件没有数字签名,官方的安装脚本却强制要求签名验证通过。两者矛盾,安装必然失败。
在 GitHub 上搜索,确认这是已知问题:QwenLM/qwen-code#12394,标题 “Windows UIAccess worker exe is unsigned”。P1 优先级,2026 年 9 月底报告。写作时该 issue 为 Open、没有关联 PR;截至 2026 年 10 月底有了进展:修复 PR #12442(发布前对驱动签名)已于 2026-10-24 合并,但负责签发签名证书的 #14539 仍为 Open。合并晚于最新版 0.26.0 的发布(2026-10-21),已下载该版驱动包验证:三个可执行文件仍然全部未签名,安装脚本也仍然无条件强制验签。在带签名的版本实际发布之前,本文的手动方案对 npm 上的现有所有版本仍然必要。
未签名是发布流程的有意设计
issue 里另一位贡献者对源码做了完整追踪,找到了矛盾的来源。CI 工作流(cd-cua-driver.yml)里有一个验证步骤,逻辑与安装脚本正好相反:如果驱动带有签名,构建直接失败:
$signature = Get-AuthenticodeSignature -LiteralPath $workerif ($signature.Status -ne 'NotSigned') { throw "Release worker must be unsigned for target-machine signing; ..."}设计意图是目标机器签名模式:官方发布未签名的驱动,由部署方在目标机器上用自己的证书签名,再放入 Program Files 的安全路径。这套模式面向的是企业部署场景。
问题出在安装脚本没有跟上这个设计。发布端保证驱动未签名,安装端(npm postinstall 和 install.ps1)却无条件要求签名有效,两份契约相互矛盾。CI 的安装验证跑在 Linux 上(npm ci --ignore-scripts),Windows 分支的安装逻辑从未被测试覆盖,矛盾于是在每个版本上静默存在,直到普通用户在 Windows 上执行 npm install。
为什么 Windows 要求这个驱动必须有签名
qwen-cua-driver-uia.exe 是一个 UIAccess worker。UIAccess 是 Windows 的一种权限级别,允许程序读取和操控其他窗口的界面,即使那些窗口以更高权限运行。computer-use 要控制别的应用,驱动就需要这个权限。
Windows 对这类高权限程序有一条强制规定:必须经过 Authenticode 数字签名。Authenticode 是 Windows 的代码签名机制,签名能证明两点:程序来自可信的发布者,且发布后没有被篡改。没有签名的 UIAccess 程序,系统默认不可信。
所以问题变成:签名验证本来要提供的那份保证,用什么手段补上?
解决方案:手动安装驱动,用 SHA256 校验替代确认
签名验证的核心目的有两个:确认驱动来自官方,确认驱动没有被篡改。这两点可以用校验和来验证:
- 从官方 GitHub Release 下载驱动包
- 计算文件的 SHA256 值,与官方同时发布的
checksums.txt里的值对比,一致(b46127a3...,对得上),说明文件确实来自官方仓库,且下载过程没有被篡改 - 手动解压到 SDK 的缓存目录
- 用管理员权限把 UIAccess worker 复制到
C:\Program Files\Qwen\CuaDriver\
包本体和依赖则绕开有问题的 postinstall,用 --ignore-scripts 参数安装。这个参数让 npm 跳过所有安装脚本,只安装文件:
PS> npm install --no-save --package-lock=false --ignore-scripts --registry=https://registry.npmmirror.com @qwen-code/cua-sdk@0.20.11added 7 packages in 12s跳过 postinstall 不会影响功能:cua-sdk 运行时会检查缓存目录里有没有 complete.json 文件,发现驱动已经就位就直接使用,不依赖安装脚本。
整个方案可以回退:官方修复之后,删掉手动安装的目录,重新执行 npm install,就回到官方流程。
绕过一项安全验证时,先理解它防的是什么,用其他手段补上同样的保证,并保留回退路径。
实施和使用过程中遇到的两个新问题
方案确定后,实施和使用时先后遇到两个问题,记录如下。
问题一:因解析器缓存失败结果,导致包安装后导入仍失败
MCP 连上之后,在 node REPL 里导入 cua-sdk,报错:
> await import('@qwen-code/cua-sdk')cannot resolve package '@qwen-code/cua-sdk' from 2 module rootsnode REPL 是这个 MCP 服务器提供的 JavaScript 运行环境,AI 通过它执行代码。先验证包本身有没有问题:绕开 node REPL,直接用 Node 测试导入:
PS> node --input-type=module -e "import('@qwen-code/cua-sdk').then(m => console.log('OK', Object.keys(m).length))"OK 138直接用 Node 导入完全正常,138 个导出。但同一个包,在 node REPL 内核里导入却失败。同一个 Node,同一台机器,同一套文件,结果不同。
这种环境之间不一致的问题排查难度最大,因为每个局部测试都是通过的,没有一个明确的故障点。
用对照实验来定位:在模块目录里手动创建几个测试包,逐个排除可能的怀疑点。每个包配一个只有一行导出语句的 index.js,差别只在 package.json。
实验一:最简单的包,验证内核的解析机制。 创建一个不带任何特殊配置的包:
{ "name": "test-probe", "version": "1.0.0", "type": "module", "main": "index.js"}在内核里导入:
> await import('test-probe')test-probe: OK, hello=world成功。说明内核的模块解析机制正常,注册的模块搜索目录也生效。问题不在内核本身。
实验二:带 exports 的包,验证包描述文件结构。 cua-sdk 的 package.json 声明了 exports 字段,而且只提供 import 条件、没有 require 条件,这是一种比较少见的写法,可能是怀疑点。照着这个结构创建测试包:
{ "name": "test-probe2", "version": "1.0.0", "type": "module", "main": "./index.js", "exports": { ".": { "types": "./index.d.ts", "import": "./index.js" } }}导入:
> await import('test-probe2')test-probe2: OK, hello=probe2成功。排除 exports 结构的问题。
实验三:作用域包名,验证 @scope/ 形式的解析。 cua-sdk 的包名是 @qwen-code/cua-sdk,带 @scope/ 前缀。创建同形式的包:
{ "name": "@test-scope/probe3", "version": "1.0.0", "type": "module", "main": "./index.js", "exports": { ".": { "types": "./index.d.ts", "import": "./index.js" } }}导入:
> await import('@test-scope/probe3')scoped probe3: OK, hello=probe3-scoped还是成功。排除作用域包名的解析问题。
三组对照全部通过。用同样的方式导入真实包:
> await import('@qwen-code/cua-sdk')cannot resolve package '@qwen-code/cua-sdk' from 2 module roots失败。测试包和真实包之间只剩一个差别:测试包是刚创建的,cua-sdk 是内核启动时曾经导入失败过的。
原因确定:Node 的 ESM 解析器会缓存模块解析结果。ESM 是 JavaScript 的模块标准,解析指根据包名找到实际文件路径这一步。Node 会把解析结果记在内存里,同样的导入请求下次直接返回缓存结果,不再查磁盘,而失败的解析结果也会被缓存。
时间线还原:内核启动时,cua-sdk 还没有安装完整;第一次导入失败,这个失败结果被缓存;之后安装完成,再次导入时,解析器直接返回缓存里的失败结果,没有去磁盘确认。
解决方法:执行 node_repl_reset 重置内核。重置会终止当前内核进程并启动新进程,内存缓存随之清空(已注册的模块目录会保留):
> node_repl_resetnode_repl kernel reset (generation 3). All bindings were discarded. 1 registered module root(s) were retained.重置后再导入:
> await import('@ubjs/core')@ubjs/core: OK, keys=51> await import('@qwen-code/cua-sdk')cua-sdk: OK, keys=138和直接用 Node 的结果一致。
由此得到一条实用规则:在 node REPL 里安装任何包之后,如果之前导入失败过,先执行 reset 再试。
问题二:因系统拒绝启动未签名 UIAccess 程序,导致窗口操控失败
前一个问题解决后,初始化和列举应用都正常:
> await computer.listApps()应用列表 (共 276 个)返回 276 个应用,看起来配置已经完成。但读取窗口内容时失败了:
> await chromeApp.getState()ComputerUseError: The app state could not be read. Check that its window is available,then call app.getState() again.Code: uiaccess_worker_unavailable列举应用走的是基础接口,不需要特殊权限;读取和操控窗口内容需要另一个组件:UIAccess worker。检查进程列表,这个组件根本没有在运行。手动启动它试试:
PS> Start-Process 'C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe'Start-Process : This command cannot be run due to the error: A referral was returned from the server.启动被 Windows 拒绝。查询这个 exe 的嵌入 manifest(manifest 是 Windows 程序内嵌的声明文件,描述程序需要的权限级别),找到了声明:
<requestedExecutionLevel level="asInvoker" uiAccess="true"></requestedExecutionLevel>manifest 声明了 uiAccess="true"。Windows 对这类程序有一条强制规定:启动时必须持有有效的 Authenticode 签名。安装阶段的签名检查通过手动安装绕过了,但操作系统在启动 UIAccess 程序时还会再检查一次,这是同一问题的第二道拦截。
解决方法:把 manifest 里的 uiAccess 从 "true" 改成 "false"。修改后 worker 以普通权限运行,仍然可以操控非管理员权限运行的窗口,浏览器、办公软件、编辑器都属于这一类。代价是无法操控以管理员身份运行的窗口,对常规使用没有影响。
修改嵌入 manifest 用 Win32 的 UpdateResource API,PowerShell 可以直接调用。核心逻辑:
# 定位二进制里的 manifest XML,替换 uiAccess 值,写回资源段$exe = 'C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe'$b = [IO.File]::ReadAllBytes($exe)$t = [Text.Encoding]::UTF8.GetString($b)$i = $t.IndexOf('uiAccess')$s = $t.LastIndexOf('<?xml', $i)$e = $t.IndexOf('</assembly>', $i)$m = $t.Substring($s, $e + 11 - $s).Replace('uiAccess="true"', 'uiAccess="false"')$d = [Text.Encoding]::UTF8.GetBytes($m)
# 通过 UpdateResource 写回(RT_MANIFEST = 24,资源 ID = 1)$h = [RU]::BeginUpdateResource($exe, $false) # RU 是 P/Invoke 声明的封装类[RU]::UpdateResource($h, [IntPtr]24, [IntPtr]1, 0x0409, $d, $d.Length) | Out-Null[RU]::EndUpdateResource($h, $false) | Out-NullRU 封装类用 Add-Type 声明 BeginUpdateResource、UpdateResource、EndUpdateResource 三个 kernel32 函数。exe 在 Program Files 下,修改需要管理员权限。建议先复制到临时目录修改并验证启动正常,再用管理员权限复制回去。
修改后重新测试:
> await chromeApp.getState()窗口: xxx - Google Chrome文本: Full accessibility state: [0] Window "xxx - Google Chrome" [1] Pane "xxx - Google Chrome" [2] Button "最小化" actions=["invoke"] [3] Button "恢复" actions=["invoke"]...(后续窗口元素省略)getState 返回了窗口的完整可访问性树,之后的点击、输入、快捷键操作全部正常。至此操控能力才真正可用。listApps 成功并不代表配置完成,能读取和操作窗口内容才是最终验证标准。
最终配置步骤
完整步骤整理如下。命令使用本文验证通过的版本(cua-sdk 0.20.11、node-repl-mcp 0.1.6)。截至 2026 年 10 月底,两个包在 npm 上的最新版本均为 0.26.0;已下载该版驱动包验证,三个可执行文件仍未签名,安装脚本仍强制验签,UIAccess worker 的 manifest 仍声明 uiAccess="true",两层拦截在 0.26.0 上原样存在。本文方案对 0.26.0 同样必要,把步骤中的版本号 0.20.11 相应替换即可,完整流程未在 0.26.0 上实测。
第一步,添加 MCP 服务器:
qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.6文档里写的 @0.1.7 版本在写作时不存在于 npm,当时的最新版本是 0.1.6,所以用 0.1.6。这个问题已提交给上游:issue #13501,上游随后发布了 0.1.7,issue 已于 2026-10-13 关闭。执行后可以用 qwen mcp list 确认状态是 Connected。
第二步,安装 cua-sdk:
npm install --no-save --package-lock=false --ignore-scripts \ --registry=https://registry.npmmirror.com @qwen-code/cua-sdk@0.20.11在项目目录下执行。--ignore-scripts 跳过有问题的安装脚本,--registry 指定国内镜像源加快下载。
第三步,手动安装原生驱动:
从 GitHub Release 下载驱动包,校验 SHA256 之后,解压到 %LOCALAPPDATA%\Qwen\cua-sdk\0.20.11\windows-x86_64\(%LOCALAPPDATA% 即 C:\Users\用户名\AppData\Local),再用管理员权限把 qwen-cua-driver-uia.exe 复制到 C:\Program Files\Qwen\CuaDriver\0.20.11\。
第四步,修改驱动的 manifest:
按问题二的方法,把 C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe 的嵌入 manifest 里 uiAccess 从 "true" 改成 "false"。跳过这一步的话,安装和列举应用正常,但读取和操控窗口会失败。
第五步,重启 Qwen Code,在会话里初始化:
// 先用 node_repl_add_node_module_dir 工具注册 node_modules 所在目录// 如果之前导入失败过,先执行 node_repl_reset
const computer = await ( await import('@qwen-code/cua-sdk/computer-use')).ComputerUse.create();
const platform = await computer.getPlatform(); // 返回 "windows"初始化之后先做快速检查,再验证真正的操控能力:
> await computer.listApps()应用列表 (共 276 个):[运行中] Windows Software Development Kit[运行中] powershell.exe[运行中] WindowsTerminal.exe[运行中] Google Chrome[运行中] 微信[运行中] Notepad++[运行中] Clash Verge...(其余 269 项省略)> const chromeApp = await computer.getApp('Google Chrome')> await chromeApp.getState()窗口: xxx - Google Chrome文本: Full accessibility state: [0] Window "xxx - Google Chrome" [1] Pane "xxx - Google Chrome" [2] Button "最小化" actions=["invoke"]...(后续窗口元素省略)listApps 只验证了基础接口;getState 能返回窗口内容,操控能力才算真正可用。
小结
这次解决的问题分属两个层面:上游发布缺陷(驱动有意以未签名状态发布,安装脚本却强制验签,契约矛盾在安装和运行时各拦截一次)、运行时机制(ESM 解析缓存)。
签名问题出现了两次,形式不同:安装阶段是脚本主动验签,运行阶段是操作系统拒绝启动 UIAccess 程序。绕过第一道时想不到还有第二道,直到实际操控窗口才发现。这给了一个提醒:验证一个能力是否真正可用,要用它最核心的功能去测。listApps 能列举应用,但它走的权限路径和窗口操控不同,用它做验证标准会得出错误的结论。
两个问题的排查思路是通用的:先分层确认每一层独立可用,再用对照实验缩小范围,最后检查缓存。
配置完成后,AI 就能直接操控桌面应用。后续我会用它做自动化尝试,有了结果再写。
附录:提交到 GitHub issue 的解决方案评论
以下评论已提交到 QwenLM/qwen-code#12394(issue 为英文,评论使用英文)。内容与本文的解决方案一致,供参考。文中提到的版本号问题已单独提交为 issue #13501(上游随后发布了 0.1.7,该 issue 已于 2026-10-13 关闭)。
Workaround for Windows (verified on Windows 11, Node v24.15.0, @qwen-code/cua-sdk@0.20.11):
The signature problem has two layers, and both need to be bypassed: the install-time script check, and a runtime OS check that blocks launching unsigned UIAccess executables.
Layer 1 — install failure. The postinstall script throws UIAccess worker Authenticode status is NotSigned. The signature check exists to guarantee the driver is official and untampered; SHA256 verification against the official checksums.txt provides the same guarantee:
- Download the driver archive from the official release:
https://github.com/QwenLM/qwen-code/releases/download/cua-driver-rs-v0.20.11/cua-driver-rs-0.20.11-windows-x86_64-binary.zip - Verify the SHA256 against
checksums.txtfrom the same release (expected:b46127a3f56e4aa267fa67ed9d3c9ac6f0b0259b50fcd65db1c80ab210825d87) - Extract to
%LOCALAPPDATA%\Qwen\cua-sdk\0.20.11\windows-x86_64\— keepcomplete.jsonin place, the runtime checks for it and skips the install script - Copy
qwen-cua-driver-uia.exetoC:\Program Files\Qwen\CuaDriver\0.20.11\(requires admin) - Install the package with scripts disabled:
npm install --no-save --package-lock=false --ignore-scripts @qwen-code/cua-sdk@0.20.11Layer 2 — runtime launch failure. After the steps above, ComputerUse.create() and listApps() work, but getState() / click() fail with uiaccess_worker_unavailable. Windows enforces the signature requirement again at process launch: the worker’s embedded manifest declares uiAccess="true", and Windows refuses to start unsigned UIAccess executables (launching it manually returns “A referral was returned from the server”).
Fix: patch the embedded manifest to uiAccess="false" via the Win32 UpdateResource API. The worker then runs as a normal process and can automate non-elevated windows (browsers, office apps, editors). Trade-off: it cannot control windows running elevated/as-admin. Run elevated (the exe is under Program Files):
Add-Type @"using System.Runtime.InteropServices;public class RU { [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] public static extern System.IntPtr BeginUpdateResource(string f, bool d); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool UpdateResource(System.IntPtr h, System.IntPtr t, System.IntPtr n, ushort l, byte[] d, uint s); [DllImport("kernel32.dll", SetLastError=true)] public static extern bool EndUpdateResource(System.IntPtr h, bool disc);}"@$exe = 'C:\Program Files\Qwen\CuaDriver\0.20.11\qwen-cua-driver-uia.exe'$b = [IO.File]::ReadAllBytes($exe); $t = [Text.Encoding]::UTF8.GetString($b)$i = $t.IndexOf('uiAccess'); $s = $t.LastIndexOf('<?xml', $i); $e = $t.IndexOf('</assembly>', $i)$m = $t.Substring($s, $e + 11 - $s).Replace('uiAccess="true"', 'uiAccess="false"')$d = [Text.Encoding]::UTF8.GetBytes($m)$h = [RU]::BeginUpdateResource($exe, $false)[RU]::UpdateResource($h, [IntPtr]24, [IntPtr]1, 0x0409, $d, $d.Length) | Out-Null[RU]::EndUpdateResource($h, $false) | Out-NullAfter patching, getState(), click(), typeText(), hotkey() all work — I used it to navigate Chrome and interact with GitHub pages.
Both layers are reversible: delete the manually installed directories and reinstall normally once a signed driver ships.
Related Windows setup notes (anyone following the computer-use skill guide will hit these next):
- The skill guide references
@qwen-code/node-repl-mcp@0.1.7, which does not exist on npm — latest is 0.1.6. Use 0.1.6. qwen mcp addexpects the command and its arguments as separate CLI positionals:qwen mcp add --scope user node-repl npx -y @qwen-code/node-repl-mcp@0.1.6(unquoted). Passing the whole command as a single quoted string stores it verbatim in thecommandfield ofsettings.json, and the server never starts (shows Disconnected). Check~/.qwen/settings.jsonif that happens.- If imports failed before the package finished installing, reset the node-repl kernel (
node_repl_reset) — Node’s ESM resolver caches failed resolutions.