云电脑机队治理 · 企业版 × 个人版混编

八台机器
跑的是同一份程序吗

今天的答案是:没人知道。八台在册云电脑里,中央能查到版本号的是 零台。这份方案讲怎么把「不知道」变成「随时能查、不一致就报警」。

// 现状数据经 2026-07-25 当日实测核对 · 方案分四层、可分阶段落

3 台企业版 · 可远程下发 5 台个人版 · 只能自己拉 25 个脚本 · 2 条投递链 MANIFEST 驱动 版本自报 + 漂移告警
SCROLL / 向下滚动
01
Inventory · What We Actually Have

先把家底盘清楚

谈治理之前先回答一个基本问题:我们到底有几台云电脑。这次逐台核对下来,答案和团队记忆里的不一样——多出一台没人记录过的企业版机器

0
在册云电脑总数(3 企业版 + 5 个人版通道)
0
需要分发到机器上的脚本文件
0
现有更新工具永远拉不到的文件
0
中央能查到「在跑哪个版本」的机器
机器 / 通道身份与用途远程可控性核对结果
企业版 EDS · eds.enterprise_office.6c12g · 有 RunCommand
ecd-5qfb…biog0成都 · 企微人设「小顾」· 常驻代回循环■ 可远程下发Running / 有登录会话
ecd-eq68…vxrl2成都 · 企微人设「夏伟」· 常驻代回循环■ 可远程下发Running / 有登录会话
ecd-1yrf…vysox杭州 · 归属账号 yuanyurou545 · 用途未登记■ 可远程下发■ 无人记录,07-22 开通、0 会话、当前离线
个人版 EDSP · 运营各自持有 · 无 RunCommand
小文(野荞)抖音 DM + 潜在触达 route-B■ 只能机器自己拉心跳 25.0 小时前
文哥抖音 DM 通道■ 只能机器自己拉心跳 26.8 小时前
有大有小抖音 DM 通道(独立品牌号)■ 只能机器自己拉心跳 26.8 小时前
饭粒(一筑)抖音 DM + 反向评论■ 只能机器自己拉心跳 43 分钟前
零星(夏夏)抖音 DM + 反向评论■ 只能机器自己拉心跳 43 分钟前
两处要先澄清

①杭州那台企业版没有出现在任何运维文档里,需要确认是谁开的、干什么用、还要不要留——它每月在计费。②心跳快照:派单侧判在线的阈值是 5 分钟,取数那一刻五台个人版全部在窗口外。这只是一个时点快照,不等于故障,但正说明「靠心跳只能看出活没活,看不出跑的是哪版」。

02
Two Delivery Chains · Where Drift Enters

一套代码,两条投递链

企业版和个人版今天走的是两条完全不同的分发链:源码仓不同、触发方式不同、校验方式不同。漂移不是某一环坏了,而是两条链的最后一米都靠人

企业版 · PUSH◉ 中央推

RunCommand 直投

独立仓 → 本机压缩分块 → 远程写盘

本机把脚本 gzip + base64 切块,逐块经远程命令写进机器,解压后 MD5 逐字节校验,对上才替换、然后重启计划任务。

✅ 有校验 · ✅ 中央可发起 · ❌ 全靠手动跑
个人版 · PULL◈ 机器拉

公共镜像 + update.bat

主仓 → GitHub Actions → 公共镜像 → 机器 curl

合并到主分支后自动同步到公共镜像(免 token 的 raw 地址),机器上运营双击 update.bat 把文件拉下来。

✅ 自动同步到镜像 · ❌ 无校验 · ❌ 靠人双击

把这条链逐段量一遍,问题出在哪一段就很清楚了:

源码 → 公共镜像25/25
镜像 → update.bat 覆盖11/25
机器版本中央可见0/8

// 自动化的那一段(GitHub Actions → 镜像)是好的,25 个文件逐字节一致;坏的是它之后的每一段。

实测:更新工具漏掉 14 个文件,包括它自己

机器上双击的 update.bat 只下载 11 个文件,而镜像里有 25 个。拉不到的 14 个包括:整个 route-B 潜在触达脚本族、开机自愈看门狗、以及——update.bat 自己。更新器不在自己的更新列表里,等于单向棘轮:机器上那份停在它被摆渡下来那天,之后仓库里新加的文件永远拉不到。而运营跑完看到「all downloaded」,会以为已是最新。

比报错更危险的是「静默陈旧」

这类故障不会红,只会安静地旧。真实案例:某台机器的发送脚本停在几个月前的截断版本,导入链一崩把整条「回复捕获」腿废掉——但发送腿照常工作,日志滚得快,很久没人发现。「跑了更新」和「真的更新了」之间隔着一整条没人验的链。

03
Root Cause

版本这个事实,从没离开过那台机器

所有漂移问题往下挖一层,都会撞到同一堵墙:中央没有任何渠道知道某台机器此刻在跑什么版本。不是查起来麻烦,是这个数据根本没被传出来过

agent 里的版本号 —— 定义了,打印了,然后就没了
# wuying_poll_agent.py 第 53 行
AGENT_VERSION = '2026-07-03+reply-priority'

# …全文再出现一次,就是打到本机控制台:
print(f'=== wuying_poll_agent  v={AGENT_VERSION}  account=…')

# 全仓搜索结果:AGENT_VERSION 共 2 处命中 —— 定义 + 打印。
# 它从未被写进数据库、从未随心跳上报、从未离开这台机器。

现有心跳只证明「活着」

机器每 60 秒调一次派单接口,服务端顺手把 accounts.last_poll_at 盖成当前时间。这是一个纯粹的存活信号——它能告诉你进程还在,完全无法告诉你进程在跑哪一版代码。

于是只能靠人肉考古

想知道某台在跑哪版,今天的办法是:连上远程桌面 → 翻文件字节数 → 数关键函数出现次数 → 和仓库版本对字符串。八台机器全查一遍要小半天,而且下次还得再来一遍。

Root Cause

问题不在「怎么把新版推下去」——推的办法有的是。问题在推完之后没有任何回执能证明它生效了没有可观测,就没有治理:你无法管理一个你看不见的东西。所以整套方案的第一步不是改分发,而是让每台机器把自己的版本说出来

04
Design Principle

统一判据,差异化投递

企业版能被中央远程下发,个人版不能——这个能力差不可能靠流程抹平。所以不要强求「两种机器走同一条投递通道」,那是把简单问题做复杂。真正该统一的是另外三件事。

统一 ①同一份清单

「这一版由哪些文件、哪些哈希构成」——全机队共读一份 manifest,不管你是被推的还是自己拉的。

统一 ②同一种自报

每台机器用同样的字段、同样的节奏上报自己在跑什么版本、文件指纹是多少。企业版个人版一视同仁。

统一 ③同一条告警

期望版本与实际版本对不上,同一个定时任务、同一张红牌推到同一个群。不分机型。

差异化的只有一件事:怎么把文件送过去

企业版 = 推(中央一条命令下发到位,可确认);个人版 = 拉(机器定时自己去取,中央只能等它来报到)。两种投递方式最终汇合到同一个判据——上报的哈希是否等于 manifest 里的哈希。投递方式可以不同,验收标准必须相同。

推论:不要为了「统一」去全升企业版

升级并不能解决漂移——RunCommand 改变的只是投递方向,不改变验收缺失这个真问题。真按每台每月约 ¥249 算,五台个人版升上来一年多花约 ¥1.5 万,买到的是「能远程推」,而漂移照旧,因为没人在推完之后验。先把 L1/L2 做了,再按需要单独评估升级。

05
Layer 1 · Self-Report

让每台机器把版本说出来

投入最小、收益最大的一步,排在所有事情前面。机器本来就每 60 秒调一次派单接口,只要在这个已有的调用上多带两个字段,中央立刻从「全盲」变成「全见」——不需要新增任何一次网络请求。

改法 · 在既有心跳上捎带,不新增调用
# ① agent 侧:启动时算一次本地文件指纹(清单里每个文件的 sha256 取前 8 位后拼接再摘要)
BUNDLE_HASH = _fingerprint(MANIFEST_FILES)   # 例:'a3f91c2e'

# ② 心跳照旧每 60s 调,只是多带两个参数
claim_dispatch(
    p_account_id  = ACCOUNT_ID,
    p_limit       = CLAIM_LIMIT,
    p_agent_version = AGENT_VERSION,   # '2026-07-03+reply-priority'
    p_bundle_hash   = BUNDLE_HASH,     # 'a3f91c2e'
)

# ③ 服务端:本来就在这个函数体里盖 last_poll_at,顺手多盖两列
UPDATE accounts SET last_poll_at = now(),
                    agent_version = p_agent_version,
                    bundle_hash   = p_bundle_hash
WHERE id = p_account_id;
01

为什么要两个字段

版本号是人写的、会忘记改文件指纹是算出来的、不会撒谎。两个一起看:版本号相同但指纹不同 = 有人在机器上手改过文件,这恰恰是最难查的一类漂移。

02

企业版怎么接

企微常驻循环也有自己的心跳链路,同样在上报里加这两个字段即可。字段名与个人版完全一致,这样一张表、一个查询就能覆盖全机队,不用维护两套看板。

03

立刻能回答的问题

「哪几台还在旧版」「上周那个修复到底铺开了没」「这台的异常是不是因为版本旧」——全部变成一句 SQL,而不是连八次远程桌面。

刻意的取舍

不做「中央主动去查机器」(个人版根本没这个通道),只做机器主动上报。代价是离线机器的信息会陈旧——但陈旧的时间戳本身就是信息,配上心跳一起看就够了。能在两种机型上用同一种方式实现的方案,才值得做。

06
Layer 2 · Single Manifest

把三份手维护的名单合成一份

今天「一个文件要不要发到机器上」这件事,被记在三个互不相干的地方:同步流水线的触发路径、同步流水线的拷贝循环、以及机器上更新工具的下载列表。加一个新文件要同时改三处,漏改任何一处都不报错,只是静默失效

✕ 现在:三份名单

  • 流水线触发路径清单(25 项)
  • 流水线拷贝循环清单(25 项)
  • 更新工具下载清单(11 项
  • 漏改 = 静默失效,不报错
  • 更新工具不含自己 = 永远不更新

✅ 改成:一份 manifest

  • 仓库里一个 bundle.json
  • 版本号 + 文件清单 + 每文件 sha256
  • 流水线按它同步、并自动生成
  • 更新工具按它下载(含自己)
  • 远程下发也按它推

◈ 顺带治好的

  • 加文件只改一处,漏不了
  • 下载完能逐文件校验哈希
  • 更新器自举(先更新自己再更新别人)
  • 机器指纹可与 manifest 直接比对
  • 「装了什么」变成可版本化的事实
bundle.json · 由流水线自动生成,人不手写
{
  "bundle_version": "2026-07-25.1",
  "channel": "douyin-dm",
  "files": {
    "wuying_poll_agent.py":      { "sha256": "9f2c…", "bytes": 41230 },
    "douyin_dm_grounded.py":     { "sha256": "1ab7…", "bytes": 22549 },
    "update.bat":                { "sha256": "c40e…", "bytes":  3180 },
    … 其余 22 个,由 CI 遍历目录生成,不手写
  }
}

# 更新器逻辑变成 5 行,且再也不用维护名单:
#   1. 先拉 bundle.json          2. 按 files 键逐个下载
#   3. 逐个校验 sha256 对不上就重试   4. 全对才替换 + 重启
#   5. 把 bundle_version 写进本地状态文件 → 供心跳上报
关键细节:更新器必须自举

update.bat 要出现在 bundle.json 的 files 里,并且执行顺序是「先更新自己 → 用新的自己更新其余」。少了这一步,更新器就是单向棘轮:仓库里加的东西它永远看不见,而机器上的人以为自己在跑最新版。

07
Layer 3 · Push & Pull

企业版推,个人版拉,都验同一个哈希

有了清单和自报,投递就退化成一件小事。两条链保持各自形态,但结尾必须相同:校验哈希、写状态、上报、由中央确认。

共同起点
合并主干
代码合入主分支 → CI 遍历目录自动生成 bundle.json(版本号 + 全量哈希),同步到公共镜像
企业版
PUSH
一条命令按 manifest 下发到 N 台(单次最多 50 台)→ 远端解压校验 → 回执直接带回校验结果,中央当场知道成没成
个人版
PULL
把「运营双击」改成「计划任务定时跑」——已有的开机看门狗就是现成载体,每天固定时间自拉一次,人不参与
两条链
汇合
更新完写本地状态文件,下一次心跳把 bundle_version + 实际指纹 报上来
中央
裁决
定时比对期望版本 vs 上报版本,不一致即告警——这是唯一的验收口径,两种机型共用

// 注意重点不是「让个人版也能被推」,而是「把人从关键路径上拿掉」——双击是人的动作,定时任务不是。

个人版的现实约束要认

机器关机就更新不了、运营在用时不能随便重启进程、抖音登录态是运营私人的。所以个人版的拉取要做成「开机后延迟若干分钟 + 每日固定时段各一次」,并且更新前先检查有没有任务在跑,跑着就跳过等下一轮。宁可晚几小时铺开,不要打断正在发的会话。

08
Layer 4 · Alerting, Canary, Rollback

漂移要会自己叫,上线要能收回来

漂移告警让「旧」变成一件会响的事

定时任务比对期望与实际,命中任一条推红牌:版本落后于期望版本相同但指纹不同(有人手改过)、超过 N 天没上报过版本。前两条治漂移,第三条治「机器还在但更新链断了」。

灰度先上一台,看着再铺

期望版本不是全局一个值,而是按机器 pin。新版先 pin 到一台金丝雀机器,观察一个周期(心跳正常 + 业务量没塌),再把其余机器的 pin 推上去。

回滚把 pin 改回去就行

公共镜像按版本号保留最近若干个 bundle,回滚=把机器的期望 pin 指回旧版本号,下一轮自拉时自动退回。不需要人连上机器操作。

看板一屏看完八台

一张表:机器 / 归属 / 通道 / 期望版本 / 实际版本 / 指纹是否匹配 / 最后上报时间。绿=对齐,黄=落后,红=指纹不符。这张表就是「统一管理」这四个字的具体形态。

别再用「跑完没报错」当验收

这套体系里,唯一算数的验收信号是「机器上报的哈希 == manifest 里的哈希」。更新脚本返回成功、日志打印 OK、运营口头确认「我点过了」——都不算。这条纪律不立住,前面三层白做。

09
Config vs Code

代码必须一致,配置本来就该不同

「统一管理」有个常见误区是连配置一起统一。不行也不该——每台机器的屏幕坐标、账号身份、人设、限流额度天生不同。要做的是把两者分开,各用各的纪律

类别例子要求怎么保证
代码发送脚本、轮询 agent、看门狗全机队逐字节一致manifest 哈希比对 + 漂移告警
机器配置输入框/发送键坐标、屏幕裁剪比例、分辨率每台必然不同本机标定;禁止跨机复制(客户端版本一升坐标就变)
身份配置账号 ID、企微人设、抖音登录态每台唯一1:1 绑定;恢复备份前必须核对 ID,防止张冠李戴
策略配置日限、时限、单客户连发上限、工作时段应集中管理尽量收到数据库里由中央下发,别散在各机 .env
配置文件也要有自检,别再靠运气

真实事故:用带 BOM 的编码写 .env,把首行的变量名污染成不可见字符,程序读不到 → 进程死循环重启。同一条命令下发到两台,一台首行是注释就没事,另一台首行是变量名就炸。修法是把「必填项检查」写进启动自检并打印到日志首行,改完必须读回那一行确认,而不是看到进程起来了就走。

10
Rollout & Cost

怎么落,先做哪一步

四层不必一次做完。按「投入 ÷ 收益」排,第一阶段一天之内能出效果,而且它本身就是后面几步的验收工具。

阶段做什么立刻拿到什么粗估投入风险
P0盘清家底:杭州那台企业版确认用途或退订;补齐机器登记表不再为不知道的机器付费半天
P1版本自报:心跳带 agent_version + bundle_hash,落两列八台机器版本一屏可见,从全盲到全见1~2 天·纯增字段
P2单一 manifest:CI 生成 bundle.json,更新器改为按它下载并校验、且自举加文件不会再漏;下载有校验2~3 天·需逐台换更新器
P3漂移告警:定时比对期望 vs 实际,红牌推群「静默陈旧」变成会响的故障1 天
P4去掉人工双击:个人版改计划任务定时自拉;企业版按 manifest 批量推更新不再依赖运营记得点2~3 天·须避开发送时段
P5灰度与回滚:按机器 pin 期望版本,镜像保留历史 bundle敢发新版;出事一分钟退回2 天
P1 为什么必须排第一

因为后面每一步都要靠它验收。没有版本自报,你做完 manifest 也不知道有几台真的换上了;做完定时自拉也不知道它有没有在跑。先把尺子造出来,再去改被量的东西。

批量扩机时的一条红线(已踩过)

将来要复制更多机器时:绝不能把「已装并登录过客户端」的机器做成基础镜像去克隆。设备指纹会被一起复制,多台在平台眼里成了「同一台设备挂多个号」,触发风控后整批一起出问题,还可能连坐封号。正确做法是基础镜像只装依赖不装客户端,克隆后逐台安装、逐台登录。

一句话总括:今天的问题不是「更新推不下去」,而是推完之后没人能证明它生效了——八台机器里中央能查到版本的是零台。所以顺序必须是先让机器自报版本(造尺子),再把三份手维护名单收敛成一份 manifest(定标准),最后才谈投递方式。企业版推、个人版拉可以永远不同,但「上报的哈希等于清单里的哈希」这一条验收口径必须全机队统一。做完这三步,「统一管理」才从一句口号变成一张能看的表。