现有心跳只证明「活着」
机器每 60 秒调一次派单接口,服务端顺手把 accounts.last_poll_at 盖成当前时间。这是一个纯粹的存活信号——它能告诉你进程还在,完全无法告诉你进程在跑哪一版代码。
今天的答案是:没人知道。八台在册云电脑里,中央能查到版本号的是 零台。这份方案讲怎么把「不知道」变成「随时能查、不一致就报警」。
// 现状数据经 2026-07-25 当日实测核对 · 方案分四层、可分阶段落
谈治理之前先回答一个基本问题:我们到底有几台云电脑。这次逐台核对下来,答案和团队记忆里的不一样——多出一台没人记录过的企业版机器。
| 机器 / 通道 | 身份与用途 | 远程可控性 | 核对结果 |
|---|---|---|---|
| 企业版 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 分钟,取数那一刻五台个人版全部在窗口外。这只是一个时点快照,不等于故障,但正说明「靠心跳只能看出活没活,看不出跑的是哪版」。
企业版和个人版今天走的是两条完全不同的分发链:源码仓不同、触发方式不同、校验方式不同。漂移不是某一环坏了,而是两条链的最后一米都靠人。
独立仓 → 本机压缩分块 → 远程写盘
本机把脚本 gzip + base64 切块,逐块经远程命令写进机器,解压后 MD5 逐字节校验,对上才替换、然后重启计划任务。
主仓 → GitHub Actions → 公共镜像 → 机器 curl
合并到主分支后自动同步到公共镜像(免 token 的 raw 地址),机器上运营双击 update.bat 把文件拉下来。
把这条链逐段量一遍,问题出在哪一段就很清楚了:
// 自动化的那一段(GitHub Actions → 镜像)是好的,25 个文件逐字节一致;坏的是它之后的每一段。
机器上双击的 update.bat 只下载 11 个文件,而镜像里有 25 个。拉不到的 14 个包括:整个 route-B 潜在触达脚本族、开机自愈看门狗、以及——update.bat 自己。更新器不在自己的更新列表里,等于单向棘轮:机器上那份停在它被摆渡下来那天,之后仓库里新加的文件永远拉不到。而运营跑完看到「all downloaded」,会以为已是最新。
这类故障不会红,只会安静地旧。真实案例:某台机器的发送脚本停在几个月前的截断版本,导入链一崩把整条「回复捕获」腿废掉——但发送腿照常工作,日志滚得快,很久没人发现。「跑了更新」和「真的更新了」之间隔着一整条没人验的链。
所有漂移问题往下挖一层,都会撞到同一堵墙:中央没有任何渠道知道某台机器此刻在跑什么版本。不是查起来麻烦,是这个数据根本没被传出来过。
# 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 盖成当前时间。这是一个纯粹的存活信号——它能告诉你进程还在,完全无法告诉你进程在跑哪一版代码。
想知道某台在跑哪版,今天的办法是:连上远程桌面 → 翻文件字节数 → 数关键函数出现次数 → 和仓库版本对字符串。八台机器全查一遍要小半天,而且下次还得再来一遍。
问题不在「怎么把新版推下去」——推的办法有的是。问题在推完之后没有任何回执能证明它生效了。没有可观测,就没有治理:你无法管理一个你看不见的东西。所以整套方案的第一步不是改分发,而是让每台机器把自己的版本说出来。
企业版能被中央远程下发,个人版不能——这个能力差不可能靠流程抹平。所以不要强求「两种机器走同一条投递通道」,那是把简单问题做复杂。真正该统一的是另外三件事。
「这一版由哪些文件、哪些哈希构成」——全机队共读一份 manifest,不管你是被推的还是自己拉的。
每台机器用同样的字段、同样的节奏上报自己在跑什么版本、文件指纹是多少。企业版个人版一视同仁。
期望版本与实际版本对不上,同一个定时任务、同一张红牌推到同一个群。不分机型。
企业版 = 推(中央一条命令下发到位,可确认);个人版 = 拉(机器定时自己去取,中央只能等它来报到)。两种投递方式最终汇合到同一个判据——上报的哈希是否等于 manifest 里的哈希。投递方式可以不同,验收标准必须相同。
升级并不能解决漂移——RunCommand 改变的只是投递方向,不改变验收缺失这个真问题。真按每台每月约 ¥249 算,五台个人版升上来一年多花约 ¥1.5 万,买到的是「能远程推」,而漂移照旧,因为没人在推完之后验。先把 L1/L2 做了,再按需要单独评估升级。
投入最小、收益最大的一步,排在所有事情前面。机器本来就每 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;
版本号是人写的、会忘记改;文件指纹是算出来的、不会撒谎。两个一起看:版本号相同但指纹不同 = 有人在机器上手改过文件,这恰恰是最难查的一类漂移。
企微常驻循环也有自己的心跳链路,同样在上报里加这两个字段即可。字段名与个人版完全一致,这样一张表、一个查询就能覆盖全机队,不用维护两套看板。
「哪几台还在旧版」「上周那个修复到底铺开了没」「这台的异常是不是因为版本旧」——全部变成一句 SQL,而不是连八次远程桌面。
不做「中央主动去查机器」(个人版根本没这个通道),只做机器主动上报。代价是离线机器的信息会陈旧——但陈旧的时间戳本身就是信息,配上心跳一起看就够了。能在两种机型上用同一种方式实现的方案,才值得做。
今天「一个文件要不要发到机器上」这件事,被记在三个互不相干的地方:同步流水线的触发路径、同步流水线的拷贝循环、以及机器上更新工具的下载列表。加一个新文件要同时改三处,漏改任何一处都不报错,只是静默失效。
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 里,并且执行顺序是「先更新自己 → 用新的自己更新其余」。少了这一步,更新器就是单向棘轮:仓库里加的东西它永远看不见,而机器上的人以为自己在跑最新版。
有了清单和自报,投递就退化成一件小事。两条链保持各自形态,但结尾必须相同:校验哈希、写状态、上报、由中央确认。
bundle.json(版本号 + 全量哈希),同步到公共镜像
// 注意重点不是「让个人版也能被推」,而是「把人从关键路径上拿掉」——双击是人的动作,定时任务不是。
机器关机就更新不了、运营在用时不能随便重启进程、抖音登录态是运营私人的。所以个人版的拉取要做成「开机后延迟若干分钟 + 每日固定时段各一次」,并且更新前先检查有没有任务在跑,跑着就跳过等下一轮。宁可晚几小时铺开,不要打断正在发的会话。
定时任务比对期望与实际,命中任一条推红牌:版本落后于期望、版本相同但指纹不同(有人手改过)、超过 N 天没上报过版本。前两条治漂移,第三条治「机器还在但更新链断了」。
期望版本不是全局一个值,而是按机器 pin。新版先 pin 到一台金丝雀机器,观察一个周期(心跳正常 + 业务量没塌),再把其余机器的 pin 推上去。
公共镜像按版本号保留最近若干个 bundle,回滚=把机器的期望 pin 指回旧版本号,下一轮自拉时自动退回。不需要人连上机器操作。
一张表:机器 / 归属 / 通道 / 期望版本 / 实际版本 / 指纹是否匹配 / 最后上报时间。绿=对齐,黄=落后,红=指纹不符。这张表就是「统一管理」这四个字的具体形态。
这套体系里,唯一算数的验收信号是「机器上报的哈希 == manifest 里的哈希」。更新脚本返回成功、日志打印 OK、运营口头确认「我点过了」——都不算。这条纪律不立住,前面三层白做。
「统一管理」有个常见误区是连配置一起统一。不行也不该——每台机器的屏幕坐标、账号身份、人设、限流额度天生不同。要做的是把两者分开,各用各的纪律。
| 类别 | 例子 | 要求 | 怎么保证 |
|---|---|---|---|
| 代码 | 发送脚本、轮询 agent、看门狗 | 全机队逐字节一致 | manifest 哈希比对 + 漂移告警 |
| 机器配置 | 输入框/发送键坐标、屏幕裁剪比例、分辨率 | 每台必然不同 | 本机标定;禁止跨机复制(客户端版本一升坐标就变) |
| 身份配置 | 账号 ID、企微人设、抖音登录态 | 每台唯一 | 1:1 绑定;恢复备份前必须核对 ID,防止张冠李戴 |
| 策略配置 | 日限、时限、单客户连发上限、工作时段 | 应集中管理 | 尽量收到数据库里由中央下发,别散在各机 .env |
真实事故:用带 BOM 的编码写 .env,把首行的变量名污染成不可见字符,程序读不到 → 进程死循环重启。同一条命令下发到两台,一台首行是注释就没事,另一台首行是变量名就炸。修法是把「必填项检查」写进启动自检并打印到日志首行,改完必须读回那一行确认,而不是看到进程起来了就走。
四层不必一次做完。按「投入 ÷ 收益」排,第一阶段一天之内能出效果,而且它本身就是后面几步的验收工具。
| 阶段 | 做什么 | 立刻拿到什么 | 粗估投入 | 风险 |
|---|---|---|---|---|
| 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 天 | 低 |
因为后面每一步都要靠它验收。没有版本自报,你做完 manifest 也不知道有几台真的换上了;做完定时自拉也不知道它有没有在跑。先把尺子造出来,再去改被量的东西。
将来要复制更多机器时:绝不能把「已装并登录过客户端」的机器做成基础镜像去克隆。设备指纹会被一起复制,多台在平台眼里成了「同一台设备挂多个号」,触发风控后整批一起出问题,还可能连坐封号。正确做法是基础镜像只装依赖不装客户端,克隆后逐台安装、逐台登录。