✅ 状态:已实施并在真机验证过(
task = "id",六模块 + 三段式界面)。 本文件保留作为设计依据;实现取舍与实测锚点见tiktok-auto/README.md第 3.5 节。范围:
tiktok-app(界面)↔tiktok-auto(执行引擎)两端。 目标:让「任务 › ID提取」从只有页面变成点了运行真的会去 TikTok 里抓 ID。 依据:《实施规格书_飞舟脚本_2026-10-13》(资料库JZf9xTgLXBFgjXJ4dJWVsU)。 现状核对时间:2026-10-15;设备 TikTok47.1.4(Pixel 5a / 1080×1920 / Android 14)。
界面这一侧已经做完了,缺的全在执行侧。 四个脚本卡的 40 个配置字段、单选互斥、 运行态、停止、记录、日志回传通道全都现成;真正没有的是「拿着这些配置去 TikTok 里干活」 的那套流程。所以本方案的重点不是页面,而是新增一条与「关键词视频」并列的执行链路, 以及为它补上引擎目前完全没有的三类能力:用户页读取、列表取材、特征过滤。
| 能力 | 位置 | 状态 |
|---|---|---|
| 四张脚本卡 + 单选互斥 | tiktok-app/src/page-id.js Pages.toggleIdScript |
✅ |
| 功能设置 14 项(40 个字段) | store.js → tabs.id.features.* / tabs.id.scripts.s*.* |
✅ 全部落盘、可回读 |
| 运行前置校验 5 步 | main.js App.requestRun |
✅ 顺序照规格书 §2.5 |
| 界面↔引擎文件通道 | engine.js:engine-config.json / run-state.json / stop.flag / run-log.jsonl |
✅ 已真机验证 |
| 停止(可中断全流程) | 引擎 bridge.js 的 StopSignal 从 wait.tick() 抛出 |
✅ |
| 进度显示 | 引擎 report({phase}) → 界面状态条 |
✅ 只看 phase 文案,done/total 不显示 |
| 运行记录 / D3 详情 / 结果文件行 | records.js(resultText / resultFile / logs) |
✅ 字段已留好 |
| 缺口 | 说明 |
|---|---|
| ① 脚本没声明引擎 | Pages.ID_SCRIPTS 四条都没有 engine 字段 → 点运行被 Dialog.engineMissing 拦下。page-video.js 里是 engine: "tiktok-auto" |
| ② 界面配置构建器只认视频页 | Engine.buildConfig() 写死读 tabs.video.*;Engine.prepare() 里那条「关键词为空就拦」也只对视频成立 |
| ③ 引擎只有一条业务链路 | tiktok-auto/src/flow.js 的 runSession = 搜索→挑视频→看→评论。没有用户页、没有列表、没有 ID 概念 |
④ pageState() 认不出用户页/列表页 |
现在只有 feed / search / results / player / comment-panel / unknown |
| ⑤ 没有产物落盘 | 规格书 §5 要求结果是 uid_YYYYMMDD.txt;引擎现在只发评论、不写业务文件 |
| ⑥ 没有去重集合 | 「主播去重 / UID 博主去重 / 云端去重」三项都没有承载者 |
规格书只描述了 UI,没说"四个脚本到底在做什么"。把四个脚本的设置逐项读一遍, 能看出它们其实只在回答同一个问题:候选 ID 从哪儿来。
一切 ID 提取 = 取材源(一个或多个) → 候选用户 → 打开用户页 → 采集 → 过滤 → 收录
↑ ↑
四个脚本的唯一差别 14 项功能设置在这里生效
四个脚本各自的设置项,逐条落到「取材源」上:
| 脚本 | 卡里的设置 | 对应取材源(Source kind) |
|---|---|---|
| 一 · 粉丝页提 ID | 进入页面(粉丝页/评论页/主页/关注页) | listOf(anchor, enterPage):fans / following / comment / home |
| 自定关注或粉丝页面提 ID | 是否走「作者主页 → 切页签 → 列表」这条入口 | |
| 自定页面提 ID | userSearch(keyword) —— 直接在搜索页的 Users 页签里抓 |
|
主播 ID(# 分隔,可留空) |
取材起点;留空 = 从版面现场取(见 §2.3 的歧义) | |
| 使用文本 ID(路径) | fromText(path) —— 绕过取材,直接把文本里的 uid 当候选 |
|
| 主播去重(去重模式) | 不是取材源,影响的是收录阶段(见 §6) | |
| 二 · 评论页提 ID | 自定评论页面提 ID | commentersOf(video) |
| 提取评论的人的粉丝 ID | 上一步的候选再展开一层:fansOf(commenter) |
|
| 三 · 文本 UID 裂变 | 文本 UID 裂变提 ID + 裂变文本路径 | fromText(path) → 每个 uid 展开 fansOf/followingOf = 裂变 |
| UID 博主去重 | 收录阶段 | |
| 四 · 关键词视频 | 关键词 + 关键词视频提 ID | searchVideos(keyword) → 视频 → 作者 |
| 关键词提取主播粉丝 ID | 作者再展开 fansOf(anchor) |
|
| 关键词提取评论区 ID | commentersOf(video) |
|
| 关键词提取评论的人的粉丝 ID | 再展开一层 |
s1: anchors = 主播ID(或现场) ; 候选 = listOf(anchors, enterPage) | userSearch(kw)
s2: 候选 = commentersOf(视频) [+ fansOf(每个评论者)]
s3: 候选 = fansOf/followingOf(fromText(裂变文本里的 uid))
s4: 候选 = authorsOf(searchVideos(关键词)) + fansOf(作者) + commentersOf(视频) + fansOf(评论者)
fansOf / followingOf / commentersOf / searchVideos / searchUsers / fromText
六个取材源覆盖全部四个脚本,且每一个都是「返回一串 uid + 滚动翻页」的同一形态。
这条抽象是整个方案的地基:如果它成立,四个脚本只需 6 个取材源 + 1 条公共流水线;
如果不成立(比如四个脚本的翻页行为互不相同),工程量会翻好几倍。 所以 §11 的第一条探测
就是来验它的。
listOf 的页面判定,
判定不到就报错说清楚"请先进入某个列表页或填写主播 ID"。customFansPage=true
且 enterPage=fans),无法从配置区分"从主页进"与"直接在粉丝页列表上抓"。
我的解释:customFansPage = 是否先打开主播主页再切到对应页签;
关掉 = 假定用户已经在那个列表页上,直接抓。fromText 要换成
searchUsers。这两种在设置项文案上都说得通,必须确认。idflow.runSession)与现有 flow.runSession 并列,同一份基础设施(应用启动、弹窗守护、坐标点击降级、
wait.tick() 的停止、bridge.report() 的状态上报)。
runSession(config):
report(phase="启动应用") ← 复用 flow.launchApp
report(phase="进入信息流") ← 复用 flow.ensureFeed
if !ensureFeed: fail("未能进入信息流首页")
sources = buildSources(config.id) ← §2.2 的六种,按脚本组合成一个队列
seen = UidSet.load(config.id.dedupe) ← §6
out = ResultWriter.open(config.id.output)
for source in sources: ← 取材源按顺序(不是并发:同一台手机只有一个前台)
for batch in source.batches(): ← 每次翻一屏
for uid in batch:
if uid in seen: continue
if matchesTextIdMode(uid): continue
report(phase="正在读取 @{uid}(已收 N 个)")
profile = profile.read(uid) ← §4
verdict = filters.judge(profile, config.id.filter) ← §5(纯函数)
if !verdict.ok: log("跳过 @{uid}:{verdict.reason}"); continue
seen.add(uid); out.write(uid)
if limit && out.count >= limit: goto done
if !source.next(batch): break ← 列表到底了
← 一个取材源耗尽就换下一个,不是失败
done:
out.close() ← 写 /sdcard/Download/tiktok_app/uid_YYYYMMDD_HHmm.txt
UidSet.save(seen)
finish(status="finished", summary={extracted: N, skipped: M, resultFile: "…"})
三个与视频链路的真实差异,必须在实现时处理:
total 不存在。处理:
report({phase}) 里带「已收 N 个 / 上限 M(0 = 不限)」,
界面状态条只读 phase(已确认 main.js:574),所以界面零改动。extractLimit = 0 的语义是"不限制"(store.js 里写明),
但真跑起来没有上限就等于"跑到没电"。处理:保留界面语义,引擎另加两个物理护栏:
单次运行的最大时长(runtime.maxRuntimeMs,默认给一个值如 30 分钟)与
单列表最大翻屏数。到护栏时如实报告「因达到取材上限而结束」,不偷偷装作抓完了。StopSignal 从 wait.tick() 抛出已覆盖,但收尾必须写文件 ——
收到停止信号时要走 out.close() 再抛,否则用户"停了但结果文件是空的"。src/profile.js)「ID」在 TikTok 里就是用户名(@username)。要在用户页上读到的东西:
| 字段 | 用途 | 数据源 | 可行性 |
|---|---|---|---|
uid |
产物本体 | 用户页顶部的 @xxx 文本控件 |
待实测(控件位置/文案格式) |
nickname |
昵称检测 / 语言判定 | 昵称 TextView | 待实测 |
signature |
个签检测 / 语言判定 | 个签 TextView | 待实测 |
follow / fans / likes / videos |
四组数量门槛 | 用户页顶部四组「数字 + 标签」 | 待实测(数字是缩写形态,见下) |
isPrivate |
过滤私密账户 | 私密账号的提示文案 | 待实测 |
hasAvatar |
过滤无头像账户 | 头像 ImageView | 待实测(可能要像素法,见下) |
region / flag |
提取区域 | ? | 无可靠数据源,见 §5.3 |
avatarBitmap |
AI 头像识别 | 头像 | 需要模型,见 §5.4 |
数字缩写解析是一个独立的小模块(parseCount("1.2M") → 1200000),
必须覆盖 TikTok 的多种写法(1.2M / 12.3K / 1,234 / 1.2万)
并且语言/地区不同写法不同。这是纯函数,可以 100% 离线自检 —— 值得单独写测试。
无头像的判定很可能要像素法(头像位是灰色占位图):
player.js 里已经有 probe.usePixels 那套截图取像素的基础设施可以复用。
这是方案里唯一一处"功能设置需要像素"的地方,会是探测清单里的重点。
规格书只给了 UI,没说怎么实现。逐项给出能做什么、靠什么做、做不到怎么办。 分三级:A = 这次能做;B = 能做但依赖 §11 的实测结论;C = 本次不做,运行时如实告知。
| 项 | 实现方式 |
|---|---|
| 过滤私密账户 | 用户页/列表页读到"私密"提示 → 丢(B 级的实测给具体文案) |
| 关注量 / 粉丝量 / 获赞量 / 视频量 区间 | 读数字 → parseCount → 落区间判定。纯函数,可离线全量自检 |
| 提取符合 N 个 ID 后停止 | 计数上限(0 = 不限,但受 §3 的物理护栏约束) |
| 语言区域(通杀/仅中文/仅英文) | 按昵称 + 个签的字符集判定:CJK 字符占比 → 中文;拉丁字母占比 → 英文。纯本地、零依赖、可自检 |
| 昵称个签检测(通杀/仅检测昵称/仅检测个签) | 我理解为上面各项判定的取样字段:通杀 = 昵称+个签都看;仅昵称 = 只看昵称;仅个签 = 只看个签。⚠️ 这条解释需要确认(见 §14 待拍板第 3 条) |
| 主播去重 / UID 博主去重 | 收录阶段的 UidSet(§6) |
| 云端去重 | 见 §5.5 |
| 项 | 依赖的实测结论 |
|---|---|
| 过滤无头像账户 | 头像控件能否用文本/语义判定;不行就走像素法(player.js 的像素基础设施) |
| 进入页面(粉丝页/评论页/主页/关注页) | 用户页的页签条锚点与实际标签文案 |
| 提取数量 / 列表翻页 | 列表页的滚动容器、每屏行数、返回后位置是否保持 |
实话:TikTok 用户页不显示国家。 所以这一项没有可靠数据源。三条可选路径:
| 路径 | 覆盖率 | 依赖 | 说明 |
|---|---|---|---|
| (a) 昵称/个签里的国旗 emoji 或国家名匹配 | 低(只有写了的用户) | 无 | 纯本地可做,但大量用户会被判成"未知" |
| (b) 与「语言区域」合并,用语言替代地区 | 中 | 无 | 语义被悄悄改变,不能这么干 |
| (c) 云端按 uid 查地区库 | 看库 | 服务端 | 要建/买一份 uid→地区 的数据,成本在服务端 |
| (d) 本次不做 | — | — | 界面上保留该项,运行时明确告知「本次不会按区域过滤」 |
推荐 (d) + 预留 (c):默认不做、如实告知;同时把 regionCodes 原样传进引擎配置,
将来 (c) 就绪时不用改界面。不要做 (b) —— 那是把一个"看起来生效"的假功能交给用户。
需要人脸检测 + 性别/年龄模型。三条路:本地 TFLite(要确认 AutoX 能否加载模型、 模型从哪来)、云端 API(要服务端 + 上传头像,涉及隐私与成本)、不做。
推荐本次不做,理由:这三项在 14 项里最边缘,缺失不影响主链路(取材→过滤→产物完整可用),
而"为了凑齐设置项去接一个没验证过的模型"风险远大于收益。
落地方式与现有的「点赞/关注/私信尚未实现」完全一致:Engine.unsupportedNotes() 逐条如实告知,
用户勾了就在启动时提示一次 + 记进 run-config.json 的 notes。
(现有的 unsupportedNotes() 是写死看视频页的,改造成按 task 分派时顺手支持 id。)
现状(page-id.js 的"连通测试"按钮)已经如实说「当前未配置云端去重服务地址」。
所以本方案要的是接口契约 + 明确的退化行为:
POST {cloudBaseUrl}/dedupe/check
body {"table":"<表名>","uids":["a","b",...]} ← 批量,减少往返
resp {"new":["a"],"dup":["b"]} ← 只保留 new
cloudBaseUrl → 只用本地 UidSet,并在运行日志里写一行
「云端去重未配置,本次仅本地去重」。不静默。cloudBaseUrl 字段(现在只有表名)。| 文件 | 内容 | 说明 |
|---|---|---|
/sdcard/Download/tiktok_app/uid_20261015_1831.txt |
本次提取的 uid,一行一个 | 规格书 §5 写的是 uid_20261013.txt(只有日期)。我建议加时分:同一天跑两次不会互相覆盖 —— 这是对规格书的有意识偏离,理由与代价都很清楚 |
/sdcard/Download/tiktok_app/uid_extracted.txt |
累计已提取过的 uid(去重基准) | 跨轮去重靠它。只追加、不删除(files.remove 命中引擎的危险调用黑名单,见 build.js 的 DANGEROUS_RULES) |
run-config.json |
完整配置快照 | 已有通道,ID 提取的配置也走它(追溯用) |
run-log.jsonl |
逐行日志 | 已有通道 |
产物格式细节:纯文本、一行一个、\n 结尾、不带 @(下游 UID 私信页直接读它当输入,
uid_list.txt 那套约定就是纯 uid)。要不要写表头注释行?不写 —— 下游是逐行读的,
写 # 注释要靠每个下游都记得跳过(readLines 现在确实跳 #,但别赌)。
src/uidset.js)uid_extracted.txt(可能几十万行)→ 建 Set。内存里是 Set,落盘是追加。dedupe: text)与 ID 模式的差别:文本模式是按
textIdPath / fissionTextPath 那份输入文本去重,即"输入里已经有的就不算新"。
两种模式的集合来源不同,实现上是同一个 UidSet 的两个装载源。tools/build.js 的 MODULE_ORDER)| 新文件 | 职责 | 行数估计 |
|---|---|---|
src/profile.js |
用户页资料采集(§4) | ~250 |
src/sources.js |
六个取材源 + 列表翻页(§2.2) | ~350 |
src/filters.js |
14 项判定的纯函数实现(§5) | ~250 |
src/uidset.js |
去重集合 + 结果文件读写(§6) | ~180 |
src/idflow.js |
ID 提取编排(§3) | ~300 |
| 文件 | 改动 |
|---|---|
src/tiktok.js |
① pageState() 新增 profile / user-list / users-results 三个状态;② 新增用户页锚点、列表锚点、页签锚点;③ 新增 usernameOf / statNumbers / privateHint / avatarNode 读取 |
src/config.js |
DEFAULTS 新增 id 段(§8 的契约)、task 字段、timeouts 加用户页/列表相关的上限 |
src/main.js |
入口按 config.task 分派:video → flow.runSession,id → idflow.runSession;__END__/RESULT/终态上报的逻辑两路共用(不能各写一份) |
src/ui.js |
列表翻页需要 scrollList(现有 dragScreen 够用就复用,不新增) |
src/__selftest.js |
新增 parseCount / filters.judge / uidset 的纯函数自检 |
tools/build.js |
MODULE_ORDER 插入新模块(顺序:filters/uidset 无依赖可早放,profile/sources 依赖 tiktok,idflow 放最后) |
引擎产物是单文件 concat,模块顺序由 MODULE_ORDER 决定,且各模块在加载时就取
TikTokAuto.xxx 的常量引用(var layout = TikTokAuto.layout;)。新模块必须遵守同一约定,
且不能在加载时调用别的模块的函数(那时可能还没定义)。
engine-config.json)新增顶层 task 字段用于分派,新增 id 段承载全部设置:
{
"task": "id", // 新增:"video" | "id";缺省时按 "video"(向后兼容)
"id": {
"script": "s1", // s1..s4,用于日志与记录
"source": { // ← §2 的取材源,字段随脚本给不同的子集
"kind": "fans", // fans|following|home|comment|userSearch|textFission|keywordVideo
"enterPage": "fans",
"anchorIds": ["123","456"], // 界面是 "123#456" 字符串,界面侧拆好再传
"userSearchKeyword": "",
"fissionTextPath": "/sdcard/Download/tiktok_app/uid_list.txt",
"keyword": "fashion",
"includeAnchorFans": false,
"includeCommentArea": false,
"includeCommenterFans": false,
"customFansPage": true,
"customPage": false
},
"useTextId": { "enabled": false, "path": "…" }, // 脚本一:绕过取材直接用文本
"dedupe": { "enabled": true, "mode": "id" }, // mode: id | text
"filter": {
"privateAccounts": true, "noAvatar": true,
"regionCodes": ["JP","TW","US"], // ← 传下去但本次不生效(§5.3),
// 但要写进 run-config 以便追溯
"langRegion": "any", "nicknameCheck": "any",
"aiAvatar": false, "aiGender": "male", "aiAgeMax": 20,
"follow": {"min":0,"max":500000}, "fans": {"min":0,"max":500000},
"likes": {"min":0,"max":500000}, "videos":{"min":0,"max":500000}
},
"limit": 0, // 0 = 不限(受 §3 的物理护栏)
"output": { "dir": "/sdcard/Download/tiktok_app" }
},
"runtime": { "configPath": "…", "dryRun": false, "maxRuntimeMs": 1800000 }
}
配置映射要做成"按 task 分派":
Engine.buildConfig(taskKey) → video: 现有逻辑原样搬成 buildVideoConfig()
→ id: 新增 buildIdConfig()(读 tabs.id.*)
Engine.prepare(taskKey) → 前置校验也按 task 分派
Engine.unsupportedNotes(taskKey)
⚠️ 改签名会碰到两处调用:main.js:855(Engine.prepare())与
tools/smoke.js:571(自检里直接调 Engine.prepare()) —— 自检要一起改,
否则离线检查会在这一步红掉(这是好事,说明它真的在守这件事)。
page-id.js:Pages.ID_SCRIPTS 四条的 name 后加 engine: "tiktok-auto" → 解掉 F「引擎尚未接入」拦截。engine.js:buildConfig / prepare / unsupportedNotes 三个函数按 task 分派(§8)。engine.js 的 buildIdConfig:读 tabs.id.* 40 个字段 → §8 的结构;同时
把 anchorIds 的 # 拆成数组、把区间字段组装成 {min,max}。Dialog.runBlocked / Dialog.engineUnavailable,不新增弹窗类型。main.js 的 App.startRun:把 task.task 传给 Engine.prepare()(一行);
收尾时把引擎 summary 里的 resultFile / extracted 写进记录
(Records.add 已有 resultFile 字段,不用改数据结构)。records.js 的文案:任务卡的「上次 …」现在走 resultLine,
引擎回传 resultText = "提取 128 个 ID" 即可与设计稿一致(上次 10:24 · 提取 128 个 ID)。不需要改的:状态条(只读 phase)、Session(done/total 不显示)、
四个脚本卡与功能设置的任何控件(字段已齐)、Page-id 的滚动结构。
| 项 | 规格书 | 代码 | 建议 |
|---|---|---|---|
| 脚本三「文本 UID 裂变提 ID」默认 | ✗ | true(store.js) |
待确认:若照规格书改成 ✗,脚本三展开后全为空,用户会以为坏了 |
| 脚本四「关键词视频提 ID」默认 | ✗ | true |
同上 |
两处都是"按规格书改会让页面看起来没内容"的冲突,与浮窗那次 「§10.1 说不透明度 1.0 / §10.9 又建议降透明度」是同一类问题。本方案不动它们, 只在这里记一笔,等用户拍板。
tiktok-auto/tools/diag-*.js 已经有一套成熟的探针模式(--eval 加载全部模块、
只换入口代码)。照这个模式写一个 diag-profile.js,一次性把下面 9 条测清楚。
每一条都给出「什么结果算通过」,探测结论直接决定 §2 的地基是否成立。
| # | 要测什么 | 通过判据 | 决定什么 |
|---|---|---|---|
| 1 | 用户页能否稳定读到 @username |
反复打开 10 个不同用户页,10/10 读到且格式一致 | 整个方案的地基 |
| 2 | 粉丝页/关注页列表结构与滚动 | 能读到列表每行的 uid;连续 3 次滚动都能拿到新行(不重复) | §2.2 的 listOf |
| 3 | 从用户页返回后列表位置是否保持 | 返回后原行还在原位(不用重新滚回) | 翻页策略;不保持则要"先收集一整屏再逐个开" |
| 4 | 用户页四组数字的控件形态 | 能定位到 关注/粉丝/获赞/视频 四个数字;拿到原始字符串(验证缩写形态) | §4 的 parseCount 与四组门槛 |
| 5 | 私密账户在列表页与用户页的可见特征 | 两者的 txt 能区分;列表页也能判(否则只能打开后判) | 过滤私密账户的效率 |
| 6 | 头像控件的可判定性 | 先看有没有语义/文本可判;不行则截图取像素,验证占位图与真头像的像素差异可量化 | 过滤无头像账户 |
| 7 | 搜索结果页 Users 页签的行为 | 点 Users 后出现的是用户列表(不是视频);能读 uid | 脚本一「自定页面提 ID」 |
| 8 | 评论列表里能否读到评论者的 uid | 无需进用户页就能读(否则每个评论者都要开一次页,成本翻倍) | 脚本二/四的效率 |
| 9 | 昵称/个签控件 + 中文占比判定 | 能分别读到昵称与个签;CJK 占比在中文/英文账号上确实分离 | 语言区域判定 |
探测产出:一份 _probe/ID_EXTRACT_PROBE.md(实测锚点表 + 结论 + 失败项的处理办法),
与 tiktok.js 头注释里那张"实测锚点"表同形 —— 引擎所有选择器都基于真机实测,
这是这个项目一直以来的做法。
| 阶段 | 内容 | 验收标准 | 能否独立回退 |
|---|---|---|---|
| P0 | §11 的 9 条真机探测 | 9 条都有明确结论;不成立的条目给出替代方案 | 只碰 _probe/,零风险 |
| P1 | 引擎基础设施:uidset / filters / parseCount(纯函数)+ __selftest + build.js 模块表 |
设备端 --self-test 全绿;parseCount 覆盖各种缩写写法 |
新增文件,不影响现有流程 |
| P2 | tiktok.js 扩展(用户页/列表锚点 + 三个新 pageState)+ profile.js 读取 |
能在真机上把某个 uid 的资料读成一行结构化日志 | 现有 pageState 加分支可能影响视频链路 → 必须回归跑一次视频流程 |
| P3 | sources.js + idflow.js(先只做脚本一一条取材源)+ 界面 6 处改动 |
真机:从粉丝页抓到 ≥10 个真实 uid,落盘成文件,界面记录里有「提取 N 个 ID」 | 界面加 engine 声明后即可跑;回退就是删掉声明,回到被拦状态 |
| P4 | 补齐脚本二/三/四的取材源 + 云端去重契约 + 停止收尾写文件 | 四个脚本各跑通一次;运行中点停止 → 文件里有已经抓到的部分 | 按取材源独立,出问题只关掉那个源 |
| P5 | 文档 + 记忆 + 自检补全(smoke 的 ID 提取断言、audit 的字段覆盖) | smoke 全绿 + audit CLEAN | — |
为什么 P3 只做脚本一:四个脚本共用同一条流水线,第一条打通就能验证 §2 的整个抽象。 如果脚本一跑通了,后面三个基本是加取材源(每个 ~80 行);如果脚本一就卡住, 说明抽象有问题,此时还没写另外三个的来源,改起来的代价小得多。
| 层 | 手段 | 覆盖什么 |
|---|---|---|
| 纯函数(PC + 设备) | tiktok-auto/src/__selftest.js 新增:parseCount 各种书写、filters.judge 各种组合、uidset 的装载/追加/去重 |
「14 项设置真的会拦人」这件事可以 100% 离线证明 |
| 界面(PC) | tiktok-app/tools/smoke.js 新增:buildIdConfig 的映射(40 字段 → 引擎配置)、id 的三条前置校验、unsupportedNotes("id") 的如实告知 |
配置不会在映射中丢字段(这个项目踩过"路径拼错 = 静默丢值") |
| 界面(PC) | tools/audit-values.js |
新增字段是否都有默认值、是否都能回读 |
| 真机(受控) | tools/diag-*.js 探针 + --dry-run(只取材、不写产物文件) |
选择器在真实 TikTok 上是否还准 |
| 真机(端到端) | 界面点运行 → 抓 10 个 → 校验产物文件「行数正确、无空白行、无重复、不带 @」 | 全链路 |
dryRun 的语义要新增一条:取材照做、但不写产物文件(现在 dryRun 只作用于"评论不发送")。
这样能在不污染产物、不消耗去重集合的前提下反复调选择器。
customFansPage = 先开主播主页再切页签?
③「裂变」= uid 一层展开(取这些人的粉丝/关注)?
—— 尤其 ③,两种解释的实现完全不同。uid_20261013.txt,我建议加时分(uid_20261013_1831.txt)。
要不要偏离?extractLimit = 0(不限)时,引擎给不给"最大运行时长 / 最大翻屏数"?
我建议给(默认 30 分钟),并在到达时如实说明。这个默认值要多少?| 项 | 原因 |
|---|---|
| 并发多个取材源 | 同一台手机只有一个前台,TikTok 是真机操作,没有并发可言(与"全局单跑"同源) |
| 绕开界面直接跑 | 界面是唯一入口(卡密 / 权限 / 单选 / 单跑都在这层),不给第二条路 |
| 用 API 抓 ID | 「ID提取」的语义就是"在真机 TikTok 上按页面取材";评论数那套 tikwm 链路是另一件事,不混进来 |
| 改四个脚本卡与功能设置的控件 | 40 个字段已齐、已落盘、已可回读,一行都不动 |
本期实现 regionCodes / AI 三项 |
见 §5.3 / §5.4,如实告知而不是做个假开关 |
| 动 §10 里那两处默认值 | 与执行无关,等拍板 |
界面(已完成) 引擎(本方案要做的)
┌────────────────────────────┐ ┌──────────────────────────────────┐
│ 4 张脚本卡(单选) │ │ main.js 按 task 分派 │
│ 功能设置 14 项 / 40 字段 │ │ ├ video → flow.runSession(已有)│
│ 运行态 / 停止 / 记录 / 日志 │ │ └ id → idflow.runSession ★新 │
└─────────────┬──────────────┘ └──────────────────────────────────┘
│ engine-config.json(新增 task + id 段) ↑
└───────────────────────────────────────────────┘
取材源(6 种)→ 用户页 → 过滤(14 项)→ 去重 → 产物
★tiktok.js 新增 profile / user-list / users-results 状态
依赖顺序:先跑 §11 的 9 条探测 → 再写 P1 的纯函数 → 再 P3 打通脚本一。 最大的不确定性不是工程量,而是 §11 第 1 条(用户页能否稳定读到 uid)—— 它不成立的话,整个"在真机上按页面提取 ID"的路线都要重新考虑。