实战教程 · 以 Akke 生产机队为例

用一行命令
遥控四台真机

阿里云 CLI + 无影云电脑 RunCommand:不连远程桌面、不用人坐在机器前,从 Mac 终端把脚本送进 Windows,再把结果读回来。

// 全文命令均在 Akke 生产云电脑上实跑验证 · 回执为真实输出,非示意

ALIYUN CLI 3.3.22 ECD API 2020-09-30 RAM · ECD FULL ACCESS 4 台 · 成都 ×2 / 杭州 ×2 RUNPOWERSHELLSCRIPT SESSION 0 / SESSION 1
SCROLL / 向下滚动
01
Why A CLI At All

有些活只能在真机上干

Akke 有一类工作没法在服务器上跑:企业微信自动代回抖音私信 / 反向评论。这些平台没有可用的对外接口,只能开一台真 Windows、装真客户端、用模拟人操作的方式干活。于是就有了云电脑机队——而机队一旦超过一台,「人连远程桌面手动操作」立刻变成瓶颈。

GUI 层真机才有的能力

企微 / 抖音客户端跑在真实 Windows 桌面上,靠截屏读、模拟点击写。没有接口可调,也就没法搬到无头容器里。

控制层CLI = 唯一无人化入口

aliyun ecd run-command 让脚本从 Mac 直达机器内部:装依赖、改配置、重启进程、读日志,全程不开远程桌面。

人在环只剩不可替代的那几步

首次企微扫码登录、拍板放开真发——这些留给人。其余的部署、校准、验活、恢复都自动化掉。

Prerequisite

必须是企业版无影(ECD / API 2020-09-30)才有 RunCommand。个人版没有云助手通道,只能靠人连客户端——这也是 Akke 当初从个人版升企业版的唯一理由。

02
Install & Credentials

装 CLI,配一把只能干这件事的钥匙

CLI 本身没什么可说的;凭据的粒度才是要想清楚的地方。不要用主账号 AccessKey——建一个专职 RAM 用户,只授它需要的那一类权限。

Terminal · macOS
# 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
实测输出 · 2026-07-28
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,而机器一台都不在深圳。这正是下一节第一个坑的来源。

权限:ECD 与账单是两件事

Akke 的 RAM 用户 akke-wuying-botAliyunECDFullAccess——足够查机器、发命令、重启。但开新机器还需要 AliyunBSSOpenAPIFullAccess(费用权限),否则子账号既看不到余额也改不了支付方式,会卡在「机器好像没创」。

钥匙的爆炸半径

ECDFullAccessrun-command(任意代码执行)delete-desktops。它等价于机队 root,别塞进 CI 的公共 secret、别写进会被打包的 .env;轮换要当成换生产密码来对待。

03
The Three Axes

命令集 × API 版本 × 区域

无影 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 它就回来了。本文所有命令都显式写版本,一个都别省。

坑 01

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
正确姿势 · 两处 region 都写
aliyun ecd describe-desktops \
  --api-version 2020-09-30 \
  --region cn-chengdu --biz-region-id cn-chengdu \
  | jq -r '.Desktops[] | [.DesktopId,.DesktopName,.DesktopStatus] | @tsv'
04
Akke Fleet · Live Inventory

四台机器,两个区

下表是把上面那条命令在成都、杭州各跑一次的真实返回(2026-07-28)。规格统一 eds.enterprise_office.6c12g + 80 G cloud_auto,约 ¥249/月一台。

用途 / 人设区域与授权账号DesktopId
小顾成都 · 企微自动代回 · 授权 shawsnkecd-5qfbmvz3yvpvbiog0
夏伟成都 · 企微自动代回 · 授权 shawsnk / xix5494ecd-eq68qf5rxfcpvxrl2
野荞杭州 · 抖音触达 · 授权 xix5494 / shawsnkecd-6mc7dgh8kldv19baa
夏夏杭州 · 抖音触达 · 授权 yuanyurou545 / shawsnkecd-1yrf84368u89vysox
认机器别认名字

无影客户端里显示的卡片名(YDYX-Xiawei 之类)是客户端本地别名,控制台和 API 里查不到——曾按客户端名全区域扫,扫不到就以为「机器在另一个阿里云账号」。可靠做法:认 DesktopId,或让脚本回报机内的人设标识(Akke 读 C:\akke-wecom\.envAKKE_WECOM_PERSONA)。

坑 02

给机器加人用 modify-user-entitlement,绝不要用 modify-entitlement后者语义是「把该机器的授权用户整体设成你传的这批」——它会先删掉原有全部用户,手滑就把 shawsnk 踢掉、连带杀掉跑在它交互会话里的自动化。而且单会话机上新用户一连上就会强制注销原会话,想共用同一套自动化只能共用同一个账号,而不是加用户。

05
RunCommand End-To-End

一条命令的完整旅程

RunCommand 是异步的:发命令拿到一个 InvokeId,再用它去查结果。两步分开是刻意设计——脚本可以跑几分钟,而 HTTP 请求早就返回了。

MAC
编码
PowerShell 脚本 → base64--content-encoding Base64)。命令体上限 16 KB(编码后),大脚本得先落盘再执行
API
下发
ecd run-command 交给云助手,立刻返回 InvokeId。一次最多 50 台,默认超时 300 s(Akke 按脚本长短显式给 60~400 s)
机内
执行
机器上的云助手 agent 以 nt authority\system 身份在会话 0跑脚本——看不到任何 GUI(下一节详述)
MAC
读回
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'
本页写作时实跑的回执 · InvokeId t-cd06s9k9wfvrqww
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,为绕它发明了一整套第三方消息中转方案;后来发现只是少传了一个参数。读回不通时,先把文档里的参数列表逐个对一遍,再怀疑平台。

06
Session 0 vs Session 1

能执行,不代表看得见

这是 RunCommand 最硬的一条约束,也是所有「命令成功了但界面毫无反应」的根源:脚本跑在会话 0,桌面在会话 1。Windows 从设计上就把服务与交互桌面隔开了。

SESSION 0 · SYSTEMRunCommand 直接跑的地方

能做:装 Python / 依赖、下载与替换脚本、写 .env、查进程、读日志、静默装软件、重启常驻任务。

不能做:截屏、点击、打字——看不到桌面。这里 pyautogui.size() 报的是无显示器的兜底值(如 1024×768),不是真分辨率。

SESSION 1 · 交互桌面GUI 自动化真正的舞台

企微 / 抖音客户端、模拟点击、截屏读屏都在这里。前提是有人登录着query user 显示 console 会话运行中)。

Akke 的机器上 shawsnk 常驻登录,因此会话 1 一直存在——这是整条自动化链的隐含依赖。

跨越边界的办法

会话 0 虽然自己碰不到 GUI,但它可以注册一个 LogonType=InteractiveToken 的计划任务schtasks /create /xml,指定 机器名\用户),该任务就会在登录着的会话 1 桌面里运行 → 截屏、点击、打字全部可用,且免密码。Akke 用它把坐标标定、空跑验证、常驻 loop 的启停全部搬到了 Mac 端遥控,只剩首次扫码登录必须真人。跑 GUI 脚本用 pythonw.exe,免黑窗盖住被操作的窗口。

坑 03

会话 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 进程看不到,要重启才刷。

07
Trusting The Feedback Channel

我们判死过三条通道,两条是自己按错了

遥控一台看不见的机器,回执通道的可信度就是全部。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[] 里,不在顶层

TCP 通了,HTTPS 不通

机器出口是国内阿里云直连、无代理。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 秒,稀疏事件必然抓不到。

纪律

否定性证据要先证明通道在正例下会响。拿某条日志 / 字段当回执前,先用一个「必然产生该信号」的动作跑一遍、确认看得见;否则「收不到信号」和「动作没发生」无法区分,好通道会被判死。
对照组管假成功,通道存在性管假失败——两面缺一不可。

坑 04

重启常驻进程时 schtasks /run 返回 0 也可能什么都没起来。Akke 的任务动作是 cmd /c "... >> _loop.log 2>&1";刚被杀掉的上一个实例还攥着日志句柄 4~8 秒,追加重定向建不起来 → cmd 立刻 exit 1,而错误信息本身要走那个失败的重定向,所以哪儿都看不到。修法:拉起前用同一个操作探测日志可写(echo. >> _loop.log,rc=0 才继续,最多等 60 s),验收一律看进程存在性和业务指标。这是 Windows 通用坑,不限无影。

08
File Transfer

怎么把 88 KB 塞进 16 KB 的管子

Akke 的企微 loop 主脚本已经 88 KB,而命令体上限 16 KB;同时这台机器上 GitHub raw / gist / jsDelivr 全部被墙,公共粘贴板对 >85 KB 直接 500。外部中转全死,只剩把文件顺着 RunCommand 本身灌进去

MAC
压缩
git show origin/main:<file> | gzip | base64 → 88 KB 压到 43 KB
MAC
切块
split -b 90005 块,每块塞得进 16 KB 命令体
机内
拼接
逐块 Add-Content -NoNewline 追加到 _loop.gz.b64
机内
解压校验
FromBase64StringGZipStream 解出 → Get-FileHash MD5 与 Mac 侧 md5 -q 对齐,匹配才替换旧文件

// MD5 对齐这一步不能省。分块传输任何一块丢了都会得到一个「看起来完整」的坏文件;Akke 曾因为部署链接过期,把缺了几周护栏的旧版脚本部上生产,症状是「代码明明修了、线上还在犯老错」。先校验,再替换,留 .bak

坑 05

.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 存不存在,读原始字节验首三字节

坑 06

中文日志会在 PowerShell → jq 的管道里烂掉。中文系统 PowerShell 默认 GBK(936),Python 输出是 UTF-8,读回来是 鈿狅笍 鍒嗚鲸鐜囧彉浜 这种乱码(原文是「⚠️ 分辨率变了」)。读之前先 [Console]::OutputEncoding=UTF8Get-Content -Encoding UTF8
还有一个更阴的:被常驻进程独占的日志文件,[IO.File]::ReadAllLines 会静默返回 0 行——看起来像「日志是空的」,其实是没用共享读模式打开(要 FileStream(..., FileShare::ReadWrite))。

09
Provisioning & Accounts

从零开一台,到把人加上去

机队扩容也全走 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'

坑 07 · 盘类型必须成对写

--desktop-attachment 里只写 SystemDiskCategory 会 400:User disk category is not same as root disk。阿里云要求数据盘与系统盘类型一致,两个字段都要写、且值相同(即便 DataDiskSize 是 0)。

坑 08 · --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 拉起)。所以「重启」在这套架构里不是纯远程操作——这是依赖交互会话的固有代价

10
Trap Cheat Sheet

坑位速查表

全部来自 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 自检报缺 keySet-Content -Encoding UTF8 加了 BOM 污染首行 key。用无 BOM 写法编码
日志读回 0 行文件被常驻进程独占。用 FileShare::ReadWrite 打开编码
多层引号 SyntaxErrorbase64 → 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 categoryattachment 必须同时写 System 与 Data 两个盘类型且相同开通
创建返回 400 余额不足、无 DesktopId--auto-pay false 预分配后人工付款;RAM 还需账单权限开通
11
Boundaries & Discipline

什么时候值得这么干

RunCommand 遥控真机是一条有真实成本的路:机器月费、GUI 自动化的脆弱性、看不见机器时的诊断难度。判断标准很简单——有没有接口可用

值得

目标平台没有可用接口、只有客户端能干(企微代回、抖音私信);机器数量已经多到人工连桌面成为瓶颈;操作可脚本化且需要留痕、可复现、可批量。

不值得

有官方 API 就走 API,别为了「像人操作」去养一台 Windows;一次性的、只跑一遍的操作,人连桌面点两下更快;需要毫秒级稳定性的链路——GUI 自动化做不到。

让 Claude 干这活需要的三条纪律

① 只信硬事实。「命令成功」不是证据,带主机名和当下时间戳的回执、数据库里真实落的行、进程存在性才是。
② 通道先自证。用任何日志 / 字段做回执前,先跑一个必然产生信号的动作确认它会响。
③ 危险操作先看清。覆盖 .env、替换脚本、改授权前先读原文并留备份——机器看不见,误伤没有撤销键。

这套控制链在 Akke 的落地形态与更上层的架构叙事,另有一页专讲:upio.ai/learn/claude-code-cloud-pc——本页是它的命令行工具手册面,那页是系统设计面