01 / 阅读口径
先看影响面,再看功能名
版本号只是索引,真正要判断的是「这一版会不会动到我正在跑的东西」。所以每一条都按同一个结构写:做了什么、为什么做、影响到谁、怎么迁移、怎么验证。五段里后两段最值得先读,因为它们直接决定你要不要停机、要不要重跑基线。
版本之间的关系不是功能堆叠,而是问题顺序。3.15 解决的是「设备能不能连上、流程能不能跑完」;3.16 解决「跑完了但输入不进去、屏幕状态读不到」;3.17 解决「能跑但操作形态太整齐」;3.18 解决「跑了但说不清发了什么」;3.19 解决「说清了但到点发布时内容已经不成立了」。按这个顺序读,你会看出每一版都在补上一版暴露出来的洞,而不是在加新花样。
三类使用者受影响的程度不同。只跑单账号、每天几条的,主要看 3.19 的复检闸门与 3.17 的参数;多设备并行的,3.16 的端口锁与 3.15 的存活判定是必读;多账号代运营的,3.18 的台账口径会改变你对外交付的记录形态,迁移成本最高。
如果只想确认一件事——升级安不安全——直接看每条的「怎么验证」。验证方式全部是可以在本机跑出来的:跑一轮自检看哈希有没有变、构造一条必然被拦的商品看闸门拦不拦、两条通道跑同一份计划看记录数对不对。没有一条验证需要问我们,这是这套系统刻意保持的性质:结论要能从日志里自己读出来。
版本文件的更新方式与部署步骤写在下载与部署里;如果你还不确定自己需要哪几个模块,先看产品功能总览,那里有模块之间的依赖顺序。
关于兼容性,有一条贯穿五个版本的约定:配置文件与流程文件的格式只增不改,新版本必须能读旧文件,旧版本读不了新文件是允许的。这条约定的代价是新字段会一直留着,好处是回滚不需要改配置。所以升级的正确姿势是换程序不换文件,确认稳定之后再按需改文件;反过来先改文件再升级,回滚时就没有可退的版本了。
还有一处容易被误读:版本号不等于功能数量。3.16 与 3.17 都没有加新模块,改的是已有模块的内部口径,但这两版对稳定性的影响比任何一次功能新增都大。判断一版值不值得升,看的是它改的是你正在痛的地方,而不是看它加了几项。
02 / 3.19
发布时刻复检:到点再判一次
这一版改的是闸门的时机。此前合规检查发生在发布计划生成的时候:计划上午生成,闸门上午跑一遍,通过就进队列,到点直接提交。问题出在中间这段时间——上午成立的条件,晚上不一定成立。
具体的失效形态有三种。第一种是同款撞车:上午这个类目下没有别人用同一个标题结构,下午有人先发了,你的标题与主图相似度在当下基线里已经超标。第二种是类目归属变化:平台侧的类目入口调整,上午能归进去的品,晚上归不进去了。第三种是节奏冲突:同一批计划里前后两条挤在同一分钟,单条服务线的每日上限在合并后其实已经超了,但每条单独看都合规。
这一版把判定挪到发布时刻:到点提交之前,用当下的基线把合规闸门完整重跑一遍,不通过就不发。这是 fail-closed——默认拦住,要放行必须给出理由,而不是默认发出、出事再说。改动看起来只是一个时机,实际上要求闸门的每一项都能被单独调用、输入必须来自同一份发布计划、结论要落进日志并带原因码,而不是返回一个布尔值。
影响到谁:所有使用定时发布的账号,尤其是多账号与多服务线的。单账号、手动即时发布的受影响很小,因为生成与提交的间隔本来就短。
怎么迁移:先在测试环境把闸门的检查项逐项单独跑通,确认每一项都能在不依赖计划生成上下文的情况下给出结论;再把定时任务的提交入口改成「先复检、后提交」,复检失败时不要静默跳过,要把计划停在待处理状态并写原因;最后确认原因码能被台账读到,否则复检拦下来你也不知道为什么。
怎么验证:构造一条必然被拦的商品放进计划——比如主图与已有链接几乎一样、或者标题与同款高度重合——到点观察它是否停发,并且日志里有明确原因。反向测试 PASS 指的就是这一类用例:本该失败的确实失败了。如果一条本该被拦的商品顺利发出,说明复检没生效,或者复检读的不是当下基线。闸门各项的判定口径写在商品合规检查,节奏参数写在定时发布与节奏控制。
这一版带来的副作用要说清楚:复检会拦掉一些你上午以为已经过了的内容,发布成功率这个数字会往下掉。它掉的是「本就不该发的」,不是「能发的」。如果你的监控只看成功率,这一版之后要把「被复检拦下」单独算一类,否则会误判成系统退化。
复检还要保证幂等:同一份计划、同一份基线,跑两次得到的结论必须一致。这条要求不高但容易破——只要检查项里有一项读了当前时间、随机数或者上一次的执行结果,结论就会漂。漂的后果是同一批计划里有的被拦、有的没被拦,而你无法解释差别在哪。验证方法很简单:拿一份会被拦的计划连跑三次,三次的原因码要相同,次序也要相同。
另外要接受一点:复检只能在提交这一端做,它管不住提交之后的事。商品发出去了、半小时后被平台清理,这不属于闸门的范围。所以台账里要把「已提交」与「已确认在线」分成两个状态:前者是系统能保证的,后者要靠回查。这一步经常被省略,直到某天发现台账里全是已发布,平台上一件都没有。
03 / 3.19 事故
自检写进生产记录:根因不在自检,在路径
3.19 之前发生过一次事故,值得单独写一条,因为它改变的是纪律而不是功能。事故现象是:生产台账里出现了一批「已发布」的记录,但平台侧并没有对应的商品。追下去发现,这些记录是自检跑出来的——自检为了验证发布链路,会走完整流程并写记录,本该写进隔离目录,实际写进了生产路径。
根因不是自检逻辑写错,而是路径解析的时机。程序启动时先解析了数据目录,再读取环境变量;而隔离目录正是通过环境变量指定的。解析早于生效,环境变量等于没设,自检和老老实实写生产的行为完全一样。这类问题的可怕之处在于它不报错:路径存在、写入成功、日志正常,只是写错了地方。
修复分成四道防护,每一道单独都能拦住同一类问题。第一道是顺序:先读环境变量,再解析任何路径,解析结果要打印在启动日志里,肉眼能看见这次跑的是哪个目录。第二道是备份:写生产数据之前先留 .bak,任何一次写入都可以退回上一份。第三道是标记:自检进程带标记运行时,一旦要写生产数据直接抛错,不写入、不降级、不静默跳过。第四道是比对:整轮自检开始前与结束后,对生产文件做哈希比对,两次数值不一致就判定这轮自检污染了生产数据,整轮作废。
影响到谁:所有在同一个环境里既跑自检又跑生产的人。这是最常见也最危险的部署形态——一台机器,两个用途,靠环境变量区分。
怎么迁移:如果你现在的自检与生产共用目录,先把自检挪到独立目录再升级;如果暂时不能挪,至少确认启动日志里打印的数据目录是隔离目录,并且第三道防护的标记确实生效(手动跑一次自检,观察它是否在写生产时抛错)。
怎么验证:记录自检前的生产文件哈希,跑一轮完整自检,再取一次哈希,两次必须一致。这条验证不需要看日志、不需要读代码,只看两个数。事故之后我们把这条做成了每轮自检的固定动作,哈希不一致时整轮结果不可信。台账本身的记录口径见发布数据台账。
这一条之所以写在更新日志里,而不是写在内部文档里:只要你的自检与生产共用一个数据目录,你就有同样的问题,只是还没遇到。
事故之后还补了一条纪律:任何一次「全绿」的自检结果,都必须附上哈希比对的两个值,否则不采信。理由是自检全过但污染了生产,比自检报错更危险——报错你会停下来看,全绿你会直接往下走。现在自检的输出里固定带三行:这一轮跑的数据目录、跑之前的哈希、跑之后的哈希。三行对不上就不用往下看了。
04 / 3.18
台账收成单一口径,归属改成算出来的
3.18 之前,两条通道各自记自己的账。手机通道记一份,网页通道记一份,字段名不同、时间戳口径不同、item_id 有时为空。结果是同一条商品在两边各有一条记录,或者两边都没记——因为都以为对方记了。这个问题在单账号时只是看着乱,在多账号代运营时是致命的:客户问「昨天那三条发出去没有」,你答不上来。
这一版把台账收到一个模块里,两条通道写同一本,字段固定为 item_id、标题、描述、价格、时间戳。写入入口只有一个,任何通道都必须经过它,不允许旁路写文件。时间戳统一取提交完成的时刻,不取计划里的预定时刻——这两者经常差几分钟,而差的几分钟恰恰是判断「到底发没发」的关键。
更关键的一处改动是槽位归属。同一份发布计划里,某一条归哪条通道执行,以前是靠人工标记的:生成计划时打个字段说这条走手机、那条走网页。人工标记的问题是它会漂——计划改了、通道挂了、重跑一次,标记还停在原处。3.18 改成算出来的:归属由同一个函数对象根据计划内容与通道可用性计算,不读任何标记字段。同一份输入永远得到同一个归属,重跑不会变。
影响到谁:所有同时使用两条通道的部署;只跑一条通道的,收益是记录变整齐,迁移成本很低。
怎么迁移:历史记录按新口径重导一次,字段对齐,缺 item_id 的标为待补而不是丢掉——丢掉等于承认这段历史不可查。重导之后不要立刻删旧文件,保留一个版本周期。归属字段如果以前是配置项,升级后要删掉,留着会让人以为还能手改。
怎么验证:用同一份发布计划,让两条通道各跑一遍,比对台账记录数与计划条数是否一致、有没有同一条出现两次、有没有 item_id 为空。再跑第二遍同样的计划,确认归属结果与第一遍完全相同——如果变了,说明还有地方在读标记而不是在算。台账字段的完整说明见发布数据台账,网页通道的提交细节见电脑网页版通道。
收成一本账之后,导出的形态也要跟着定。对外交付时导出的不是数据库文件,而是一张按时间排序的表:时间戳、item_id、标题、价格、状态。标题按当时的原文导出,不取当前值——商品后来改标题是常事,导出当前值会让记录与当时的发布对不上。这条看着细,但需要回溯「当时到底怎么写的」时,只有原文能回答。
05 / 3.17
拟人化参数收敛,从「慢一点」变成具体数
3.17 之前,流程里的等待是靠固定睡眠写的:每一步之间停一秒,滑动用直线拖过去,点击落在控件正中。这套写法能跑通,但跑出来的形态有明显特征——间隔永远是整一秒,轨迹是几何直线,落点精确到像素中心。人不会这样操作。
这一版把等待收进一个统一的层,给的是区间而不是常数:点击间隔 0.34 到 1.30 秒,在这个区间内取样而不是取固定值;滑动走 smoothstep 曲线,起止慢、中段快,符合手指加速再减速的形态;落点做 ±4 像素的偏移,不落在控件正中;按压时长 72 到 155 毫秒,覆盖轻点与稍重按压的区间。
为什么是这些数:区间取自真实手动操作时录到的分布,取的是覆盖范围而不是平均值。用平均值等于把所有操作改成同一个节奏,那和固定一秒没有本质区别。这也是这一版最容易被误解的地方——它不是为了「更像人」这个说法,而是为了让间隔的分布不呈现单一峰值。
影响到谁:所有走设备端的流程。网页通道不受影响,因为它不产生点击行为。
怎么迁移:把流程文件里手写的固定 sleep 删掉,统一走拟人化层。两层叠加是最糟的状态——流程里等一秒,外层再随机 0.3 到 1.3 秒,实际间隔变成一个更奇怪的双峰分布。迁移时逐条检查流程文件,凡是显式写死的等待都要处理。
怎么验证:看日志里记录的实际间隔与落点偏移,看分布而不是看单条。单条看起来正常没有意义,一条流程跑一百次,间隔如果集中在某个窄区间,说明随机取样没生效。滑动轨迹可以在录制回放里逐帧核对,落点偏移看坐标差是否落在 ±4 像素内。
这一版的边界要说清楚:参数只能改变操作形态,不能改变账号本身的权重、内容质量与类目竞争。把参数调到最像人,也解决不了文案与实际不符的问题。参数取值的完整说明在防封控,设备端的执行链路在手机 APP 自动化通道。
日志这一版也补了字段:每一次点击记录实际间隔、落点坐标与按压时长,滑动记录起止点与曲线类型。这些字段平时没人看,但排查时是唯一的证据——判断「是不是节奏太整齐」靠的就是间隔的分布,而不是感觉。日志保留一个版本周期即可,不长期堆着,否则翻起来更慢。
06 / 3.16
中文输入与设备层:三条通道与一把锁
3.16 解决的是「流程跑完了,但标题是空的」。根因是输入:adb input text 只能打 ASCII,中文打不进去,而且不报错——它把命令执行完,屏幕上什么也没出现。这类失败最难查,因为日志显示成功。
这一版给了三条输入通道,按可用性排序使用:uiautomator2 通道、adbkey 通道、adb_input 通道。其中 uiautomator2 通道已验证可以输入中文,是默认选择;另外两条作为回退,在设备不支持或稳定性不足时使用。回退的设计原则是「换通道而不是整体失败」——输入这一环失败不该让整条流程作废。
同一版还处理了两个设备层的坑。第一个是端口:uiautomator2 一个进程只绑一个 adb server 端口,两个进程同时起会互相抢占,表现为随机一台设备失联。解法是端口锁——起进程之前先拿锁,拿不到就排队,不能并发。第二个是界面读取:uiautomator dump 在 launcher 屏会失败,桌面状态下拿不到层级,流程就卡在第一步。解法是先试 --compressed,失败再退回普通 dump,并且带重试;同时控件定位改为候选列表加 instance 序号,而不是单一选择器——界面上同文案的控件往往不止一个,写死一个选择器,改版一次就断。
影响到谁:所有走设备端的部署,多设备并行时最明显。
怎么迁移:把控件选择器改成候选形式,给每个候选留出 instance 序号;把输入动作改成走统一入口,不要在流程里直接调 adb 命令;多设备部署确认端口锁已生效,起进程的路径上不能有绕过锁的分支。
怎么验证:自检在这一版之后固定跑三组——网页通道 82/82、主路径冒烟 26/26、手机通道 24/24,另外在 3 台设备上各跑 13/13 的引擎自检。这些数字的意义不是「全绿」,而是覆盖到了具体分支:dump 失败后的回退、输入通道的切换、端口锁的排队。多设备并行的完整说明在多设备管理。
候选选择器要按界面上的实际顺序写,而不是按语义写。举个具体的例子:发布页里「图片」这个词可能出现三次——相册标题、已选图片计数、上传按钮,文案一样,层级不同。写死一个选择器,命中的是哪一次完全取决于加载时序;给候选加 instance 序号,才能表达「第三个出现的那个」。序号也不是越多越好,超过三四个说明这块界面本身不稳定,应该改用 dump 先定位再操作。
输入通道的切换要有日志:这一次用的是哪条通道、切换发生在第几步、切换之后有没有成功。没有这三条,输入失败会变成「偶发」,而偶发是无法修的。
07 / 3.15
ADB 通道从零落地:五层各管一件事
3.15 是这条通道的第一个可用版本。做法不是找现成方案拼,而是从零写,原因是设备端的失败形态太杂:连不上、连上了不通、通了但读到的是上一次的界面、界面读到了但点不中、点中了但输入不进去。每一种都需要独立的判定与重试,拼不起来。
最终分成五层,每层只管一件事。adb 层管连接:超时封顶,不能无限等;server 显式寻址,不靠默认;设备存活只认 shell 往返——进程在、端口开着都不算活,只有一条命令能来回才算。uia 层管界面:设备端 dump 带重试,选择器给候选与 instance 序号。humanize 层管操作形态,参数在 3.17 收敛。device 层管输入通道,三条通道的编排在 3.16 补齐。flow 层管流程:YAML 描述动作,引擎负责顺序、条件与中断。
YAML 的动作集刻意做小:by_any 按候选顺序找控件,if_enabled 与 if_exists 做条件分支,abort 主动终止,reset 回到已知状态,require 声明前置条件不满足就停,dump 与 shot 留证据。不做循环、不做变量运算,是因为流程一旦复杂到需要编程,维护成本会比手工操作更高,出错也查不出来。
这一版最大的坑不在设备,在环境:驱动跑在 WSL 里时,到 Windows 侧的 TCP 不通,adb 连不上设备。试过转发、试过改绑定地址,最终结论是不要绕——驱动必须跑在 Windows 侧,和 adb server 在同一侧。这条结论写进部署说明,因为它的现象(连不上)和很多别的原因长得一样,容易被误判成设备问题。
影响到谁:所有新接入的部署。升级本身不改变已跑通的配置。
怎么迁移:按五层的边界检查自己的流程文件——如果流程里直接写了 adb 命令,说明绕过了 uia 与 humanize 两层,这部分要收回;如果 YAML 里出现大量重复的等待与点击,说明该拆成分步动作。
怎么验证:先跑引擎自检(每台设备 13/13),再跑手机通道自检(24/24),最后跑主路径冒烟(26/26)。三组都过再上真实商品。部署步骤与运行环境要求见下载与部署,通道本身的设计说明见手机 APP 自动化通道。
超时封顶与存活判定是这一版最不显眼但最省事的两处。没有封顶时,一个卡住的命令会把整条流程挂在那里,看起来像设备死机;有了封顶,卡住的步骤会带着超时退出,流程继续往下走或者按 abort 收口。存活判定改成只认 shell 往返,是因为进程在、端口开着这两种状态都可能出现在设备其实已经掉线的时候,唯独一条命令来回能证明它还在。
require 这个动作值得单独用一次:它声明前置条件,不满足就停,而不是继续往下点。典型用法是进发布页之前先 require 当前在首页,不在就 abort——后面所有步骤都假定从首页出发,一旦起点错了,点下去的结果是随机的。
08 / 迁移
升级的四步与三条不能绕的限制
把五个版本的迁移动作收成一套通用步骤,顺序不要改。第一步,备份:生产台账、流程文件、配置文件各留一份带版本的副本,副本与线上文件不在同一个目录。第二步,隔离自检:在独立路径跑一轮完整自检,跑前跑后取生产文件哈希,两次数值必须一致。第三步,低风险试发:挑一条最不重要的商品走完整链路,从计划生成到提交到台账记录,全部看过一遍。第四步,放开:确认前三步都成立再恢复全量发布。
回滚的做法是换文件,不是改参数。流程文件与配置都带版本号,回滚就是把上一份文件换回来,然后重跑自检。改参数回滚的问题在于改到哪一步算回到原状说不清,而换文件是可验证的。
第一条限制:平台界面改版会打断控件定位。控件候选与 instance 序号能提高存活率,但不能保证永不断。改版之后流程会在某一步停住,这时看 dump 与截图定位,不要靠猜。第二条限制:设备端依赖具体机型与系统版本。同一套流程在不同机型上的表现可能不同,多设备部署要按机型分组验证,不能一台过就全过。第三条限制:系统对发布结果不做承诺。发出去了会不会被平台清理、账号权重如何变化,这些不在系统能保证的范围内,一切都以实际执行日志为准。
还有一条不属于技术限制但更常被忽略:这套系统不承诺绕过平台规则。合规闸门拦下的内容,改标题重发不会变得合规,只会换一个原因被拦。闸门的检查项与判定口径见商品合规检查。
下一版的方向集中在两处:一是把复检闸门的原因码做得更细,现在只能区分大类,定位还要人工看日志;二是把多设备的分组验证做成固定流程,减少按机型逐台手测的成本。两处都不改变现有配置,升级不需要额外迁移。风险边界与责任划分的常见问法在常见问题里。
升级的时间窗口也值得挑一下。不要在发布窗口内升级——09:00 到 21:59 之间任何一次重启都可能让在途计划停在半路,而半路停下比干脆不发更难处理。稳妥的窗口是发布窗口结束之后、次日开始之前,并且确保这个时间段内没有排队的计划。如果计划是跨天排的,先把队列清空再升。