结论 ① · 推翻Output 不是被吞了
真因是请求没带 IncludeOutput=true。官方文档写得清清楚楚:不传这个参数就不返回该字段。我们把「参数没传」读成了「平台有 bug」,然后为此发明了一整套 ntfy 中转方案。
机器在成都,人在杭州,中间只有一条异步远程命令 API。这篇讲清楚:这条链路怎么搭、每一段能干什么、以及你以为坏掉的地方,多半是自己参数按错了。
// 面向工程与运维 · 全部结论标注验证方式 · 数据截至 2026-07-25
这不是「云上跑个脚本」的故事。企业微信和抖音都没有可用的对外发消息接口——前者的客服 API 卡在备案,后者对机房 IP 风控极严。于是唯一能把消息真发出去的办法,是在一台登录着真实账号的 Windows 上,模拟人去点鼠标、敲键盘。
需要 GUI,就需要一台常开的 Windows;需要常开的 Windows,就不能靠人天天远程桌面连上去伺候。要么雇一个人盯着,要么让 Claude 去盯着——这条链路存在的全部理由。
企业微信 · 抖音 PC 客户端
一个 Python 常驻循环:截屏 → 视觉模型读出新消息 → 调后端大脑生成回复 → 定位输入框打字 → 点发送。全程没有一次 API 调用碰到企微或抖音的官方接口。
EDSP → EDS Enterprise
个人版无影只能人坐在客户端前面操作。企业版多出来的关键能力是一套面向管理员的 OpenAPI——其中就包含云助手的远程命令。没有它,一切自动化免谈。
本机终端 · 不在云电脑上
Claude 跑在 Mac 的终端里,不在云电脑内部。它拿着阿里云的 RAM 凭据,通过 CLI 把 PowerShell 脚本投进去、把结果捞回来——本质是一个会写脚本、会读回执、会自己纠错的运维。
本文讲的是控制通道——怎么让 Claude 可靠地在远端执行、确认、纠错。至于云电脑上那个循环本身怎么读屏、怎么生成话术,是另一条线的事。
从你在终端里说一句话,到云电脑上真的有个窗口被点了一下,中间要穿过七层。每一层都有它自己的失败模式,也各有各的证据。
aliyun ecd run-command 命令行
InvokeId,不等执行结果
nt authority\system 身份在 会话 0 里跑。这里看不见任何桌面——装依赖、写文件、改注册表可以,点按钮不行
InteractiveToken 计划任务,让它在已登录的会话 1 桌面里启动 Python
DescribeInvocations 回捞;业务是否真的发生,另查数据库落库记录(下详)
// 关键认知:会话 0 和会话 1 是两个世界。远程命令只能落在会话 0,GUI 只存在于会话 1,中间那道计划任务跳板是整条链路的枢纽。
整套控制只依赖两个 OpenAPI:一个把脚本投进去,一个把结果捞回来。下表是官方文档口径,右列标注我们实际踩到的对应后果。
| 参数 / 字段 | 官方口径 | 实际影响 |
|---|---|---|
| Type | RunPowerShellScript / RunBatScript 两种 | 只有这两种,没有直接跑 Python 的类型——想跑 py 得用 PowerShell 包一层 |
| ContentEncoding | Base64 或 PlainText(默认后者) | 中文脚本一律走 Base64,否则编码问题防不胜防 |
| 命令体大小 | base64 编码后不超过 16 KB | 硬墙。88 KB 的脚本没法一次投进去,必须压缩+分块 |
| Timeout | 默认 300 秒 | pip 装依赖常年顶线,必须挂国内镜像源提速 |
| DesktopId | 数组,单次 1 ~ 50 台 | 多台批量部署可一次下发,但结果要逐台看 |
| 返回值 | 异步,立刻返回 InvokeId | 发出去 ≠ 跑完了,必须轮询查状态 |
| InvocationStatus | Pending / Running / Success / Failed / Stopping / Stopped / PartialFailed | 多台下发时 PartialFailed 是常态,别只看整体状态 |
| ExitCode | 整型,逐台返回在 InvokeDesktops[] 里 | 忠实反映脚本里的 exit N(见下一节实测) |
| Output | 仅当请求带 IncludeOutput=true 才返回 | 最大的坑。不传这个参数,输出永远是空的——我们为此绕了半年远路 |
| Output 截断 | 超过 24 KB 截断,丢弃字符数记在 Dropped | 长日志要么自己截尾,要么写文件再分段捞 |
| 执行记录留存 | 可查近 2 周,最多 10 万条 | 事后复盘有窗口期,别指望翻上个月的执行史 |
# ① 把脚本 base64 后投进去 —— 注意 region 要写两遍,机器在哪个区就写哪个 B64=$(printf 'Write-Output "PROBE-OK $(hostname)"\nexit 7\n' | base64) 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-xxxxxxxx --timeout 60 # → {"InvokeId": "t-cd06rye9ua051q8"} # ② 捞结果 —— --include-output true 是关键,不传它 Output 永远为空 aliyun ecd describe-invocations --api-version 2020-09-30 \ --region cn-chengdu --biz-region-id cn-chengdu \ --invoke-id t-cd06rye9ua051q8 --include-output true # → InvocationStatus: "Failed" ExitCode: 7 ErrorCode: "ExitCodeNonzero" # → Output: "UFJPQkUtT0sg..." (base64,解出来是 PROBE-OK <主机名>)
--region 和 --biz-region-id 两处都要写机器所在区。只改一处,接口会安静地返回「0 台机器」,看起来像机器不存在——这是新手第一天必踩的坑。
Windows 从 Vista 起就把服务和用户桌面隔离在不同会话里:服务跑在会话 0,第一个登录的人在会话 1。远程命令属于服务那一侧——它有最高权限,却连一个像素都截不到。
.env破法是让会话 0 注册一个登录类型为 InteractiveToken 的计划任务(schtasks /create /xml,指定 <UserId>机器名\用户名</UserId>),它会用该用户的交互令牌在已登录的会话 1 桌面里启动进程——免密码、有 GUI、pyautogui 全套可用。前提是那台机器确实有人登录着(query user 看到 console 会话在运行)。
跳板里启动 GUI 脚本要用 pythonw.exe——它不开控制台窗口。用 python.exe 会弹一个黑窗口盖在企业微信上,紧接着截屏读到的就是那个黑窗口,整条视觉链当场失效。
首次扫码登录。企业微信、抖音的账号登录必须有人拿手机扫一次。登录态建立之后,后面所有事都能自动化——所以一台机器的「开荒」是人工的,「常驻运维」是全自动的。
这一节是整篇最值得读的部分——不是因为技巧,而是因为它是一次把自己的结论推翻的记录。写这页时顺手复测,结果和团队记忆里的三条「已知死路」全对不上。
写在内部记忆里、被反复引用了三天的三条结论:① 回执 Output 恒为空,「云助手吞了 stdout」;② ExitCode 恒为 0,脚本里写 exit 3 也没用,不能用退出码编码任何诊断结果;③ 这台机器的远程命令执行层已死,agent 只是假装成功、根本不执行脚本。
# 探针脚本:输出一行含主机名与当前时刻的文本,然后故意非零退出 Write-Output "PROBE-OK $(hostname) $(Get-Date -Format o)" exit 7 # 回捞(带 --include-output true): InvocationStatus : Failed ExitCode : 7 ← 脚本写 7,回来就是 7 ErrorCode : ExitCodeNonzero Output : UFJPQkUtT0sgNjhxZjVyeGZjcHZ4cmwyIDIwMjYtMDctMjVUMTI6NTA6MjAu... ↓ base64 解码 PROBE-OK 68qf5rxfcpvxrl2 2026-07-25T12:50:20.1608417+08:00 # 第二台(记忆里被判「执行层已死」的那台),改成 exit 3: InvocationStatus : Failed ExitCode : 3 Output (解码) : PROBE-OK fbmvz3yvpvbiog0 2026-07-25T12:50:38.6871888+08:00
真因是请求没带 IncludeOutput=true。官方文档写得清清楚楚:不传这个参数就不返回该字段。我们把「参数没传」读成了「平台有 bug」,然后为此发明了一整套 ntfy 中转方案。
exit 7 回 7,exit 3 回 3,并附带 ErrorCode: ExitCodeNonzero。逐台的退出码在 InvokeDesktops[] 数组里,不在顶层——大概率当初读错了层级。
两台机器都返回了真实主机名和当前秒级时间戳——不可能是缓存或回放。当初那晚的「假装成功」是网络故障期的一次性现象,重启后已愈,不该被写成这台机器的常态属性。
| 读回通道 | 说明 | 该信到什么程度 |
|---|---|---|
| 数据库落库记录 | 业务真的发生了才会有行——回复入库、发送状态、心跳计数 | ■ 硬事实。任何结论以它为准 |
| DescribeInvocations (带 IncludeOutput) | 脚本 stdout + 逐台 ExitCode + 错误码 | ■ 可信,实测无误。注意 24 KB 截断 |
| 脚本主动 POST 出来 | 让云电脑把结果推到消息服务或后端接口 | ■ 可信但依赖出口网络,网络一抖就静默失联 |
| 人工远程桌面截图 | 真人连上去看一眼 | ■ 绝对可靠,但不可自动化,只作最后兜底 |
不是「哪个字段不能信」,而是——验证探针本身要先过对照组校准。当时确实做了对照实验(写一个必然失败的脚本,看它是否报失败),但探针自己也在故障链路里,于是得到一串误导性的「成功」,进而推出「回放缓存」「平台吞输出」等一连串错误根因。信道失真时,用失真信道做的所有诊断都要作废重来,而不是在上面继续叠推理。
要更新的主脚本 88 KB,单条命令上限 16 KB,而这台机器的出口拉不到 GitHub、拉不到常见 paste 服务——外部中转全断。剩下唯一的路:把文件本身当命令内容送进去。
// 每块留足余量给 PowerShell 包装代码本身 → 43 KB ÷ 9 KB ≈ 5 块,一次成。
git show origin/main:<file> | gzip | base64 → split -b 9000
Add-Content -NoNewline 往同一个 .gz.b64 文件追加
[Convert]::FromBase64String + GZipStream 解压出原文件
md5 -q 逐字节比对,对上了才覆盖线上文件,然后重启进程
它会在文件开头写一个 UTF-8 BOM。如果 .env 第一行恰好是个变量名,BOM 就把变量名污染成 ANTHROPIC_API_KEY,程序读不到、自检失败、进程死循环重启——而值本身完好无损,长度都没变,排查时极难看出来。同一条命令下发到两台机器,一台首行是注释就没事,另一台首行是变量名就炸,靠运气决定命运。正确写法是显式指定不带 BOM 的编码器。
云电脑的出口是国内直连、无代理。一批境外域名会出现一种很有迷惑性的症状:端口探测显示握手成功,但真正发 HTTPS 请求永远超时。这不是断网,是按域名的定向阻断。
国内站点 200、自家海外后端 200、唯独某几个域名全线超时;而端口探测(TCP 层)却报成功。判据是:TCP 握手成功、TLS ClientHello 发出后被重置 — 典型的按 SNI 阻断,而不是链路不通。
在已经能连通的自家海外服务上加一个 OpenAI 兼容的透传端点,把云电脑的调用改指过去:云电脑 → 自家东京节点 → 目标服务。凭据原样透传、中转方不持有,上游写死单一目标不是开放代理。客户端零改动,只换一个 base URL。
同一批症状我们经历过两次:第一次是真的定向域名阻断,修法是中转;第二次是本机级网络故障,表现几乎一样,修法是重启机器——重启后当场恢复。先分清「某个域名不通」和「所有出站都不通」,前者做中转,后者重启。用一个已知可达的自家端点当对照组,一次探测就能分开。
按「症状 → 真因 → 修法」排列。第一次接手这条链路的人,把这张表看完能省掉大约两周。
| 症状 | 真因 | 修法 |
|---|---|---|
| 接口返回 0 台机器 | region 默认走了凭据配置里的区,看不到别的区 | --region 与 --biz-region-id 都写机器所在区 |
| Output 永远是空 | 请求没带 IncludeOutput=true | 加上该参数;结果是 base64,记得解码 |
| 远程命令跑了,弹窗/点击全无效 | 脚本在会话 0,看不见桌面 | GUI 步骤走 InteractiveToken 计划任务跳会话 1 |
| GUI 脚本起来了但截屏是黑窗 | 用了 python.exe,控制台窗口盖住目标 | 改用 pythonw.exe |
| 内嵌 Python 一行命令报语法错 | base64 → PowerShell → Python 多层引号绞杀 | 写成 .ps1 文件执行;能用系统命令的别嵌 Python |
| pip 装依赖顶到超时 | 默认源跨境慢 | 挂国内镜像源,必要时调高 Timeout |
| 脚本起不来,说找不到 python | 装 Python 时没勾「加入 PATH」 | 用启动器 py(永在 PATH)或写绝对路径;会话 0 下环境变量还有缓存,改了要重启才生效 |
| 日志重定向到文件就崩 | 日志里有 emoji,写文件时退回系统编码 | 启动前置 PYTHONUTF8=1 |
| 中文日志回传全变问号 | PowerShell 默认按系统代码页,Python 输出是 UTF-8 | 管道前显式转码,或整个会话切 UTF-8 |
| 改完 .env 一台好一台炸 | Set-Content -Encoding UTF8 写了 BOM,污染首行变量名 | 用不带 BOM 的编码器;改前备份,改后必看自检行 |
| 常驻循环每轮全跳过 | 分辨率严格相等检查,远程桌面重连会差几像素 | 改成容忍 ±20px 的范围检查 |
| 新机器套老机器坐标全点偏 | 客户端版本不同导致布局变化,分辨率相同也不行 | 每台重新标定坐标,别复制粘贴 |
| 视觉模型把示例卡片读成真实消息 | 截图裁剪只切了左边界,右侧侧栏的演示内容进了画面 | 补一个右边界裁剪参数,把无关面板切掉 |
| 三台机器同时搜不到人 | 从已装客户端的机器做镜像克隆,设备指纹被一起复制 | 基础镜像只装依赖不装客户端,克隆后逐台安装登录 |
技术上这套链路并不复杂,难的是盲操作——Claude 看不到那块屏幕,只能靠回执推断发生了什么。要让它可靠,靠的不是更聪明的提示词,而是几条硬纪律。
任何用来判断远端状态的信道,先跑一个「必定失败」的对照脚本。它要是也报成功,这条信道当场作废,别在上面做任何推理。
「命令执行成功」和「消息真的发出去了」是两件事。前者问接口,后者只能查库里有没有那一行。所有对外声明以后者为准。
远端没有撤销键。覆盖任何文件前先复制一份带日期的备份;替换后必须读回自检行确认服务真的起来了,而不是看到「命令返回成功」就宣布完成。
这条链路的每个参数、每个坑,都落成一份团队共享的记忆文件。新会话开工先读它——省下的不是打字,是重新踩一遍坑的两周。
本页第 05 节就是例子:三条被反复引用的「已知死路」,复测下来两条是自己参数按错、一条是一次性故障。写进文档的结论也会过期,定期复测比继续引用更重要。
常驻循环会不停把目标窗口拉到前台点击。真人这时连上远程桌面操作,两边抢焦点,双输。要人工介入,先把循环停掉。
Claude 负责组装命令、解析回执、比对校验和、发现矛盾就停下来复测;人负责扫码登录、拍板参数、以及在信道全断时连上去看一眼。分工的界线不是「谁更聪明」,而是「这件事有没有确定性的答案」——有的交给代码,没有的才交给人。
| 项 | 现状 | 口径 |
|---|---|---|
| 机器 | 2 台,成都可用区,均 6 核 12G、80G 系统盘、Windows 11 | eds.enterprise_office.6c12g |
| 单机月成本 | 约 ¥249/月(办公型主机 + 系统盘) | 包年包月 |
| 控制通道 | 远程命令 + 计划任务跳板,本机 Claude Code 驱动 | ECD OpenAPI 2020-09-30 |
| 人工介入点 | 首次扫码登录、坐标标定、信道全断时的兜底截图 | 其余全自动 |
| 开荒耗时 | 单台从空白到真发,约半天(含依赖、脚本、坐标、验证) | 复制到第二台快得多 |