这份文档的目的:让没有历史对话的 AI(或新同事)能安全地改这个项目,不重蹈已经踩过的坑。
一切结论都来自真机实测(Pixel 5a / Android 14 / TikTok 47.1.4 / AutoX 7)。标「实测」的都是跑过的,标「推断」的说明没验证。
| 你的任务 | 先读哪节 |
|---|---|
| 第一次接触这个项目 | 1 → 2 → 3 |
| 要改代码 | 5(硬规则) → 6(坑库) → 7 |
| 报错排查 | 10 |
| 改界面字段 / 加取材源 | 7.1 / 7.2 |
| 找选择器 | 8 |
| 决定"这个能不能做" | 9 |
最重要的一句:这个项目的坑几乎都是「看起来正常但结果是错的」——没有报错、没有崩溃,只是数据不对或功能静默失效。所以第 6 节比第 5 节更重要。
给 TikTok 做自动化:跑数据 + 批量提取用户 ID,界面用 AutoX 的 UI 模式,业务逻辑用无障碍操作 TikTok App。
┌─────────────────────────────────────────────┐
│ tiktok-app 界面(UI 模式) │
│ 登录激活 → 选任务 → 配参数 → 点「开始运行」 │
└───────────────┬─────────────────────────────┘
│ 通过 4 个 JSON 文件通信
▼
┌─────────────────────────────────────────────┐
│ tiktok-auto 引擎(无界面脚本) │
│ 两个 task: │
│ · video 搜索 → 观看 → 评论 │
│ · id 取材 → 过滤 → 去重 → 落盘 → 换数字uid│
└─────────────────────────────────────────────┘
│ 无障碍 API 操作 TikTok
▼
com.zhiliaoapp.musically
两条链路共用同一套基础设施:日志、桥接、等待原语、页面状态机、守护线程、停止信号、护栏。
unknown。所以有悬浮球(让用户能切走再回来)| 层 | 目录 | 产物 | 运行环境 |
|---|---|---|---|
| 界面 | tiktok-app/src/(19 模块) |
tiktok-app/dist/tiktok-app.js |
AutoX UI 模式 |
| 引擎 | tiktok-auto/src/(19 模块) |
tiktok-auto/dist/tiktok-auto.js |
AutoX 无界面脚本 |
| 独立脚本 | 根目录 tiktok_stats.js 等 |
不构建,直接用 | PC Node 或 手机 AutoX |
构建方式是「单文件拼接」:src/ 多模块 → 拼成一个自包含文件。
原因:AutoX 只能下发一整段脚本,不能带 require。
project.json 的 "main" → ./main.js
main.js 第一行必须是 "ui";
main.js 只做两件事:加载 tiktok-app/dist/tiktok-app.js + 可操作的报错
★
"ui";只对入口文件有效。写在被require的模块里只是一句字符串。
| 文件 | 方向 | 内容 |
|---|---|---|
/sdcard/tiktok_app/engine-config.json |
界面 → 引擎 | 完整配置 |
/sdcard/tiktok_app/run-state.json |
引擎 → 界面 | status / phase / done / summary |
/sdcard/tiktok_app/stop.flag |
界面 → 引擎 | 内容 = runId(不是空文件) |
/sdcard/tiktok_app/run-log.jsonl |
引擎 → 界面 | 逐行 JSON,界面读回做日志页 |
/sdcard/tiktok_app/ball-rect.json |
悬浮球 → 引擎 | 避让矩形 |
为什么走文件而不是进程内通信:子脚本看不到父脚本的任何全局变量(实测)。
三条容易踩的:
stop.flag 用「内容 = runId」而不是「文件存在」——删除有竞态,而且 files.remove 命中 MCP 的危险调用黑名单run-state.json 必须比对 runId,否则"刚点开始就显示上一轮进度"run-log.jsonl 而不是 .json——半截行只丢一行StopSignal 从唯一的等待原语 wait.tick() 抛出 → 全流程可中断、业务代码零改动。
界面两步:写 flag → 显示「正在收尾」→ 等引擎上报 stopped 才复位(约 1 秒,8 秒超时兜底)。
tiktok-scripts/
├── project.json AutoX 项目描述(main → ./main.js)
├── main.js ★ 入口:加载界面产物 + 报错兜底
│
├── tiktok-app/ 界面工程
│ ├── src/ 19 个模块(见下表)
│ ├── dist/tiktok-app.js 构建产物
│ ├── tools/build.js 构建 + 两道自检(ui 位置 / 语法 / 未完成标记)
│ ├── tools/smoke.js ★ 414 项结构自检(PC 侧,stub 环境)
│ ├── tools/audit-values.js 六场景取值审计
│ └── tools/fake-engine.js 假引擎(验证界面而不真跑 TikTok)
│
├── tiktok-auto/ 引擎
│ ├── src/ 19 个模块(见下表)
│ ├── dist/tiktok-auto.js 构建产物
│ ├── tools/build.js 构建 + 语法检查 + 危险调用预检
│ └── tools/pc-selftest-id.js ★ 68 项纯函数自检(秒级)
│
├── tiktok_stats.js 独立脚本:抓点赞/评论数/全量评论(PC Node)
├── tiktok-uid和视频短链解析.js 独立脚本:短链 → 视频 → 作者 uid(687 行)
├── tiktok_extract_cookie.js 独立脚本:登录态提取(已不需要,见 9)
│
├── docs/AI_CONTEXT.md ← 你正在读的这份
├── PLAN_*.md / REQ_*.md 历史方案(可读,但结论以本文档为准)
└── _probe/ 真机探测脚本与截图(可清理)
| 模块 | 职责 |
|---|---|
theme.js |
色值令牌、尺寸、字号(令牌缺键 = 静默视觉删除) |
store.js |
★ 配置默认值的唯一来源 Store.FIELDS(69 条),含版本迁移 |
sources.js |
ID 提取的取材清单(独立 storage key,不进 FIELDS) |
records.js |
运行记录(同上,独立 key) |
device.js |
设备能力探测(包名、权限、无障碍) |
service.js |
卡密 / 版本 / 更新检查 |
engine.js |
★ 界面配置 → 引擎配置的映射;起引擎 |
session.js |
运行态(纯内存,不落盘) |
widgets.js |
★ 控件工厂(W.*),所有输入框的唯一入口 |
dialog.js |
自绘弹窗(UI 模式禁止阻塞式 dialogs) |
floatball.js |
★ 悬浮球(两个 overlay 窗口,见 6.B) |
page-*.js |
6 个页面 |
main.js |
App 装配、路由、生命周期 |
main 入口,构建输出 19)logger → bridge → config → wait → layout → ui → popups → tiktok → player → comment → flow
↓
parse → uidset → filters → profile → idsource → resolve → idflow
└──────────── ID 提取(task=id)────────────┘
↓
main
main.js 不在依赖图里,它是入口与收尾:解析参数 → 按 config.task 分派到
flow.runSession(视频)或 idflow.runSession(ID 提取)→ 保证 __END__、关线程、
输出 RESULT、上报终态。
| 模块 | 职责 | 关键点 |
|---|---|---|
wait.js |
★ 唯一等待原语 | waitFor / poll / waitQuiet(settle) / tick(停止检查点) |
layout.js |
✦ 无障碍 API 适配层 | 所有平台怪癖都关在这里 |
ui.js |
点击四级降级 / 输入+回读 | |
tiktok.js |
★ 选择器 + 页面状态机 | idPageState() / pageState() / 各处 *Rows() |
bridge.js |
状态上报 / 停止信号 | 未启用时零开销(PC 自检靠这个) |
parse.js |
纯函数 | 计数缩写 / 语言判定 / 区间 / 用户名归一 |
uidset.js |
去重集合 + 结果文件 | 每 20 个刷盘;只追加不删 |
filters.js |
判定纯函数 | 读不到资料 ≠ 不符合 |
profile.js |
用户主页导航与读取 | waitProfile 两段等待;头像像素判定 |
idsource.js |
五种取材源 | 列表型 / 文本型 / 逐个点开型 |
resolve.js |
username → 数字 uid | 三级降级,见 6.F |
idflow.js |
ID 提取编排 | 护栏、停止、收尾 |
export MSYS_NO_PATHCONV=1
NODE="C:/Users/61554/.workbuddy/binaries/node/versions/22.22.2-6/node.exe"
# 如果 node 已在 PATH 里,把 $NODE 换成 node 即可
# 改完引擎
cd tiktok-auto && $NODE tools/build.js
$NODE tools/pc-selftest-id.js # 期望:通过 68 项
# 改完界面
cd tiktok-app && $NODE tools/build.js
$NODE tools/smoke.js # 期望:通过 414 项
$NODE tools/audit-values.js # 期望:AUDIT_CLEAN
build.js 自带的两道检查(写盘前跑):
new Function(src))remove( / call( 等会命中 MCP 黑名单的调用)界面侧还有:入口首行 "ui";、确实加载了产物、无 TODO/FIXME 标记。
adb connect <你的云机>:<端口>
# 引擎(界面启动时用这个路径)
adb push tiktok-auto/dist/tiktok-auto.js \
/sdcard/Scripts/tiktok-demo/tiktok-auto/dist/tiktok-auto.js
# 界面
adb push tiktok-app/dist/tiktok-app.js \
/sdcard/Scripts/tiktok-demo/tiktok-app/dist/tiktok-app.js
# 启动界面
adb shell "am start -n org.autojs.autoxjs.v7/org.autojs.autojs.external.open.RunIntentActivity \
-d file:///sdcard/Scripts/tiktok-demo/main.js"
⚠️ 不要用 MCP 下发脚本(120 秒上限,长脚本会被截断)。用上面的
am start。
build.js 会写时间戳,所以每次构建的 md5 都不同——不能拿"md5 变了"当失败。正确做法:
md5sum dist/tiktok-auto.js
adb shell "md5sum /sdcard/Scripts/tiktok-demo/tiktok-auto/dist/tiktok-auto.js"
# 两边一致才算推上去
adb logcat -c # 先清
adb shell "am start -n ... -d file:///sdcard/Scripts/xxx.js"
adb logcat -d | grep -E "\[TTA\]|\[app\]" # 引擎 / 界面
# 或者读结构化日志(更好)
adb shell "cat /sdcard/tiktok_app/run-log.jsonl"
| 规则 | 后果 |
|---|---|
| 新增模块前查重名 function | 单文件拼接,同名 function 静默互相覆盖(真出过:keysOf 覆盖了 store 的,把卡密读坏,全程零报错) |
| 检查命令 | for f in src/*.js; do grep -o '^function [a-zA-Z_]*' "$f"; done \| sort \| uniq -d |
入口 "ui"; 必须第一行 |
否则以非 UI 模式执行 + 一堆不着边际的报错 |
| 规则 | 说明 |
|---|---|
默认值只在 Store.FIELDS 定义 |
别处硬编码 = 下次改默认值时漏改 |
取值只用 getEnum/getNumber/getText/getBool |
直接读 storage 会绕过类型归一 |
| 一项一控件 | 复合项 → 两行;区间 → 一行两项(W.rangeRow,联动只刷兄弟那格) |
输入框只有 W.inlineField 一个入口 |
它负责 writing 标志(try/finally 复位)、数字框退格不写、失焦提交 + 正规化回写 |
commitPendingInputs() 放在 renderContent() 与 currentTask() |
否则"输入完直接点运行"会丢最后一次编辑 |
| 不定长列表用独立 storage key | 不进 Store.FIELDS(那是定长配置表) |
| 加枚举 → 两处一起改(FIELDS 与控件) | 只改一处 = 选项点了没反应 |
U.spacer() 不能用于横向布局;不用 weight=1 撑标签 |
布局塌 |
图标只用 U+25xx 区段 |
其他字符在设备上可能是豆腐块 |
UI 模式下禁止阻塞式 dialogs.* |
会静默返回 undefined,什么都不弹 |
| 规则 | 说明 |
|---|---|
所有等待都走 wait.* |
只有它会在 tick 里检查停止信号,业务代码才能零改动支持停止 |
不允许 sleep() |
同上,sleep 不可中断 |
| 模块之间不在加载时取引用 | 一律在函数体内按名字取(TikTokAuto.xxx.yyy),所以模块顺序不影响运行 |
平台怪癖只写在 layout.js |
散落到业务里就无法统一适配 |
每条格式:现象 → 根因 → 正解。 这一节里 90% 的坑都「不报错」,只是结果错。改代码前扫一遍你这节要动的部分。
A1|同名 function 静默覆盖
build.js 是单文件拼接,所有模块共享顶层作用域sourcesKeysOf 而不是 keysOf)A2|build.js 每次都改 md5
B1|app.launchPackage() 会重置页面(★ 最容易误判)
launchPackage 不是"切到前台",而是"重新打开 App"——即使已在前台也会再发 startActivity,把叠着播放页/评论面板清掉正解:
if (foregroundPackage() === pkg) { return true; } // 已在前台,什么都不做
app.launchPackage(pkg);
B2|Activity 关闭 ≠ 脚本结束
:script 进程存活),悬浮球浮在桌面上:script 进程的线程里,Activity finish 不终止脚本events.on("exit") + 轮询 activity.isFinishing()/isDestroyed() 兜底清理(见 main.js 的 bindLifecycleCleanup)B3|overlay 窗口归创建它的进程所有
dumpsys window windows 里窗口名只写包名,看不出属主进程,容易误以为"主进程持有"正解:想确认残留就用窗口计数
adb shell "dumpsys window windows | grep -cE 'Window\{.*autojs'"
# 启动界面约 6(界面 3 + 球 3);只剩球 3;干净 0
B4|http.get 在这个 AutoX 版本是坏的
TypeError: Cannot find function newCall in object ib.z@...正解:用 Java HttpURLConnection,并区分正常流/错误流:
var stream = code >= 400 ? conn.getErrorStream() : conn.getInputStream();
只用 getInputStream() 的话,4xx 会抛异常,把「404 账号不存在」误判成「网络错误」
B5|engines.execScriptFile 的参数会变成对象
argv[0] 取不到值arguments: ["--config", path] 传进去变成 {"0":"--config", "1":...}argv[i] 取,不要当数组用B6|子脚本看不到父脚本的全局变量
B7|threads.shutDownAll() 不波及父脚本的 timer
StopSignal(见 2.4),不要指望关线程B8|UI 回调里的异常不报错、不弹窗、不结束脚本
try/catch 并留痕,否则"事件到了、代码没执行、什么都没有"B9|setOnClickListener(null) 会吃掉点击
null 占位B10|键盘弹起会让整个界面位移
keyevent 111;跑任务时不要 uiautomator dump(会撞掉无障碍服务)B11|悬浮球必须是两个固定几何的窗口
WindowManager(floaty 有去不掉的 12dp 内边距、同时只能绘制一个);必须带 FLAG_LAYOUT_IN_SCREEN;球窗口要后加(同类型按加入顺序叠放)B12|requestScreenCapture(false) 是静默的
CAPABILITY_CAN_TAKE_SCREENSHOT,申请当场返回 true,不弹 MediaProjection 框captureScreen() 会抛 No screen capture permissionC1|settle()(waitQuiet)会提前返回(★ 高频)
正解:凡是"点完要等新内容",判据必须是"新内容真的出现了"
waitFor('rows', () => rows().length > 0 ? true : null, timeout, tick)
C2|数字会先渲染成占位的 0(★)
78 和 0C3|两种"用户列表页"的行结构不同
| 页面 | uid 在 | 行解析器 |
|---|---|---|
| 粉丝/关注列表 | TextView.txt_desc |
userListRows() |
| 搜索结果用户页 | TextView.tv_username |
usersResultRows() |
userRows 就绪 和 listRows 超时 同时出现profile.listRows() 写死成粉丝列表那个解析器isListPage() 同时认两种页)C4|点列表行不能用 node.click()
C5|用户名被 FID 隔离符包裹
\u2068 / \u2069(肉眼不可见),layout.sanitize 不剥(那不是零宽字符)parse.username() 里单独剥 [\u2066-\u2069\u202a-\u202e],再剥 @C6|这一片的资源 id 是语义化的
e4g/cn6)完全不同,可以直接按 id 定位(见第 8 节)C7|用户页没有"视频数"
C8|评论区只有昵称、没有 uid
C9|头像只能靠像素法
ImageView 无 text / 无 desc / 不可点0.040、真头像 0.600,阈值 0.12C10|首次打开粉丝列表会弹 Facebook 授权窗
Don’t allow(撇号是 U+2019),会盖住整个列表openList 里专门点掉它再等一次C11|找入口失败要重试,不要立刻报错
statEntry() 返回 nullC12|点赞按钮与分享按钮共用同一个 resource-id
g8g,只有 desc 不同Like video. 开头;已赞 desc 变成 Video liked、且里面 ImageView 的 selected=true
(两个判据都留:desc 受本地化影响,selected 不受文案影响)C13|关注按钮"消失"就是已关注
Follow <作者名> 这个 Button 整个不存在了(不是变成 Following)<作者名> profile)的存在性做交叉判断:
头像在 + 按钮不在 = 已关注;两个都不在 = 页面还没好Following 页签是 FrameLayout,限定 className('android.widget.Button') 就能排除C14|卡片"看得见"要看容器,不能看文案(★ 2026-10-28,这条让 cat 从 0 个变成 2 个)
cat 报「结果页已无更多卡片」,但页面上明明有卡片;fashion 正常[8,1242]→[536,1920+])。
结果页上只要多一条通栏广告或一行筛选 chips,网格被推下去,文案就落到屏幕外(实测 y=2099 > 1920)。
旧判据 bottomOf(node) <= H - 12 于是把这些看得见、点得着的卡片全丢掉layout.center 用 visibleRect 夹取,永远在屏内cardContainer 的高度上限从 80% 放宽到 105% —— 卡片贴边时 bounds() 会被裁切得又大又怪,
卡在 80% 上会把整排卡片判成"没有容器"C15|结果页混着三种"长得像视频卡"的东西
| 类型 | 点开之后 | 处理 |
|---|---|---|
| 广告视频 | 正常播放页(带 Ad + Shop now) |
正常看,能点赞/关注 |
| 照片轮播帖 | 照片查看器,没有播放页 | 用信息行的 Photo 标记提前判掉(约 2 秒,别等满 20 秒) |
| 商品/带货卡 | 商品详情页(Add to cart / Buy Now) |
打不开就当"非视频"跳过 |
Photo · 1w ago」两个独立 TextView,视频帖是「Video · …」;
限定文案完全等于 Photo 且在下半屏,避免误伤正文里出现 Photo 的视频errors(否则"4 个视频里有 1 张照片帖"看起来像故障)C16|浮层按钮会冒充卡片
Customize Settings / Confirm My Choices 被当成卡片点开,白等 20 秒GridView/RecyclerView 里(insideResultGrid())
② 必须能找到"卡片尺寸"的可点容器(找不到时 cardContainer 会退回节点本身,那就不是卡片)D1|阶段性的"停止"不能共用(★ 踩了两次)
shouldStop() = limitReached() || timeUp(),而抓取阶段的"够了/到点了"与收尾阶段的"必须做完"是相反的要求D2|不要在入口做全局的页面清理
runSession 一启动就 ensureFeed 把页面退回首页,而各取材源自己会导航anchor/search 走 prepareSearch(内部有 ensureFeed);screen/video(screen) 不动页面;text 不需要 UID3|深度展开要分两阶段
D4|"读不到资料" ≠ "不符合"
D5|"不限"必须有物理护栏
limit = 0 的语义是"不限",但没有上限等于跑到没电D6|时长预算要按"个数"反算,不能拍一个固定值
预算 = clamp(max(用户设的下限, limit × 20秒 + 5分钟), 4小时)
count × 3秒 + 1分钟D7|点"已选中"的页签会让列表重刷(★)
resultTab 也是 Top —— 等于每次都点一次当前页签。
TikTok 会因此重刷整个网格,而 waitQuiet 判不出"正在重刷"(签名和点之前一样)resultTabSelected() 已经是目标页签就跳过点击;真要切页签时,判据必须是"卡片重新出现了"D8|去重只按文案会死循环
swagger #fashion … vs … #fashion #fyp),
按 caption 去重认不出是同一张;而 chooseCard 还有"全都试过就退回全量再挑"的兜底 —— 那就是死循环入口| 编号 | 现象 | 正解 |
|---|---|---|
| E1 | 改了默认值但界面没变 | 默认值只在 Store.FIELDS;改完看是否需要版本迁移 |
| E2 | 控件改完重启又变回旧值 | 检查是否漏了 commitPendingInputs,或字段路径拼错(拼错 = 静默丢值) |
| E3 | 某个颜色突然没了 | Theme 令牌缺键 = 静默的视觉删除 |
| E4 | 弹窗点"确定"后界面错乱 | 弹窗"确定"不要预先关窗,先执行动作再关 |
| E5 | 新加的枚举选项点了没用 | 枚举要两处一起改(FIELDS + 控件) |
| E6 | 空串默认值补不上 | 只有版本迁移能补(Store.MIGRATIONS) |
| E7 | 函数返回值少一个字段 | 会静默错配,调用方拿到 undefined 继续跑 |
三级降级,每条只探一次(第一个账号证明不可用后,本轮不再试):
| 级 | 路径 | 2026-10-20 实测 |
|---|---|---|
| ① | www.tiktok.com/api/user/detail/?uniqueId= |
200 但响应体为空(风控静默拒绝,最容易被误判成"接口还好") |
| ② | www.tiktok.com/node/share/user/@handle |
403 |
| ③ | www.tiktok.com/@handle 整页 SSR |
✅ 200 / 约 388KB / 约 2 秒 / JSON.parse 仅 7ms |
走不通的路(别再试):
| 路径 | 结果 |
|---|---|
tikwm.com/api/user/info |
Cloudflare 挑战页 |
m.tiktok.com/@user |
404(那条短链只认 /v/<videoId>.html) |
www.tiktok.com/oembed |
只有昵称与 handle,没有数字 id |
AutoX 的 http.get |
本机坏的(见 B4) |
三个概念不要混:
| 例子 | 界面上可见 | 用途 | |
|---|---|---|---|
| uniqueId / handle | beautifulsoul78869 |
✅ | 能搜到人、能 @ |
| nickname | 🌹Beautiful Soul💀 |
✅ | 显示名,可重名可改 |
| id(数字 uid) | 254801657882882048 |
❌ | 平台主键,对接/入库要它 |
store.js 的 FIELDS 加一条(唯一默认值来源)page-*.js 加控件(输入框只用 W.inlineField / W.numberField)engine.js 的 buildConfig/buildIdConfig 加映射tools/smoke.js 补断言:
ID提取功能设置是 23 个字段)$NODE tools/build.js && $NODE tools/smoke.js第 4 步的"映射断言"很关键:只断言字段数不变,映射写错了照样全绿。
idsource.js 里加分支,先判断前置状态walkListctx.onRowctx.onRow,不要在源里自己做(否则 duplicates 统计永远是 0)sources.js 加对应 kind + page-id.js 加选项pc-selftest-id.js(纯函数部分)与 smoke.js(映射部分)_probe/probe-dump.js 在真机上 dump 当前页面tiktok.js 里对应的 *Rows() / *Node()这一片的 id 是语义化的,和信息流那批混淆 id 完全不同。
| 页面 | 控件 | 内容 |
|---|---|---|
| 用户主页 | Button 文案 @xxx |
uid/handle(被 \u2068/\u2069 包裹) |
TextView 两两成组(t_r 数字 / t_q 标签) |
Following / Followers / Likes | |
Button(长文本) |
个签 | |
| 粉丝/关注列表 | TextView.txt_desc |
uid |
TextView.txt_user_name |
昵称 | |
顶部三页签 Following N/Followers N/Suggested |
可直接切,不必退回主页 | |
| 搜索用户列表 | TextView.tv_username |
uid |
TextView.tv_aweme_id |
昵称(id 名违反直觉) | |
TextView.tv_desc |
594 followers · 2.5K likes(行内就有计数) |
|
| 播放页 | 底部 Button 文案 @xxx |
作者 uid |
| 播放页·点赞 | Button desc Like video. 2.4M likes |
未赞;里面 ImageView selected=false |
| 播放页·已赞 | 同一个 Button desc → Video liked |
ImageView selected=**true**(不是 Unlike) |
| 播放页·关注 | Button desc Follow <作者名> |
已关注时整个按钮消失(不是改文案) |
| 播放页·作者头像 | ImageView desc <作者名> profile |
用来区分「已关注」与「页面没渲染完」 |
| 评论区 | Button.title |
只有昵称,没有 uid |
ImageView.blm(约 72×72,左侧) |
评论者头像(可点进主页) | |
| 用户主页头像 | ImageView 192×192 @ [856,168][1048,360] |
无 text/desc/不可点 → 像素法 |
三条导航事实:
性能实测(决定取材怎么设计):
| 模式 | 速度 | 说明 |
|---|---|---|
| 不过滤 | ~1 秒/个 | 直接从列表抄 uid,不开用户页 |
| 开过滤 | ~8 秒/个 | 每个候选都要开用户页读资料 |
| 评论者 | ~5~8 秒/个 | 只有昵称,必须点头像进主页 |
| 换数字 uid | ~2.5 秒/个 | 要下整页 SSR |
| 项 | 情况 |
|---|---|
| 无头像判定 | 像素法,纯黑白照片头像会误判,运行时已告知 |
| 视频量门槛 | TikTok 用户页不显示视频数 → 放行 + 告知 |
| 提取区域(国家) | 用户页不显示国家,没有可靠数据源 → 不做 |
| AI 画像(性别/年龄) | 需要模型 → 不做 |
| 云端去重 | 服务端未接入 → 降级本地 + 告知 |
| 登录态 | 对抓取零贡献,相关代码与配置项已删,不要重新引入 |
| 暂停/恢复 | AutoX 的 ScriptEngine 只有 forceStop(),没有 pause → 只能协作式实现。已决定不做(2026-10-28),不要再实现,见 PLAN_pause_resume.md |
| 单跑 | 同一台手机物理上无法并发操作一个 App |
:script 进程 |
界面退出后进程会留着空转(AutoX 自己的行为),不影响使用 |
| 症状 | 先查 |
|---|---|
| 界面白屏 / 一堆怪错 | main.js 第一行是不是 "ui";;main.js 是否真的 require 了产物 |
| 点「开始运行」被弹窗拦下 | 校验顺序:卡密 → 权限 → 已选脚本 → 引擎是否接入 → 是否在跑 |
| 引擎起来了但页面全 unknown | TikTok 不在前台(引擎必须待在前台) |
| 取材 0 行 | ① 页面对不对(screen 源要求停在列表/评论区)② 解析器是否分派对了(C3)③ 首屏是否还没加载完(C1) |
| 数量读成 0 | 占位值(C2) |
| 该收的没收 | 过滤条件 + 日志里的"跳过 @xxx:原因" |
| uid 没生成 | 是否被 limit/timeUp 掐断(D1);看日志最后三行 |
| 悬浮球残留 | 数窗口(B3);后台保留 是设计,彻底退出用「我的」→ 退出应用 |
| 推送后行为没变 | md5 双向核对(4.3);是不是推错了路径(界面 vs 引擎) |
读日志的正确姿势:优先读 /sdcard/tiktok_app/run-log.jsonl(结构化),比 logcat 干净。
Store.FIELDS,别在页面里硬编码wait.waitFor 且判据是"新内容出现",不要用 settle 等"安静"ctx.shouldStop() 或 wait.tick()null,由上层决定拦还是放行# 引擎改动
cd tiktok-auto && $NODE tools/build.js && $NODE tools/pc-selftest-id.js
# 界面改动
cd tiktok-app && $NODE tools/build.js && $NODE tools/smoke.js && $NODE tools/audit-values.js
说清楚三件事:
| 词 | 含义 |
|---|---|
| 取材源(source) | 从哪儿找人:指定账号 / 搜索结果 / 某个视频的评论区 / 文本文件 / 当前屏幕 |
| uid(本项目语境) | 通常指 handle(@xxx);"数字 uid" 会明确写"数字" |
| 收录 / 跳过 / 重复 | 通过过滤 / 被过滤 / 已在去重集合里 |
| 深度展开 | 对已收到的人"再取他的粉丝列表",最多 2 层 |
| 收尾阶段 | 抓取完成后的工作(当前是:username → 数字 uid) |
| 护栏 | 物理上限(时长、翻屏数),防止"不限"跑到没电 |
| 文件 | 内容 | 状态 |
|---|---|---|
PLAN_id_extract.md |
ID 提取执行方案 | 已实施 |
REQ_id_extract_v2.md |
三段式需求重整(38 字段的逐条处置) | 已实施 |
PLAN_comment_completeness.md |
评论完整性 | 未实施 |
PLAN_pause_resume.md |
协作式暂停 | 已取消(用户决定不做,只作历史记录) |
README.md / tiktok-app/README.md / tiktok-auto/README.md |
各模块详细设计与实测记录 | 持续更新 |
冲突时以本文档为准(它是最新实测结论的汇总)。