梦境 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици
..
config 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици
dist 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици
src 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици
tools 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици
README.md 8b1e6876db 完整工程入库:界面 v6 + 悬浮球(双窗口)+ 执行引擎 + ID提取两份方案 преди 2 седмици

README.md

tiktok-auto — TikTok 真机自动互动(AutoX + MCP)

自动完成这条链路:

打开 TikTok → 搜索关键词 → 从结果里挑一个视频 → 随机观看 10–35 秒 → 发一条评论

面向"要长期跑、网络会抖、界面会弹东西"的场景写的,所以重点不在"能跑一次", 而在网络慢的时候也判得准、失败时说得清、改文案不用改代码。


1. 快速开始

前置:手机装 AutoX 并开启无障碍;VS Code 里 AutoX 扩展已激活(它会起 MCP 桥 127.0.0.1:18789)。

# 0) 把配置推到手机(改文案/关键词只改 JSON,不用改代码)
node tiktok-auto/tools/run.js --push-config

# 1) 设备端纯函数自检(不碰界面,约 1 秒)
node tiktok-auto/tools/run.js --self-test

# 2) 走完整流程但不真的发评论(安全验证)
node tiktok-auto/tools/run.js --dry-run --keyword fashion

# 3) 正式跑(会真的发评论)
node tiktok-auto/tools/run.js --keyword fashion --videos 1

常用参数:

参数 作用
--keyword <词> 覆盖搜索关键词
--videos <n> 本次处理几个视频
--dry-run 输入评论但不发送
--config <file> 用 JSON 文件覆盖配置(见 config/smoke-2videos-dryrun.json)
--push-config 把 config/tiktok-auto.config.json 推到手机
--self-test 只跑设备端纯函数自检
--eval <file> 调试用:加载全部模块但入口换成该文件的代码
--raw 打印设备端原始日志(不汇总)

退出码:0 成功,1 流程未达成,2 MCP 桥不可达,3 脚本命中危险调用黑名单需人工确认。

⚠️ MCP 单次下发有 120s 上限。--videos 大时请分批跑,别指望一次下发里连看十条视频。


2. 目录结构

tiktok-auto/
├── config/
│   ├── tiktok-auto.config.json      # 业务配置(关键词/评论池/时长/超时/开关)
│   └── smoke-2videos-dryrun.json    # 冒烟用覆盖项
├── src/                             # 源码:一个文件只做一件事
│   ├── logger.js     # 结构化日志
│   ├── bridge.js     # ★ 与界面宿主的桥(状态上报 / 停止请求),未启用时零开销
│   ├── config.js     # 默认值 / 加载 / 深合并 / 校验
│   ├── wait.js       # ★ 唯一的等待原语(waitFor / poll / waitQuiet)
│   ├── layout.js     # ★ AutoX 无障碍 API 适配层(所有平台怪癖都关在这里)
│   ├── ui.js         # 点击四级降级 / 输入+回读校验
│   ├── popups.js     # 干扰弹窗:检测与执行分离
│   ├── tiktok.js     # ★ TikTok 选择器与页面状态机
│   ├── player.js     # ★ 播放存活探测 + 有界观看
│   ├── comment.js    # ★ 评论发布与**分级证据校验**
│   ├── flow.js       # 业务流程编排
│   ├── main.js       # 入口与收尾(保证 __END__ / 关线程 / 输出 RESULT / 上报终态)
│   └── __selftest.js # 设备端纯函数自检
├── tools/
│   ├── build.js      # 构建:src/ 多模块 → dist/ 单文件(含两道自检)
│   ├── mcp.js        # MCP 桥最小客户端
│   ├── run.js        # PC 侧执行器
│   └── diag-*.js     # 排障探针(见第 7 节)
└── dist/tiktok-auto.js              # 构建产物:可直接在 AutoX 里跑的单文件

为什么要有 tools/build.js:人维护代码时希望"一个文件只做一件事"(src/ 拆成 12 个模块), 但 AutoX 通过 MCP 下发的是一整段脚本,没法带一堆 require。 中间的 concat 一步同时满足两边:开发按模块读,部署是一个自包含文件。


2.1 两种运行方式

同一个产物既能独立跑,也能被 tiktok-app 界面唤起:

独立运行 被界面唤起
触发 MCP 下发 / 打开产物 / tools/run.js 界面点「开始运行」→ engines.execScriptFile(产物, 参数)
配置从哪来 config.resolve(injected) 读 /sdcard/Download/tiktok-auto/config.json 界面写好的 engine-config.json,路径由 --config 传入
界面能看到进度吗 不能(也没界面) 能:bridge.report() 写 run-state.json,界面每秒读
能叫停吗 靠 PC 侧断连 / 手动停 能:界面写 stop.flag(内容 = runId),wait.tick() 轮询到就抛 StopSignal
收尾 打印 __END__ + RESULT 额外写一次终态(finished/stopped/failed)

桥未启用时全部是空操作(bridge.isEnabled() 为假),所以独立运行的行为与以前完全一致。

三个设计要点,都写在 src/bridge.js 的注释里:

  1. 停止用异常抛出(StopSignal),并且在唯一的等待出口 wait.tick() 里检查。 停止可能发生在任意深度的等待里,逐层返回码要改几十处调用点、漏一处就变成"点了停止但不停"。 放在 tick() 里则一次实现、全流程生效。
  2. stop.flag 用内容比对(runId)而不是"文件在不在":不必删除它 (删除有竞态,files.remove 又命中 MCP 危险调用黑名单), 上一轮的残留标志也不会误杀本轮。
  3. 注释 comments.enabled:关掉就是"只浏览不发评论"。界面没勾「评论」时走这条, 用户的意图优先于引擎的默认行为。

能力边界(如实标注):评论动作已接入;关注 / 私信 / 点赞尚未实现 —— 界面勾了会在启动时提示一次。收到 StopSignal 时守护线程也会一并收工 (否则它每一轮巡检都会记一条 WARN 把日志刷满)。


3. 配置

改 config/tiktok-auto.config.json → --push-config,不需要重新构建、不需要改代码。

段 关键字段 说明
target keyword 搜索关键词
maxVideos 本次处理几个视频
resultTab 用哪个结果页签(Top / Videos / …)
pickStrategy random(默认)或 first
watch minMs / maxMs 观看时长区间,默认 10000–35000
comments pool 评论文案池,随机挑,同轮不重复
strictPostCheck true 时只认强证据,否则弱证据也算成功
verifyInList 是否打开评论列表做地面真相校验(默认开)
timeouts 各阶段上限 全部是上限,不是固定等待
probe useAudio / usePixels 播放存活证据源开关
livenessIntervalMs / stallTolerance 卡顿判定节拍与容忍度
guard enabled / intervalMs / dismissLabels 干扰弹窗守护线程
runtime dryRun 只输入不发送
configPath 设备上配置文件的路径
selectors sendButtonId 发送按钮的 id 逃生舱(见下)

逃生舱:评论发送按钮在 TikTok 里没有可用文案(其 content-desc 是个无关账号串), 所以默认靠结构定位(工具行同高、最右、且不属于右侧竖排动作栏)。 若某次 TikTok 改版导致结构定位失效,把它的资源 id 填进 selectors.sendButtonId 即可,不用改代码。 (实测两条路径都指向 Rect(952,1264 - 1056,1328),即同一个控件。)


4. 需求 → 设计对照

需求 1:网络延迟下也要稳定,不能只靠 sleep

做法:脚本里不存在"拿 sleep 当等待"的代码。 区分两件事:

  • 轮询节拍(probe.pollTickMs,默认 150ms)= 多久检查一次,允许存在;
  • 盲等(sleep(N) 赌它 N 秒后会好)= 禁止。

所有"等"都走 wait.js 的 waitFor(条件, 上限):快的时候立刻返回,慢的时候一直等到条件成立或到上限。 于是"网络有多慢"和"脚本能不能跑通"被彻底解耦。

waitQuiet() 更进一步:判据不是时间,而是可见文案签名连续 N 次不变, 天然适配"加载中 → 不断有新节点 → 稳定"这个过程。

postMs / pageReadyMs 这类参数都是上限,不是固定等待。

需求 2:稳定性

机制 解决什么
播放存活探测 观看期间按节拍采样证据(AudioManager.isMusicActive(),可选像素差),连续 stallTolerance 次无证据即判定卡顿,轻点画面尝试恢复一次
页面身份守卫 观看全程校验"还在播放页",连续漂移即提前结束并如实上报 aborted
多证据状态判定 播放页要 返回箭头 + 无底部导航 + 点赞 + 评论 四条同时成立(只看点赞+评论会把首页也判成播放页——实测踩过)
发布分级校验 见下方"证据分级",不靠单一信号
弹窗守卫线程 独立线程清已知弹窗;主流程只用只读 popupPresent() 去等它清完,不抢它的活;绝不 back() 正常页面
未知弹窗只记录不猜 命中名单才动手;看起来像弹窗但不在名单里 → 只记 WARN,由人决定要不要加
页面状态机 feed / search / results / player / comment-panel / unknown,ensureFeed 先看再动、unknown 连续多轮才按返回,绝不无脑 back
坐标取"可见交集中心" 被状态栏切掉一半的节点,几何中心会点到状态栏
去掉不可见格式字符 评论列表头部的实际文本是 "‎150 comments"(U+200E),带锚点正则永远匹配不上——这是适配层统一吸收的平台噪音
构建期两道自检 语法检查 + MCP 危险调用黑名单预检(命中会让每次下发都要人工确认,必须在构建期挡住)
收尾必达 无论成败都打印 __END__、关线程、输出一行机器可读 RESULT

需求 3:代码规范、可维护

按《代码整洁之道》与《编写可维护的JavaScript》落地:

  • 单一职责 + 分层:ui.js 只管"怎么点",tiktok.js 只管"TikTok 长什么样",flow.js 只管"先做什么"。 换 App 或 TikTok 改版,只动 tiktok.js;flow.js 一行不用改。
  • 边界适配层:auto.root 是属性不是函数、UiObject.text() 是快照、离屏节点占四成…… 所有平台怪癖关在 layout.js 一个文件里。
  • 配置对象(Zakas ch09):会变的业务数据全部外置成 JSON。
  • 自定义错误(ch10):配置非法抛 ConfigError,带字段与非法值,而不是在几十行之后奇怪地炸掉。
  • 单全局/零全局(ch06):整个产物是一个 IIFE,不污染任何全局。
  • 注释只写"为什么":每一个反直觉的写法旁边都写了它是被哪次真机故障逼出来的。
  • 自动化测试:__selftest.js 覆盖配置合并、poll 的假值/异常/超时语义、评论挑选、 选片策略、随机区间等纯逻辑,一条命令在真机上跑完。

评论发布的证据分级

级别 判据 说明
高 评论列表里出现该文案 地面真相
中 评论数自增 必须轮询着等:角标有 1–2s 延迟,看一眼就下结论会把成功误判成失败
低 面板收起 + 输入框复位为占位符 必要条件,单独不足以证明

最终置信度会如实写进 RESULT(confidence: high/medium/low), --dry-run 时不会误报失败,strictPostCheck 可要求只认强证据。


5. 真机实测记录

设备:Pixel 5a(1080×1920,Android 14,容器化远程机),com.zhiliaoapp.musically。

已验证通过:

  • 搜索 → 结果页 → 进视频 → 观看 → 评论 全链路
  • 观看时长实测 26974/26889ms、35053/34983ms(均在 10–35s 随机区间内),证据源 audio
  • 评论计数 190→191(via=comment-count,置信度 medium);评论列表地面真相曾实测 149→150 comments 且新评论显示 1s ago(置信度 high)
  • 多视频循环:2 个不同卡片、2 条不同文案、两次观看均完成(dry-run 冒烟)
  • 从"异常态"(评论面板开着、pageState=unknown)自动恢复到首页并跑完
  • 发送按钮两条定位路径(结构化 / 配置 id)指向同一控件

踩过并修掉的坑(都在源码注释里):首页被误判成播放页、评论面板开着导致状态机迷路、 ensureFeed 先盲等 15 秒、hasOwnProperty.call( 触发 MCP 确认、评论头部的 U+200E、 visibleTexts(25) 数量上限把评论条目挡在外面、发布后立刻点角标被收起动画吞掉。


6. 已知限制

  • 像素级播放校验默认关闭:Android 14 的 MediaProjection 是按会话授权的, 脚本进程拿不到(captureScreen() 报 No screen capture permission)。 想开启:给 AutoX 授予截屏/录屏权限,并设 probe.usePixels: true。 默认用音频活跃度(probe.useAudio),静音视频会退化为"页面守卫"模式, 此时 RESULT 里会明确标 probeMode=page-guard-only / degraded=true,不会假装成功。
  • 单次下发 120s 上限:MCP 桥的硬限制,maxVideos 大时请分批。
  • 评论发布在本机验证为"最终一致":角标与列表头部刷新时机不一致(实测出现角标 516 / 列表头 515)。 所以判定采用"轮询 + 分级证据",而不是单点读数。
  • 资源 id 会随 TikTok 改版重排:代码刻意优先使用无障碍文案锚点,混淆 id 只作逃生舱。

7. 排障

先不要猜着改选择器,按顺序来:

  1. 看症状定位到哪一层
    • 未捕获异常 → src/ 逻辑问题,先 --self-test
    • ensureFeed 超时 / pageState=unknown → 页面判据问题,跑 tools/diag-page-state.js
    • 评论列表校验未通过 → 发布或校验问题,跑 tools/diag-list-open.js
    • 发送按钮 相关 → 跑 tools/diag-send-button.js
  2. 跑对应探针(只读,不改设备状态):

    node tiktok-auto/tools/run.js --eval tiktok-auto/tools/diag-page-state.js --raw
    node tiktok-auto/tools/run.js --eval tiktok-auto/tools/diag-list-open.js --raw
    node tiktok-auto/tools/run.js --eval tiktok-auto/tools/diag-send-button.js --raw
    
  3. 要地面真相(会真的发一条评论,发完前后对比评论列表):

    node tiktok-auto/tools/run.js --eval tiktok-auto/tools/diag-post-verify.js --raw
    
  4. 看完整时序:加 --raw,日志带 +Nms 相对时间戳,直接能看出"卡在哪一步、卡了多久"。

症状 常见原因
日志里只有 Start operation 没有自己的输出 下发脚本的 name 少了 .js(由工具链保证,一般不会)
status=needs_confirmation 代码里出现了 MCP 黑名单里的调用;构建期本应挡住,检查是否绕过了 build.js
明明发了评论却报低置信度 角标延迟;看 verifyInList 是否被关掉
一连串 clickableAncestor 点击 文案节点本身不可点,属正常降级路径