Personal Blog

轶哥

轶哥的头像

妄图改变世界的全栈程序员。

Article

Agent远程控制Windows设备

目录
  1. 我想要的不是另一台开发机
  2. 这里说的“控制”,不是远程桌面
  3. 在 Windows 11 启用 OpenSSH Server
  4. 让 Agent 稳定执行远端 PowerShell
  5. 一次真正的 win-test
  6. 为什么要把这件事教给 Agent
  7. 命令行走不到的地方,交给 GUI MCP
    1. 普通用户不需要再手动安装一遍
    2. Agent 实际怎么操作 Windows 界面
    3. 这不是一条远程桌面隧道
  8. 多智能体在这里不是为了热闹
  9. 先把安全边界说清楚
  10. 我踩过的几个坑
  11. 如果你也想做一台 win-test
  12. 最后
  13. 相关阅读

写 Windows 功能有一个很朴素的麻烦:代码可以在 Mac 上写,问题却只肯在 Windows 上出现。

以前每次验证,我都要走到那台 Windows 小主机旁边,登录、打开应用、重复一遍刚才的操作,再把日志和截图带回来。机器离我只有几米,流程却像隔着一条河。

后来我给它起了一个很普通的名字:win-test

现在,我可以直接把任务交给 Agent。它先确认设备在线,再通过 SSH 进入一台真正的 Windows 11,运行 PowerShell、查服务、看进程、读注册表,最后把结果带回来。要改东西时先停下来确认,改完再读一次状态,而不是丢下一句“命令执行成功”。

这篇文章不讲什么神奇的“AI 接管电脑”,只记录我实际使用的这条链路,以及它为什么比我一开始想的麻烦,也比远程桌面更适合 Agent。

我想要的不是另一台开发机

虚拟机当然能测 Windows,但有些问题只会出现在一台接近普通用户环境的真机上:PowerShell 版本、文件权限、Windows 服务、注册表、路径分隔符、系统编码、防火墙,甚至应用商店留下的执行别名,都可能让结果拐个弯。

一台装满开发工具的 Windows,也未必是理想测试机。它越像我的开发环境,越容易把代码里的“想当然”藏起来。

win-test 的目标很明确:把一台 Windows 真机接进开发和验收流程。以前我说“我刚才在那台机器上看过了”;现在需要的是一条可以重跑、可以留下证据、失败后知道去哪里找原因的链路。

这里说的“控制”,不是远程桌面

这套方法的结构其实很简单:

我在 DesireCore 中交代任务
        ↓
Agent 判断风险、拆解步骤
        ↓
SSH 别名连接 Windows OpenSSH
        ↓
cmd.exe / PowerShell 执行
        ↓
JSON、日志、退出状态和复查结果
        ↓
Agent 汇报,必要时等待人工确认

它和 RDP 不是一回事。

RDP 是我坐在屏幕前点鼠标;SSH 更像给 Agent 留了一扇窄门。它可以查询文件、进程、服务、CIM/WMI、注册表、网络状态和计划任务,也可以运行脚本,但看不到一个完整的交互式桌面。

这个边界很重要。SSH 会话里能启动进程,不代表窗口一定出现在当前用户眼前;能加载截图相关组件,也不代表它一定能看到正在使用的桌面。需要操作原生 GUI 时,应该走专门的桌面自动化通道,而不是假装一条 PowerShell 命令拥有眼睛。

在 Windows 11 启用 OpenSSH Server

Windows 11 将 OpenSSH Server 作为可选功能提供。先在管理员 PowerShell 中安装并启动服务:

Get-WindowsCapability -Online | Where-Object Name -Like 'OpenSSH.Server*'
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
Get-NetFirewallRule -Name OpenSSH-Server-In-TCP

如果最后一条没有找到入站规则,再按照微软的 OpenSSH Server 配置文档 创建 TCP 22 规则。不要因为 ping 不通就判断 SSH 也不通,Windows 防火墙可能只拦了 ICMP,直接检查 TCP 22 更准。

接下来配置密钥认证。微软文档里有一个很容易踩的细节:普通用户和管理员用户存放公钥的位置不同。管理员账户通常使用:

C:\ProgramData\ssh\administrators_authorized_keys

而且这个文件的 ACL 要收紧,只保留 Administrators 和 SYSTEM 所需权限。很多 Permission denied (publickey),不是密钥生成错了,而是公钥放错了地方,或者权限看起来“更宽松”,Windows OpenSSH 反而拒绝使用。

我会在 Agent 所在设备上生成一对独立密钥:

ssh-keygen -t ed25519 -f ~/.ssh/win-test

私钥留在本机,公钥写入 Windows 对应的 authorized_keys 文件。管理员账户的 ACL 可以参考微软文档使用 icacls 收紧。非英文版 Windows 的本地组名可能不同,照抄英文组名也会失败,微软建议必要时改用 SID。具体步骤见 Windows OpenSSH 密钥认证文档

以下配置全部使用占位符,不公开真实 IP、用户名、机器名、密钥内容和指纹。

我在本机的 SSH 配置里只保留一个别名,形式大致如下:

Host win-test
    HostName <windows-ip>
    User <windows-user>
    IdentityFile <private-key-path>
    IdentitiesOnly yes

真正开始做事前,Agent 先跑的不是业务命令,而是连通性预检:

ssh -o BatchMode=yes \
    -o PasswordAuthentication=no \
    -o ConnectTimeout=8 \
    win-test \
    'cmd.exe /d /s /c "whoami & hostname & echo REMOTE_WINDOWS_OK"'

BatchMode=yes 很关键。没有它,认证失败时 SSH 可能停在密码框或确认提示上。人坐在终端前还会发现它在等输入,Agent 遇到这种情况只会一直等到超时。

我会让它证明三件事:别名能解析;认证过程不会等待人工输入;Windows 能返回预先约定的标记。三关都过了,后面才轮到 PowerShell。

让 Agent 稳定执行远端 PowerShell

简单命令很好办:

ssh win-test "powershell.exe -NoLogo -NoProfile -NonInteractive -Command Get-Service"

稍微复杂一点,事情就开始难看了。

一段命令从 macOS 发出,要经过本地 Shell、SSH、Windows 默认 Shell,最后才落到 PowerShell。肉眼看着完全正确的 $、引号、反斜杠和花括号,走完这一圈,可能已经不是原来的东西。

我最初也试过继续加反斜杠。加到第三层以后,我已经很难确认每个引号究竟由哪一层解释。能跑一次,不代表下次还看得懂。

现在复杂脚本会先转成 PowerShell 需要的 UTF-16LE Base64,再通过 EncodedCommand 发送:

powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand <BASE64>

这里的 Base64 不是加密,只是为了解决转义。密码、Token、私钥仍然不能塞进去。微软在 about_PowerShell_exe 中也明确说明了 EncodedCommand 使用 UTF-16LE 编码。

中文输出则要单独处理。PowerShell 5.1 的默认编码很容易让中文变成乱码,我在需要中文回显时会先设置:

[Console]::OutputEncoding = [Text.Encoding]::UTF8

能返回 JSON 的任务,尽量不要让 Agent 猜一大段自然语言输出。比如检查环境,我更愿意让 Windows 回这样一个结果:

$result = [ordered]@{
    computer   = $env:COMPUTERNAME
    powershell = $PSVersionTable.PSVersion.ToString()
    sshd       = (Get-Service sshd).Status.ToString()
}

$result | ConvertTo-Json -Compress

Agent 不需要从“命令未找到”或本地化后的表格里猜结论,字段是什么,结果就是什么。

一次真正的 win-test

这次写文章前,我又把链路从头跑了一遍。

别名解析正常,BatchMode 没有卡在密码和指纹提示上;远端 cmd.exe 返回了约定标记;PowerShell 的 EncodedCommand 能执行,并返回压缩后的 JSON。实际结果做了脱敏,只保留验证结论:

验证项 实测结果
远端身份 返回了预期的 Windows 用户和主机名
PowerShell 5.1,UTF-8 中文输出正常
结构化脚本 EncodedCommand 返回有效 JSON
系统查询 文件、进程、服务、CIM/WMI、注册表、网络和计划任务均可读取
失败状态 返回非零,但外层包装改写了具体数值

我没有顺手跑安装、写文件或传输测试。验证能力不等于每次都要把权限用满。能用只读方式证明的事情,就没有必要为了截图去改一遍远端系统。

倒是遇到了一个不起眼、但很典型的坑:我让远端进程主动 exit 7,本地 SSH 最后拿到的却是 1

如果代码写成“返回 7 才算我预期的失败”,这个测试就会误判。按 SSH 的正常语义,它会返回远端命令的退出状态;这次的结果说明 cmd.exe、PowerShell 或外层包装中至少有一层改写了数值,后面还需要单独定位。实际任务不能只押在一个数字上,我会把几项放在一起看:

  • 退出状态是不是非零;
  • 标准错误里写了什么;
  • JSON 中有没有明确的 successerror 字段;
  • 操作以后,目标状态有没有真的改变。

服务启动后再查一次服务,进程拉起后再查端口,配置修改后重新读取。返回成功只能说明命令没有报错,不能证明目标状态已经改变。

为什么要把这件事教给 Agent

如果只是偶尔连一台机器,Shell 历史记录已经够用。真正让我觉得值得做成 Agent 能力的,是这些细节会反复出现:

  • 开始前必须做无交互连接测试;
  • 默认只读,涉及安装、删除、重启和系统配置时先确认;
  • 中文输出要切到 UTF-8;
  • 复杂脚本使用 EncodedCommand
  • 修改前读取,修改后复查;
  • 汇报中隐藏地址、账户、路径和密钥信息;
  • DHCP 地址变化时,先查别名和网络,不要急着重做密钥。

如果这些规矩只留在当前对话和 Shell 历史里,下一次任务仍然要重新发现。于是我把连接预检、默认只读、风险确认和结果复查写进 Agent 的规则与 Skill。下一次我说“去 Windows 真机复核一下”,它知道先做什么、哪些动作可以直接做、什么地方必须停下来问我。

更重要的是,保存下来的不是私钥本身。密钥由本地 SSH 管理,Agent 只需要知道一个经过授权的设备别名和操作规矩。能力与秘密分开,远比把一段账号密码粘进提示词可靠。

这些规则、技能、记忆和任务回执会进入 AgentFS。它们是可查看、可版本管理的文件。某天我发现“精确退出码不能作为唯一依据”,这次纠正可以进入 Skill;如果教错了,也能审阅和回滚。

可培养 Agent 在设备控制场景里的价值,是记住一整套做事边界,包括我愿意把设备交给它到什么程度。

命令行走不到的地方,交给 GUI MCP

SSH 跑通以后,Windows 上大部分系统问题已经可以交给 Agent:查服务、读注册表、跑脚本、抓日志,都不难。

剩下那一小截,往往才最磨人。

安装向导停在哪一步,旧软件弹了什么框,某个按钮到底是没加载出来,还是已经变灰——这些事情,PowerShell 再熟也看不见。

这里说的 MCP,不是给 SSH 再套一层工具。DesireCore 在 Windows 上接入的是一套真正面向 GUI 的 MCP:CUA Driver。

我更愿意把它理解成给 Agent 补了一双眼睛和一双手。它可以读取当前窗口和控件状态,也可以截图、点击、输入文字、发送快捷键,再把操作后的界面读回来。

实际链路是这样的:

DesireCore Agent
        ↓
系统内置 MCP:cua-driver
        ↓
cua-driver mcp
        ↓
Windows UIA / MSAA / PostMessage / SendInput
        ↓
记事本、安装程序和其他 Windows 应用

普通用户不需要再手动安装一遍

我写这篇文章时,DesireCore Windows 正式安装包已经内置 CUA Driver 0.7.1,并在 Windows 上默认启用。

也就是说,正常情况下,安装 DesireCore Windows 版已经完成了 MCP 的安装和接入。用户不需要再装 Python,不需要用 npx 下载社区包,也不用手工修改 agent.json

DesireCore 内部注册的系统 MCP 大致如下:

{
  "transport": "stdio",
  "command": "cua-driver",
  "args": ["mcp"],
  "enabled": true,
  "probe_command": true
}

这段配置不是让普通用户照着粘贴的,它是 DesireCore 在 Windows 上的系统默认项。

每次 Agent 开始任务时,DesireCore 会先检查 Windows 的 PATH。如果用户自己安装了较新的 CUA Driver,就优先使用用户版本;如果没有,则自动回退到安装包内置的二进制文件。两边都找不到时才会静默跳过,不会让每次对话都卡在一个不存在的工具上。

在“资源管理器 → 运行环境”中,还能看到当前使用的 CUA Driver 版本和来源。对普通用户来说,这比记住某个安装目录更实际。

Agent 实际怎么操作 Windows 界面

第一次使用时,我不会直接让 Agent 到处点,而是先给它一个很窄的任务:

只列出当前打开的 Windows 窗口。
不要点击,不要输入,也不要关闭任何程序。

这时 Agent 会调用类似这样的工具:

cua-driver__list_windows

确认目标窗口以后,再逐步放开截图、读取控件、点击和输入:

cua-driver__get_window_state
cua-driver__screenshot
cua-driver__click
cua-driver__type_text

我们在 Windows 真机上做过一次完整测试。DesireCore 成功发现了 39 个 GUI 工具,列出了当时真实存在的 11 个窗口和 9 个应用。

随后我让 Agent 打开记事本并输入一句测试文字。

它先找到记事本的编辑区域,尝试用 set_value 直接写入。Windows 11 新版记事本没有给这个控件实现对应的 ValuePattern,所以第一次调用失败了。

Agent 没有在这里停住,也没有假装操作成功。它读懂错误以后,换成了另一条路线:

启动记事本
  ↓
读取窗口和控件
  ↓
set_value 失败
  ↓
点击编辑区域
  ↓
模拟键盘输入
  ↓
重新读取窗口内容

最后,Agent 从窗口状态里读回了刚刚输入的完整文字。工具返回、独立查询和肉眼检查三边一致。

这件事看起来不大,却很能说明可培养 Agent 的意义。

MCP 提供的是操作能力,Agent 学到的则是做事的方法:新版记事本不能直接写值时,要切换成点击加输入;操作以后不能只相信“调用成功”,还要重新读取界面;下次遇到同一类软件,不必再从第一个坑重新踩起。

如果任务是安装验收,我还会把执行和复核拆给两个 Agent。一个负责点击和输入,另一个只看最终窗口、版本和截图,避免执行者顺手给自己的结果判个“通过”。

这不是一条远程桌面隧道

这里还有一个边界必须说清楚。

DesireCore 当前内置的 CUA Driver MCP,运行在安装 DesireCore 的那台 Windows 设备上。它通过本机 stdio 启动 cua-driver mcp,操作的也是这台机器当前的 Windows 桌面。

它不是让 Mac 上的 DesireCore 穿过 SSH,直接取得远端 Windows 的鼠标和键盘。

所以在本文这套方案里,两条链路各做各的事:

  • SSH 和 PowerShell 负责远程查询服务、文件、注册表和系统状态;
  • 目标 Windows 上的 DesireCore 通过 CUA Driver MCP 处理窗口、按钮、输入框和截图。

把这两件事写成同一个“SSH MCP”,反而会让人误以为命令行已经拥有了桌面会话。

GUI 操作也有自己的硬边界。UAC 安全桌面、Ctrl+Alt+Del、更高权限级别的窗口,以及部分使用特殊渲染方式的应用,都不能当作普通按钮随便点。

DesireCore 不会因为 MCP 声称自己是只读工具就自动相信它。MCP 操作真实宿主机,在默认确认策略下会经过人工确认;只有我明确把 Agent 放到更宽松的权限模式,它才会跳过这道闸门。

我现在的习惯仍然是:先列窗口,再读取状态,确认窗口标题和进程,最后才允许点击和输入。操作结束以后,再读一次窗口或者补一张截图。

GUI 自动化最怕的不是失败,而是点错了以后还告诉你一切正常。

DesireCore Windows GUI 自动化实际界面

DesireCore 在 Windows 上通过内置 CUA Driver MCP 读取窗口、点击和输入。它不是 SSH MCP,也不是远程桌面。

多智能体在这里不是为了热闹

查服务或读日志时,一个 Agent 足够。完整安装验收中,我会把执行和复核分开:一个负责操作,另一个只根据版本、日志和最终状态判断是否通过。这样做是为了避免执行者顺手给自己的结果盖章。DesireCore 的 跨智能体协作 在这里刚好用来隔开两份上下文。

先把安全边界说清楚

免密登录配置完成后,这台机器也多了一个需要认真管理的远程入口。

密钥登录不是“没有密码所以更安全”,只是把安全责任从记住密码,换成了保管私钥、限制网络边界和控制操作权限。我现在给 win-test 定的规矩很朴素:

  • 只在可信局域网或 VPN 内使用,不把 SSH 端口直接暴露到公网;
  • 最好使用专用测试账号和独立密钥,默认给够用的最低权限;
  • 首次连接由人核验主机指纹,不用 StrictHostKeyChecking=no 图省事;
  • 私钥、密码、Token 和恢复码不进入提示词、截图、脚本参数和日志;
  • 删除文件、安装软件、重启机器、修改服务与系统设置,必须经过人工确认;
  • 高风险操作前做预检,操作后读回状态;
  • 任务结束后清理临时脚本和调试入口,保留必要回执。

DesireCore 的 人闸门 很适合放在“从读取变成修改”的那一刻。查询服务可以自动做;重启服务、安装驱动或删除文件,Agent 应该先把计划、影响和回退方式讲清楚,再等人点头。

我踩过的几个坑

现象 常见原因 我的处理方式
昨天能连,今天超时 DHCP 地址变化、休眠或网络切换 先检查别名解析、TCP 22 和当前地址,不急着重做密钥
Permission denied (publickey) 管理员公钥位置或 ACL 不对 使用 Windows 对应的密钥文件位置并收紧权限
Agent 一直不返回 SSH 正在等密码或首次连接确认 强制 BatchMode,设置连接超时,首次指纹由人确认
中文输出乱码 PowerShell 5.1 默认输出编码 显式切换 UTF-8,复杂结果尽量返回 JSON
命令肉眼正确,远端却报错 多层 Shell 把引号和变量拆坏 简单命令直接跑,复杂脚本使用 EncodedCommand
进程存在,但桌面上没有窗口 SSH 会话不是当前交互式桌面 不把进程状态当 GUI 证据,需要时走桌面自动化
返回码和远端预期不同 中间 Shell 或进程改变了退出状态 判断零/非零、stderr、结构化错误和最终状态
命令显示成功,实际没变化 只检查了调用,没有检查结果 每个变更动作都安排一次读回验证

这些问题单看都不大,凑在一起就很容易消耗一个下午。最值得培养的 Agent,往往不是会背最多命令的那个,而是踩过一次以后真的会绕开旧坑的那个。

如果你也想做一台 win-test

我建议先从只读巡检开始,不要一上来就追求“让 AI 自动安装软件”。

先让 Agent 完成一次最小闭环:无交互连接;返回预期用户和主机名;读取 PowerShell 版本;检查一个服务;输出 JSON;把命令、结果和仍然不确定的地方汇报出来。

接着故意制造几个安全的小问题:关闭一个测试进程、让一段脚本返回非零、输出一行中文、把地址换成失效值。看看 Agent 是真正理解失败,还是只会把终端最后一行重新念一遍。

等只读检查稳定以后,再加入需要人工确认的修改操作。GUI 自动化放到更后面,因为“看见并点击”比“读取并验证”更容易受环境变化影响。

这个顺序不够像一段炫目的演示,却更接近真实使用。

最后

这套链路把一次性的人工目测,变成了可以重跑和复查的 Windows 真机验证。Agent 执行命令以后,还要留下身份、输出、退出状态和最终状态,测试才算完成。

那台小主机多数时间仍然安静地放在桌边。不同的是,现在发现一个 Windows 问题以后,我可以把排查方法留下来,下次不必再从远程桌面的第一下点击开始。


相关阅读

打赏
交流区

暂无内容

尚未登陆
发布
  上一篇
下一篇 (AI写标书踩过的那些坑)