GUI 层真机才有的能力
企微 / 抖音客户端跑在真实 Windows 桌面上,靠截屏读、模拟点击写。没有接口可调,也就没法搬到无头容器里。
阿里云 CLI + 无影云电脑 RunCommand:不连远程桌面、不用人坐在机器前,从 Mac 终端把脚本送进 Windows,再把结果读回来。
// 全文命令均在 Akke 生产云电脑上实跑验证 · 回执为真实输出,非示意
Akke 有一类工作没法在服务器上跑:企业微信自动代回、抖音私信 / 反向评论。这些平台没有可用的对外接口,只能开一台真 Windows、装真客户端、用模拟人操作的方式干活。于是就有了云电脑机队——而机队一旦超过一台,「人连远程桌面手动操作」立刻变成瓶颈。
企微 / 抖音客户端跑在真实 Windows 桌面上,靠截屏读、模拟点击写。没有接口可调,也就没法搬到无头容器里。
aliyun ecd run-command 让脚本从 Mac 直达机器内部:装依赖、改配置、重启进程、读日志,全程不开远程桌面。
首次企微扫码登录、拍板放开真发——这些留给人。其余的部署、校准、验活、恢复都自动化掉。
必须是企业版无影(ECD / API 2020-09-30)才有 RunCommand。个人版没有云助手通道,只能靠人连客户端——这也是 Akke 当初从个人版升企业版的唯一理由。
CLI 本身没什么可说的;凭据的粒度才是要想清楚的地方。不要用主账号 AccessKey——建一个专职 RAM 用户,只授它需要的那一类权限。
# 1. 装(Homebrew 或直接下二进制均可) brew install aliyun-cli aliyun version # → 3.3.22 # 2. 配一个具名 profile(不要往 default 里塞生产钥匙) aliyun configure set \ --profile akke-wuying \ --mode AK \ --access-key-id <RAM 用户的 AK ID> \ --access-key-secret <RAM 用户的 AK Secret> \ --region cn-chengdu # 3. 核实这把钥匙确实活着 aliyun configure list
Profile | Credential | Valid | Region | Language default | | Invalid | | en akke-wuying * | AK:***61w | Valid | cn-shenzhen | en
// 注意那个 *:它标的是当前默认 profile。Akke 这台 Mac 上 default 是空的、有效的是 akke-wuying——所以 profile 的默认 region 是 cn-shenzhen,而机器一台都不在深圳。这正是下一节第一个坑的来源。
Akke 的 RAM 用户 akke-wuying-bot 挂 AliyunECDFullAccess——足够查机器、发命令、重启。但开新机器还需要 AliyunBSSOpenAPIFullAccess(费用权限),否则子账号既看不到余额也改不了支付方式,会卡在「机器好像没创」。
ECDFullAccess 含 run-command(任意代码执行)与 delete-desktops。它等价于机队 root,别塞进 CI 的公共 secret、别写进会被打包的 .env;轮换要当成换生产密码来对待。
无影 CLI 最反直觉的一点:同一件事在不同 API 版本里叫不同的名字,甚至根本不存在。绝大多数「命令找不到 / 返回空」都不是权限问题,而是这三个坐标之一填错了。
| API 版本 | 管什么 | 典型命令 |
|---|---|---|
| 2020-09-30 | 企业版 ECD(经典无影)——机器本身与远程命令,本文主角 | ecd run-command ecd describe-desktops ecd create-desktops ecd reboot-desktops |
| 2021-03-08 | 便捷账号 / 用户管理。区域固定 cn-shanghai,与机器所在区无关 | eds-user create-users eds-user describe-users |
| 2021-06-22 | 新版 EDS(轻量无影)。CLI 默认走它,删掉了用户管理命令 | get-user-by-token start-token |
// 所以 describe-users「命令不存在」的真相是:CLI 默认挑了 2021-06-22。显式 --api-version 2021-03-08 它就回来了。本文所有命令都显式写版本,一个都别省。
region 要写两遍,而且必须是机器真实所在区。--region 决定请求打到哪个 API 端点,--biz-region-id 决定查哪个区的资源——两个都不写,CLI 直接拒绝;只写 --region 却让它继承 profile 的 cn-shenzhen,返回的是空列表而不是报错,最容易被误读成「机器没了 / 在别的阿里云账号里」。
$ aliyun ecd describe-desktops --api-version 2020-09-30 Error: --biz-region-id is required
aliyun ecd describe-desktops \ --api-version 2020-09-30 \ --region cn-chengdu --biz-region-id cn-chengdu \ | jq -r '.Desktops[] | [.DesktopId,.DesktopName,.DesktopStatus] | @tsv'
下表是把上面那条命令在成都、杭州各跑一次的真实返回(2026-07-28)。规格统一 eds.enterprise_office.6c12g + 80 G cloud_auto,约 ¥249/月一台。
| 用途 / 人设 | 区域与授权账号 | DesktopId |
|---|---|---|
| 小顾 | 成都 · 企微自动代回 · 授权 shawsnk | ecd-5qfbmvz3yvpvbiog0 |
| 夏伟 | 成都 · 企微自动代回 · 授权 shawsnk / xix5494 | ecd-eq68qf5rxfcpvxrl2 |
| 野荞 | 杭州 · 抖音触达 · 授权 xix5494 / shawsnk | ecd-6mc7dgh8kldv19baa |
| 夏夏 | 杭州 · 抖音触达 · 授权 yuanyurou545 / shawsnk | ecd-1yrf84368u89vysox |
无影客户端里显示的卡片名(YDYX-Xiawei 之类)是客户端本地别名,控制台和 API 里查不到——曾按客户端名全区域扫,扫不到就以为「机器在另一个阿里云账号」。可靠做法:认 DesktopId,或让脚本回报机内的人设标识(Akke 读 C:\akke-wecom\.env 的 AKKE_WECOM_PERSONA)。
给机器加人用 modify-user-entitlement,绝不要用 modify-entitlement。后者语义是「把该机器的授权用户整体设成你传的这批」——它会先删掉原有全部用户,手滑就把 shawsnk 踢掉、连带杀掉跑在它交互会话里的自动化。而且单会话机上新用户一连上就会强制注销原会话,想共用同一套自动化只能共用同一个账号,而不是加用户。
RunCommand 是异步的:发命令拿到一个 InvokeId,再用它去查结果。两步分开是刻意设计——脚本可以跑几分钟,而 HTTP 请求早就返回了。
--content-encoding Base64)。命令体上限 16 KB(编码后),大脚本得先落盘再执行
ecd run-command 交给云助手,立刻返回 InvokeId。一次最多 50 台,默认超时 300 s(Akke 按脚本长短显式给 60~400 s)
describe-invocations --include-output true 取 base64 的 stdout。回执留存约 2 周,输出超 24 KB 截断(丢弃字节数记在 Dropped)
# ① 下发(脚本用 printf 避免 echo 带尾换行)
B64=$(printf '%s' 'Write-Output "PROBE $(hostname) $(Get-Date -Format o)"' | base64)
INV=$(aliyun ecd run-command \
--api-version 2020-09-30 \
--region cn-chengdu --biz-region-id cn-chengdu \
--type RunPowerShellScript \
--content-encoding Base64 --command-content "$B64" \
--desktop-id ecd-5qfbmvz3yvpvbiog0 \
--timeout 60 | jq -r '.InvokeId')
# ② 读回(--include-output true 是关键,见下)
aliyun ecd describe-invocations \
--api-version 2020-09-30 \
--region cn-chengdu --biz-region-id cn-chengdu \
--invoke-id "$INV" --include-output true \
| jq -r '.Invocations[0].InvokeDesktops[]
| [.DesktopId,.InvocationStatus,.ExitCode,(.Output|@base64d)] | @tsv'
Success
ecd-5qfbmvz3yvpvbiog0 Success 0 PROBE fbmvz3yvpvbiog0 2026-07-28T15:56:50+08:00
py=2
// 回执带机内主机名和当下秒级时间戳——这是判断「命令真的执行了」而不是读到缓存的硬证据。顺手把 py=2(loop 的 Python 进程数)也带回来,一次调用同时完成连通性验证和业务巡检。
输出为空 ≠ 平台吞了 stdout。官方文档明写 Output 只在请求带 IncludeOutput=true 时才返回。Akke 曾把「Output 全空」当成云助手 bug,为绕它发明了一整套第三方消息中转方案;后来发现只是少传了一个参数。读回不通时,先把文档里的参数列表逐个对一遍,再怀疑平台。
这是 RunCommand 最硬的一条约束,也是所有「命令成功了但界面毫无反应」的根源:脚本跑在会话 0,桌面在会话 1。Windows 从设计上就把服务与交互桌面隔开了。
能做:装 Python / 依赖、下载与替换脚本、写 .env、查进程、读日志、静默装软件、重启常驻任务。
不能做:截屏、点击、打字——看不到桌面。这里 pyautogui.size() 报的是无显示器的兜底值(如 1024×768),不是真分辨率。
企微 / 抖音客户端、模拟点击、截屏读屏都在这里。前提是有人登录着(query user 显示 console 会话运行中)。
Akke 的机器上 shawsnk 常驻登录,因此会话 1 一直存在——这是整条自动化链的隐含依赖。
会话 0 虽然自己碰不到 GUI,但它可以注册一个 LogonType=InteractiveToken 的计划任务(schtasks /create /xml,指定 机器名\用户),该任务就会在登录着的会话 1 桌面里运行 → 截屏、点击、打字全部可用,且免密码。Akke 用它把坐标标定、空跑验证、常驻 loop 的启停全部搬到了 Mac 端遥控,只剩首次扫码登录必须真人。跑 GUI 脚本用 pythonw.exe,免黑窗盖住被操作的窗口。
会话 0 里 py.exe 起不来、python 也可能不在 PATH。Akke 的机器上 Python 装在 C:\Program Files\Python312\ 但没勾 Add to PATH:where python 是空的,只有 C:\Windows\py.exe 可用;而 SYSTEM 身份下 py 启动器又找不到注册表项。稳妥做法:脚本里写绝对路径,或干脆纯 PowerShell 实现。另外把 PATH 写进注册表后,云助手服务的环境块有缓存、当场新起的 RunCommand 进程看不到,要重启才刷。
遥控一台看不见的机器,回执通道的可信度就是全部。Akke 在这上面栽过三次,每次都得出「平台坏了」的结论,复测后两次是自己的错。这一节是那三次的净结论。
| 通道 | 现在的口径 | 取值位置 |
|---|---|---|
| Output(stdout) | 可信。必须带 --include-output true;base64 解码,超 24 KB 截断 | InvokeDesktops[].Output |
| ExitCode | 可信。忠实反映脚本里的 exit N,可用来编码诊断结果 | InvokeDesktops[].ExitCode |
| InvocationStatus | 可信。非零退出会翻 Failed + ExitCodeNonzero | 顶层 与 InvokeDesktops[] 各有一份 |
| 机内 HTTPS 外发 | 看域名。国内直连出口,部分域名被按 SNI 阻断(下述) | — |
| schtasks 返回码 | 不可信。rc=0 不代表任务动作真起来了 | 改看进程存在性 / 业务指标 |
// 前三行都曾被写进内部记忆当成「不可信」,后来用对照探针(脚本里故意 exit 7 / exit 3)逐台复测推翻:写的 7 回的就是 7。当初的误判是读错了层级——逐台的 ExitCode 在 InvokeDesktops[] 里,不在顶层。
机器出口是国内阿里云直连、无代理。Test-NetConnection openrouter.ai -443 握手成功,但 HTTPS GET 全超时——按 SNI 定向阻断,不是断网(同机 baidu 200、Fly 东京 200)。GitHub raw / gist / jsDelivr 同样中招。
Akke 的解法:把要用的境外端点统一经自家 Fly worker 中转,机器只认一个可达域名。
Akke 曾用「机内 GET 一个不存在的路径 → Mac 端 grep 服务器日志」当回执,发 3 次、抓 86 秒日志、0 命中,结论写成「这两台机器的 RunCommand 不执行」。次日复测两台全部正常。真因是那个日志通道一次只回 100 行、而日志约 25 行/秒——快照窗口只有 4 秒,稀疏事件必然抓不到。
否定性证据要先证明通道在正例下会响。拿某条日志 / 字段当回执前,先用一个「必然产生该信号」的动作跑一遍、确认看得见;否则「收不到信号」和「动作没发生」无法区分,好通道会被判死。
对照组管假成功,通道存在性管假失败——两面缺一不可。
重启常驻进程时 schtasks /run 返回 0 也可能什么都没起来。Akke 的任务动作是 cmd /c "... >> _loop.log 2>&1";刚被杀掉的上一个实例还攥着日志句柄 4~8 秒,追加重定向建不起来 → cmd 立刻 exit 1,而错误信息本身要走那个失败的重定向,所以哪儿都看不到。修法:拉起前用同一个操作探测日志可写(echo. >> _loop.log,rc=0 才继续,最多等 60 s),验收一律看进程存在性和业务指标。这是 Windows 通用坑,不限无影。
Akke 的企微 loop 主脚本已经 88 KB,而命令体上限 16 KB;同时这台机器上 GitHub raw / gist / jsDelivr 全部被墙,公共粘贴板对 >85 KB 直接 500。外部中转全死,只剩把文件顺着 RunCommand 本身灌进去。
git show origin/main:<file> | gzip | base64 → 88 KB 压到 43 KB
split -b 9000 → 5 块,每块塞得进 16 KB 命令体
Add-Content -NoNewline 追加到 _loop.gz.b64
FromBase64String + GZipStream 解出 → Get-FileHash MD5 与 Mac 侧 md5 -q 对齐,匹配才替换旧文件
// MD5 对齐这一步不能省。分块传输任何一块丢了都会得到一个「看起来完整」的坏文件;Akke 曾因为部署链接过期,把缺了几周护栏的旧版脚本部上生产,症状是「代码明明修了、线上还在犯老错」。先校验,再替换,留 .bak。
写 .env 千万别用 Set-Content -Encoding UTF8——它会加 BOM。三个字节 EF BB BF 贴到文件开头,如果第一行恰好是一个 key,key 名就被污染成 ANTHROPIC_API_KEY,程序读不到、自检失败死循环。Akke 同一个操作在两台机上命运不同:一台第一行是 key(坏了),另一台第一行是注释(侥幸没事)。
正确写法:New-Object System.Text.UTF8Encoding($false) + [IO.File]::WriteAllLines。排查时别只看 key 存不存在,读原始字节验首三字节。
中文日志会在 PowerShell → jq 的管道里烂掉。中文系统 PowerShell 默认 GBK(936),Python 输出是 UTF-8,读回来是 鈿狅笍 鍒嗚鲸鐜囧彉浜 这种乱码(原文是「⚠️ 分辨率变了」)。读之前先 [Console]::OutputEncoding=UTF8 + Get-Content -Encoding UTF8。
还有一个更阴的:被常驻进程独占的日志文件,[IO.File]::ReadAllLines 会静默返回 0 行——看起来像「日志是空的」,其实是没用共享读模式打开(要 FileStream(..., FileShare::ReadWrite))。
机队扩容也全走 CLI。顺序是:查办公网络 → 建便捷账号 → 建机器(同时授权)→ 付款 → 验状态。
# ① 查办公网络 ID(机器必须落在某个 office site 里)
aliyun ecd describe-office-sites --region cn-hangzhou \
| jq -r '.OfficeSites[].OfficeSiteId'
# ② 建便捷账号(注意:固定 cn-shanghai,且是 2021-03-08)
aliyun eds-user create-users \
--api-version 2021-03-08 --region cn-shanghai \
--name xix5494 --nickname '野荞' --is-local-admin true
# ③ 建机器,同时用 --end-user-id 直接授权(不必事后再分配)
aliyun ecd create-desktops \
--api-version 2020-09-30 \
--region cn-hangzhou --biz-region-id cn-hangzhou \
--office-id 'cn-hangzhou+dir-xxxxxxxx' \
--end-user-id xix5494 \
--desktop-name akke-cloudpc-hz-yeqiao \
--desktop-type eds.enterprise_office.6c12g \
--system-disk-category cloud_auto --system-disk-size 80 \
--image-id '<enterprise-image-id>' \
--auto-pay false \
--desktop-attachment '{"SystemDiskCategory":"cloud_auto","DataDiskCategory":"cloud_auto","DataDiskSize":0}'
# ④ 到费用中心付返回的 OrderId → ⑤ 等状态变 Running
aliyun ecd describe-desktops --api-version 2020-09-30 \
--region cn-hangzhou --biz-region-id cn-hangzhou | jq '.Desktops[].DesktopStatus'
--desktop-attachment 里只写 SystemDiskCategory 会 400:User disk category is not same as root disk。阿里云要求数据盘与系统盘类型一致,两个字段都要写、且值相同(即便 DataDiskSize 是 0)。
--auto-pay true +余额不足=整单作废包月机创建即冻结余额(¥249/月)。余额不够时 API 直接 400、连 DesktopId 都不返回,现象就是「机器好像没创」。用 --auto-pay false:先返回 OrderId +预分配 DesktopId,人去付款,机器随即 Running——体验友好得多。价格可先 ecd describe-price 查。
# 给已有机器追加授权用户(保留原用户!) aliyun ecd modify-user-entitlement --api-version 2020-09-30 \ --region cn-chengdu --biz-region-id cn-chengdu \ --authorize-desktop-id ecd-xxxx --end-user-id xix5494 # 移除换成 --revoke-desktop-id # 重启(Akke 唯一治好过「机内 HTTPS 送达失效」的手段) aliyun ecd reboot-desktops --api-version 2020-09-30 \ --region cn-chengdu --biz-region-id cn-chengdu --desktop-id ecd-xxxx # 查便捷账号清单 aliyun eds-user describe-users --api-version 2021-03-08 --region cn-shanghai
Akke 的机器重启后需要有人连一次无影客户端触发用户登录,会话 1 才回来(企微靠注册表 Run 自启、常驻任务靠登录触发器延迟 90 s 拉起)。所以「重启」在这套架构里不是纯远程操作——这是依赖交互会话的固有代价。
全部来自 Akke 生产实践,按「症状 → 真因」组织——照症状查最快。
| 症状 | 真因与修法 | 类别 |
|---|---|---|
| 返回空列表 / 0 台机器 | --region 继承了 profile 默认区。两处 region 都显式写成机器所在区 | 参数 |
| 命令不存在 | CLI 默认挑了新版 API。显式 --api-version(用户管理=2021-03-08) | 参数 |
| Output 字段全空 | 没传 --include-output true。不是平台吞输出 | 参数 |
| ExitCode 恒为 0 | 读错层级。逐台的在 InvokeDesktops[],不在顶层 | 参数 |
| 命令成功但界面没反应 | 会话 0 看不到 GUI。改走 InteractiveToken 计划任务 | 会话 |
| SYSTEM 下 Python 起不来 | PATH 里没有 python、py 启动器在 SYSTEM 下失灵。写绝对路径 | 会话 |
| 脚本里的中文变问号 / 乱码 | GBK 与 UTF-8 冲突。显式切 [Console]::OutputEncoding | 编码 |
| emoji 让脚本崩 UnicodeEncodeError | 重定向到文件后 stdout 退回系统编码。设 PYTHONUTF8=1 | 编码 |
| 改完 .env 自检报缺 key | Set-Content -Encoding UTF8 加了 BOM 污染首行 key。用无 BOM 写法 | 编码 |
| 日志读回 0 行 | 文件被常驻进程独占。用 FileShare::ReadWrite 打开 | 编码 |
| 多层引号 SyntaxError | base64 → PowerShell → Python 多层转义绞杀。改写 .ps1 文件,别用 -c 单行 | 编码 |
| HTTPS 请求全超时但 TCP 通 | 按 SNI 定向阻断。境外端点统一走自建中转 | 网络 |
| pip install 超时 | 挂国内 PyPI 镜像 -i https://mirrors.aliyun.com/pypi/simple/ | 网络 |
| 下载脚本失败 / 半截 | GitHub 系全被墙。gzip+base64 分块经 RunCommand 灌进去 + MD5 校验 | 网络 |
| schtasks /run rc=0 但进程没起 | 日志句柄未释放,追加重定向建不起来。先探测可写再拉起 | 进程 |
| 加了个用户,原用户没了 | 用错 modify-entitlement(整体覆盖)。要用 modify-user-entitlement | 账号 |
| 创建机器 400 disk category | attachment 必须同时写 System 与 Data 两个盘类型且相同 | 开通 |
| 创建返回 400 余额不足、无 DesktopId | 用 --auto-pay false 预分配后人工付款;RAM 还需账单权限 | 开通 |
RunCommand 遥控真机是一条有真实成本的路:机器月费、GUI 自动化的脆弱性、看不见机器时的诊断难度。判断标准很简单——有没有接口可用。
目标平台没有可用接口、只有客户端能干(企微代回、抖音私信);机器数量已经多到人工连桌面成为瓶颈;操作可脚本化且需要留痕、可复现、可批量。
有官方 API 就走 API,别为了「像人操作」去养一台 Windows;一次性的、只跑一遍的操作,人连桌面点两下更快;需要毫秒级稳定性的链路——GUI 自动化做不到。
① 只信硬事实。「命令成功」不是证据,带主机名和当下时间戳的回执、数据库里真实落的行、进程存在性才是。
② 通道先自证。用任何日志 / 字段做回执前,先跑一个必然产生信号的动作确认它会响。
③ 危险操作先看清。覆盖 .env、替换脚本、改授权前先读原文并留备份——机器看不见,误伤没有撤销键。