✅ 状态:已实施。本文就是 v8 那次改造的依据(删 5 / 合并 7 / 改名 6 / 置灰 3 / 保留 17 与三段式结构都已落地)。保留作需求原始记录。
对象:
tiktok-app界面「任务 › ID提取」的四张脚本卡 + 功能设置。 目标:不再往上加功能,而是把已经存在但互相矛盾的需求理清,让每个控件都有唯一、可执行的语义。 依据:《实施规格书_飞舟脚本_2026-10-13》(资料库JZf9xTgLXBFgjXJ4dJWVsU)+ 现有代码实测。 配套:PLAN_id_extract.md(执行功能方案)。本文只谈要什么,不谈怎么实现。
乱不在控件多少,在于每个控件没有唯一的行为定义。
实测:ID提取共 38 个业务字段(+4 个 checked +4 个 expanded)。其中 10 个是"纯开关"
—— 勾上之后旁边没有任何输入或选择,执行时无从下手;而其中 4 个与它所在的卡片同名
("自定关注或粉丝页面提 ID"、"自定评论页面提 ID"、"文本 UID 裂变提 ID"、"关键词视频提 ID"),
等于"勾了这张卡之后再确认一次要不要用这张卡"。
四张卡也不是四个功能,而是「起点(人从哪来)」×「深度(要不要再展开一层)」 的 4 个不完整组合。于是同一批人能用不同卡抓到,用户不知道该选哪个; 而想同时要两种来源时,单选约束又不允许——这条约束本身就是分类错了的信号。
每条都给证据,不是感觉。
| 卡片 | 卡内第一个开关 | 问题 |
|---|---|---|
| 脚本一 · 粉丝页提 ID | 「自定关注或粉丝页面提 ID」默认 ✓ | 和卡名重复 |
| 脚本二 · 评论页提 ID | 「自定评论页面提 ID」默认 ✓ | 和卡名重复 |
| 脚本三 · 文本 UID 裂变提 ID | 「文本 UID 裂变提 ID」默认 ✗(代码 true) | 与卡名一字不差 |
| 脚本四 · 关键词视频提 ID | 「关键词视频提 ID」默认 ✗(代码 true) | 与卡名一字不差 |
后果:用户已经通过单选圆点选了这张卡,再看一个"要不要启用"的开关,只能猜: 关掉它是不是等于不选这张卡?如果等于,为什么不直接取消勾选?如果不等于,那它到底关掉了什么? 执行侧也无法回答这个问题 —— 这正是语义缺失的直接体现。
脚本一里同时存在三条取材路径,靠 2 个开关区分,且没有定义"三个都开"或"都不开"的行为:
| 控件 | 隐含的起点 |
|---|---|
| 主播 ID(输入框)+ 进入页面(下拉) | 指定账号主页 |
| 自定页面提 ID(纯开关) | 搜索结果里的用户 |
| 使用文本 ID(开关 + 路径) | 文本文件里的 UID |
其中「自定页面提 ID」勾上后没有任何输入(page-id.js:126 只有 W.checkRow,无右侧控件)
—— 自定义哪个页面?执行时从哪开始?无解。
而「使用文本 ID」做的事与脚本三的「裂变文本」是同一件事(两者默认路径完全相同,都是
/sdcard/Download/tiktok_app/uid_list.txt)。
勾选后既没有输入、也没有选择,因此无法确定执行行为:
| 卡片 | 纯开关 |
|---|---|
| s1 | 自定关注或粉丝页面提 ID、自定页面提 ID |
| s2 | 自定评论页面提 ID、提取评论的人的粉丝 ID |
| s3 | 文本 UID 裂变提 ID、UID 博主去重 |
| s4 | 关键词视频提 ID、关键词提取主播粉丝 ID、关键词提取评论区 ID、关键词提取评论的人的粉丝 ID |
只有 s1 的「主播去重」是"开关 + 右侧下拉"的完整形态(buildToggleRow 的 rightOf)。
同一个项目里两种开关形态并存,用户无法从外观判断哪个开关后面还有东西。
| 位置 | 候选值 | 实际含义 |
|---|---|---|
s1 enterPage |
粉丝页 / 评论页 / 主页 / 关注页 | "去哪个页面取材" |
s3 enterPage |
同上(默认关注页) | "对文本里的 uid 展开哪个列表" |
同一个下拉名、同一套候选值、两套语义。用户在 s3 看到"进入页面=关注页", 会以为脚本要"进入某个关注页",实际是"取这些 uid 的关注列表"。
| 位置 | 控件 | 管什么? |
|---|---|---|
| s1 | 主播去重(开关)+ 去重模式(ID 模式/文本模式) | 主播?全部 ID? |
| s3 | UID 博主去重(纯开关) | 与上面是同一个东西吗? |
| 功能设置 | 云端去重(开关)+ 表名 + 连通测试 | 本地与云端是并存还是互斥? |
三处描述同一件事的不同侧面,分布在不同卡片与不同区域。"主播去重"这个词尤其含糊 —— 产物是 ID,不是主播;到底去重对象是"取材起点"还是"输出的每一个 ID"?
脚本三的卡名与开关都叫"文本 UID 裂变",但"裂变"至少可以指: ① 文本里的 uid → 取这些人的粉丝/关注(一层展开); ② 文本里是关键词 → 搜人 → 再展开; ③ 文本里的 uid → 直接产出(不展开)。 三种实现的工作量差几倍,而界面完全一样。
文案都以「关键词」开头、长度接近、控件形态相同:
关键词视频提 ID ✗
关键词提取主播粉丝 ID ✗
关键词提取评论区 ID ✗
关键词提取评论的人的粉丝 ID ✗
实际关系是:视频来源 → 作者 → 作者的粉丝 / 评论区 / 评论者的粉丝(第 1 个是根, 后 3 个是并列的可选扩展)。排成四项平级,用户读不出"1 是根,2/3/4 是扩展", 更读不出"2 和 3 是同一层,4 是更深一层"。
| 性质 | 项 | 问题 |
|---|---|---|
| 过滤条件(会丢人) | 私密、无头像、语言、区域、四组数量门槛 | 与"检测"混在一起 |
| 检测方式(决定怎么判) | 昵称个签检测(通杀/仅昵称/仅个签) | 它是前者的取样字段,不是独立设置 |
| 需要外部能力的开关 | 云端去重、AI 头像识别、AI 性别、AI 年龄 | 当前都没有承载者,却能打开 |
「提取区域」的说明文案更直接暴露问题:弹窗写"勾选后,只提取符合上方国家简码的账号"
(page-id.js:362),但这一行根本没有勾选框,上面是一个输入框。
| 字段 | 规格书 C3 | 代码 |
|---|---|---|
s3 uidFission |
✗ | true |
s4 keywordEnabled |
✗ | true |
代码里这么写是有理由的(都设 ✗ 的话,脚本三/四展开后整片是关的,用户会以为坏了), 但这恰好说明:这些开关本身就不该存在 —— 一个"默认必须打开、否则页面看起来是坏的"的开关, 是没有信息量的。
规格书写「单选:同一时间仅可运行一个脚本」。但用户的真实需求常常是组合的: "先抓关键词视频的作者,再把文本里这批 uid 的粉丝也抓一遍"。 当前结构下做不到,只能分两次运行、两次产出去手动合并。
规格书是从画布反推出的字段表:它准确记录了"这一行有一个复选框、默认 ✓", 但没有记录"勾上之后,脚本要做什么、不勾要做什么、和别的开关同时勾时听谁的"。
这就是为什么做执行方案时被迫猜了三处语义(主播 ID 留空 / customFansPage / 裂变)。
"界面乱"和"执行要猜"是同一个病。所以本次重整的主产出不是排版,
而是给每个控件补上唯一的行为定义。
真正的两个维度是:
把四张卡拆到这两个维度上:
| 旧卡 | 起点 | 深度 | 它缺的能力(被塞在别处了) |
|---|---|---|---|
| s1 粉丝页 | 指定账号主页 | 1 层(列表) | 缺"搜索用户"(塞在 s1 自己的 customPage)、缺"文本"(塞在 useTextId) |
| s2 评论页 | 某视频评论区 | 0~1 层 | 缺"视频从哪来" —— 只有"自定评论页面"= 假定用户已停在评论区 |
| s3 文本 | 文本文件 | 1 层 | 缺"直接用文本里的 uid"(不展开) |
| s4 关键词视频 | 搜索视频 | 0~2 层(3 种扩展) | 缺"搜索用户"起点 |
四张卡各缺一块、各有别人没有的一块,合起来才完整。 所以"选一个脚本"这个动作本身就是在替用户做减法,而用户往往不想减。
任何"ID 提取"工具都只有三步。把结构改成这三步,四个脚本自然消失。
① 取材 从哪儿找人 → 可累加的取材清单(1 条或多条,串行执行)
② 过滤 只要什么样的人 → 一组共用条件(所有取材都适用)
③ 输出 要多少、存哪 → 上限 + 去重 + 文件
用户只需回答两件事:从哪开始、要不要再往下走一层。
| # | 起点 | 用户要提供 | 能取到 | 能否再展开一层 |
|---|---|---|---|---|
| A | 指定账号 | 账号(多个用 # 分隔) |
粉丝列表 / 关注列表 / 主页展示的账号 | ✅ 展开后可再取粉丝/关注 |
| B | 搜索结果 | 类型(用户 / 视频)+ 关键词 | 用户搜索结果里的人 / 视频的作者 | ✅ 同上 |
| C | 某个视频的评论区 | 视频从哪来(当前屏幕 / 关键词搜到的) | 评论者 | ✅ 取评论者的粉丝 |
| D | 文本文件 | 文件路径 | 文件里的每一行 uid | ✅ 取这些人的粉丝/关注 |
| E | 当前屏幕 | 无 | 屏幕上这个列表里的人 | ❌ 已是最底 |
规则(必须写进需求,否则又会含糊):
改结构不等于丢掉用户熟悉的用法。三个常见场景在新结构下的写法:
| 用户想干的 | 旧操作 | 新操作 |
|---|---|---|
| 抓某个主播的粉丝 | 脚本一(默认就是) | 一条取材:指定账号 xxx → 取「粉丝列表」 |
| 抓关键词视频的作者的粉丝 | 脚本四 | 一条取材:搜索结果「视频 + 关键词」→ 取「作者」→ 深度「作者粉丝」 |
| 把文本里这批 uid 的粉丝抓一遍 | 脚本三 | 一条取材:文本文件 → 深度「这些人的粉丝」 |
并且多了一个旧结构做不到的能力:以上三条可以同时配(三条取材串行跑,共用一个输出)。
A 组 · 只要这样的账号
| 项 | 执行语义(必须唯一) | 现状定性 |
|---|---|---|
| 过滤私密账户 | 读不到"私密"提示 → 保留;读到 → 丢 | 可做 |
| 过滤无头像账户 | 头像是默认占位图 → 丢 | 可做(需像素判定) |
| 语言 | 取昵称 + 个签的字符组成:中文占比 ≥ 阈值 → 中文;拉丁 ≥ 阈值 → 英文;都不满足 → 丢 | 可做(纯本地) |
| 昵称个签检测 | 不是一个过滤条件,是上面"语言"的取样范围:通杀 / 只看昵称 / 只看个签 | 需改名并归到"语言"下面 |
| 提取区域 | TikTok 用户页不显示国家,无可用的判定数据源 | 标「暂不支持」,不给可开的开关 |
| AI 头像识别 / AI 性别 / AI 年龄 | 需要模型,本期无承载者 | 标「暂不支持」 |
B 组 · 数值门槛
| 项 | 执行语义 |
|---|---|
| 关注量 / 粉丝量 / 获赞量 / 视频量(各 min~max) | 读用户页数字(1.2M / 12.3K / 1.2万 都要能解析)→ 落在区间内才保留 |
C 组 · 去重与输出(从脚本卡里全部收上来,只出现一次)
| 项 | 执行语义 |
|---|---|
| 本地去重 | 与"历史已提取"(跨轮)+ "本次已收"(同轮)比对,重复的跳过。默认开 |
| 云端去重 | 未配置服务地址时不生效,并在日志里明说「本次仅本地去重」,不静默 |
| 提取上限 | 凑满 N 个即结束。0 = 不限,但受引擎的物理护栏约束(最大时长 / 最大翻屏数),到护栏时如实说明 |
关键规则:"暂不支持"的项必须是灰的并带一个 ⓘ 说明原因,不能给一个能打开、 打开后什么也不发生的开关。现在的「云端去重」就是这样:开关能开,但"连通测试"只会告诉你 "当前未配置服务地址"。
┌─ ID提取 ────────────────────────────────────┐
│ │
│ ① 取材 │
│ 说明:可添加多条,按顺序执行;共用同一份去重与输出 │
│ │
│ ┌ 第 1 条 ─────────────────────────────┐ │
│ │ 起点 [指定账号 ▾] │ │
│ │ 账号 [ID1#ID2#ID3 ] │ │
│ │ 取 ☑ 粉丝列表 ☑ 关注列表 ☐ 主页展示 │ │
│ │ 深度 ☐ 再取这些人的粉丝(最多 2 层) │ │
│ └──────────────────────────────────────┘ │
│ ┌ 第 2 条 ─────────────────────────────┐ │
│ │ 起点 [文本文件 ▾] │ │
│ │ 文件 [/sdcard/Download/tiktok_app/uid_list.txt] [选择] │
│ │ 深度 ☑ 再取这些人的粉丝 │ │
│ └──────────────────────────────────────┘ │
│ [+ 添加一条取材] │
│ │
│ ② 只要这样的账号 │
│ ☑ 过滤私密 ☑ 过滤无头像 │
│ 语言 [通杀 ▾] 取样 [昵称+个签 ▾] │
│ 区域 [暂不支持 ⓘ] │
│ 关注量 [0]~[500000] 粉丝量 [0]~[500000] │
│ 获赞量 [0]~[500000] 视频量 [0]~[500000] │
│ │
│ ③ 输出 │
│ 提取满 [0=不限] 个后停止 │
│ 去重 ☑ 本地 ☐ 云端(需先在「我的」配置服务) │
│ 文件:uid_20261017_1100.txt(自动命名) │
└──────────────────────────────────────────────┘
与旧页面相比去掉的东西(都是本次诊断出来的问题源):
| 去掉 | 原因 |
|---|---|
| 四张"脚本"卡 + 单选圆点 | 起点与深度是两个维度,不该做成 4 个互斥选项(§2.2) |
| 「单选:同一时间仅可运行一个脚本」说明 | 约束随结构消失:取材是串行清单,本来就只有一个在跑 |
| 4 个同义反复的开关 | §1.1 |
| 6 个"勾上却无所指"的纯开关 | §1.3 —— 全部变成"选了起点才出现的输入" |
| 脚本卡里的三个去重控件 | 收到 ③ 只出现一次(§1.5) |
| 「↓ 下方还有功能设置」滚动指引 | 页面从"4 卡 + 长设置"变成三段,一屏半就能看完 |
| 「当前脚本:xxx」提示条 | 没有"当前脚本"这个概念了 |
这三条是本次重整最实质的产出,不是排版能替代的:
| # | 旧路径 | 处置 | 新归属 |
|---|---|---|---|
| 1 | s1.customFansPage | 删 | 被「起点=指定账号 + 取哪个列表」取代 |
| 2 | s1.customPage | 删 | 它是"起点=搜索结果·用户",独立成一条取材 |
| 3 | s1.dedupeEnabled | 移 | ③ 输出 · 本地去重 |
| 4 | s1.dedupe | 移 + 简化 | 与 UID 博主去重合并;"ID 模式/文本模式"的差别只是集合来源,不必单独让用户选 |
| 5 | s1.enterPage | 改名 + 收窄 | ① 取材 · 「取哪些列表」(多选:粉丝/关注/主页展示),只在起点=指定账号时出现 |
| 6 | s1.anchorIds | 保留 + 改名 | ① 取材 · 「账号」 |
| 7 | s1.useTextId | 删(合并) | 选「文本文件」这个起点即等于要用文本,不需要再确认一次 |
| 8 | s1.textIdPath | 保留 + 改名 | ① 取材 · 「文件」(与 s3 是同一个概念,只留一份) |
| 9 | s2.customCommentPage | 删 | 起点=评论区的输入改为「视频从哪来」 |
| 10 | s2.commenterFans | 合并 | ① 取材 · 深度「再取这些人的粉丝」 |
| 11 | s3.uidFission | 删 | 同义反复(§1.1) |
| 12 | s3.uidDedupe | 删(合并) | 统一去重 |
| 13 | s3.enterPage | 改名 | ① 取材 · 深度里的「展开哪个列表」(粉丝/关注)—— 与 #5 分名,避免同名不同义 |
| 14 | s3.fissionTextPath | 合并 | 与 #8 合并为同一个「文件」 |
| 15 | s4.keywordEnabled | 删 | 同义反复 |
| 16 | s4.keyword | 保留 + 改名 | ① 取材 · 「关键词」 |
| 17 | s4.anchorFans | 合并 | ① 取材 · 深度里的「作者粉丝」 |
| 18 | s4.commentArea | 保留 | ① 取材 · 与「作者粉丝」并列的一个取材范围 |
| 19 | s4.commenterFans | 合并 | ① 取材 · 深度的第 2 层「评论者的粉丝」 |
| 20 | features.filterPrivate | 保留 | ② 只要这样的账号 |
| 21 | features.filterNoAvatar | 保留 | ② |
| 22 | features.regionCodes | 保留但置灰 | ② · 标「暂不支持」+ ⓘ 说明原因 |
| 23 | features.langRegion | 保留 | ② |
| 24 | features.nicknameCheck | 改名 + 归属 | ② · 「取样范围」(通杀/昵称/个签),挂在语言下面 |
| 25 | features.cloudDedupe | 保留 | ③ 去重 |
| 26 | features.cloudTable | 保留 | ③ 去重(选云端后才出现) |
| 27 | features.aiAvatar | 保留但置灰 | ② · 标「暂不支持」 |
| 28 | features.aiGender | 保留但置灰 | ②(依赖 #27) |
| 29 | features.aiAgeMax | 保留但置灰 | ②(依赖 #27) |
| 30-37 | features.{follow,fans,likes,videos}{Min,Max} | 保留 | ② 数值门槛(8 个,一行一个区间) |
| 38 | features.extractLimit | 保留 | ③ 输出 |
统计:删 5、合并 7、改名 6、置灰 3、原样保留 17(= 38)。 没有一个字段是"想不出来才删的" —— 每一条删/合并都对应 §1 里的一个具体问题。
保留四张卡、单选、功能设置卡。做 6 件事:
enterPage 分名:s1「取哪些列表」、s3「展开哪个列表」(§1.4)page-id.js + store.js 的文案与少量字段;配置迁移小;与规格书差距小。按 §3 重做页面结构:取材清单 + 过滤 + 输出。
Store.FIELDS 那套"一项一控件"的标量表放不下,
要新建一个 Sources 模块(照 Records 的先例:另一个 storage key,不塞进字段表);
③ 要做 v6 → v7 迁移(把四张卡的 checked 翻译成"一条取材",我已有完整的映射方案)。A 档的 6 件事里有 4 件是 B 档的前置工作(删同义开关、补纯开关的输入、去重收口、置灰标注), 做了不浪费。但如果只能做一档,选 B —— A 档做完,用户最不满的那条("想同时抓两种来源") 仍然无解。
现有用户配置按"当时勾的是哪张卡"翻译成一条取材,一个字段都不丢:
| 旧状态 | 新配置 |
|---|---|
| s1 选中 | 取材 #1:起点=指定账号,账号=anchorIds,取=enterPage 对应的列表 |
s1 选中 且 useTextId=true |
取材 #1:起点=文本文件,文件=textIdPath |
s1 选中 且 customPage=true |
取材 #1:起点=搜索结果·用户(关键词留空,启动时要求补) |
| s2 选中 | 取材 #1:起点=评论区,视频来源=当前屏幕 |
| s3 选中 | 取材 #1:起点=文本文件,文件=fissionTextPath,深度=enterPage |
| s4 选中 | 取材 #1:起点=搜索结果·视频,关键词=keyword;深度按三个开关逐项映射 |
dedupeEnabled / uidDedupe / 云端三项 |
统一映射到 ③ 的去重 |
| 四张卡都未选 | 取材清单为空 → 启动时拦住「请先添加一条取材」(与现在的 F1 弹窗同形) |
迁移必须幂等(跑两次结果一样)且可回退(旧字段保留不删,只做读取时翻译)。
PLAN_id_extract.md(执行方案)的关系两份文档是互相印证的,不是重复:
customFansPage / 裂变),
在本文 §3 里全部变成明确的行为定义 —— 两处文档要么一起改,要么一起不改。若采纳 B 档,执行方案的哪些结论会变:
| 执行方案里的内容 | B 档下是否需要改 |
|---|---|
| 6 种取材源抽象 | 不变,它已经是三段式的直接映射 |
engine-config.json 的 id.source 单对象 |
要改:变成 id.sources 数组 |
| 14 项功能设置的分级(A/B/C) | 不变 |
产物与去重(uid_*.txt + uid_extracted.txt) |
不变 |
| 分阶段实施 P0–P5 | 不变,但 P3 的"只做脚本一"改为"只做一条取材(指定账号)" |
| 界面侧 6 处改动 | 要改:buildIdConfig 从读 4 张卡变成读取材清单 |
旧(四张卡 × 互斥) 新(三段式 × 可累加)
───────────────────────── ─────────────────────────
脚本一 · 粉丝页提 ID ┐
├ 自定关注或粉丝页面 ✓ │ ① 取材(可多条,串行)
├ 自定页面 ✗ │ 起点[指定账号] 账号[__] 取☑粉丝☑关注
├ 主播去重 ✓ + 模式 │ 起点[文本文件] 文件[__] 深度☑粉丝
├ 进入页面=粉丝页 │ 起点[搜索视频] 关键词[__] 深度☑作者☑评论区
├ 主播 ID [__] │
└ 使用文本 ID ✗ + 路径 ┘ ② 只要这样的账号
脚本二 · 评论页提 ID ☑私密 ☑无头像 语言[通杀] 取样[昵称+个签]
├ 自定评论页面 ✓ ┐ 区域[暂不支持ⓘ]
└ 提取评论的人的粉丝 ✗ │ 关注量[0]~[500000] 粉丝量[0]~[500000]
脚本三 · 文本 UID 裂变 │ 获赞量[0]~[500000] 视频量[0]~[500000]
├ 文本 UID 裂变提 ID ✗ │
├ UID 博主去重 ✓ │ ③ 输出
├ 进入页面=关注页 │ 提取满[0]个后停止
└ 裂变文本 [__] │ 去重 ☑本地 ☐云端
脚本四 · 关键词视频 │ 文件 uid_20261017_1100.txt(自动)
├ 关键词视频提 ID ✗ │
├ 关键词主播 [fashion] │ ← 10 个纯开关 → 0 个
├ 关键词提取主播粉丝 ✗ │ ← 3 处去重 → 1 处
├ 关键词提取评论区 ✗ │ ← 2 个同名不同义的"进入页面" → 各自分名
└ 关键词提取评论的人的粉丝 ✗ ┘ ← 单选约束 → 消失(本来就该串行)
─────────────────────────
功能设置 14 项(混了过滤/检测/待支持三种性质)