|
|
@@ -1,5 +1,5 @@
|
|
|
/*! tiktok-auto v1.0.0
|
|
|
- * built 2026-10-21T14:19:52.518Z
|
|
|
+ * built 2026-10-21T15:03:41.961Z
|
|
|
* modules: logger, bridge, config, wait, layout, ui, popups, tiktok, player, comment, flow, parse, uidset, filters, profile, idsource, resolve, idflow, main
|
|
|
* 由 tools/build.js 自动生成 —— 请不要直接编辑本文件,改 src/ 后重新构建。
|
|
|
*/
|
|
|
@@ -4368,6 +4368,18 @@ TikTokAuto.profile = (function () {
|
|
|
var current = tiktok.statNumbers();
|
|
|
if (current.follow === previous.follow && current.fans === previous.fans &&
|
|
|
current.likes === previous.likes) {
|
|
|
+ /* 三组全是 0 是**可疑值**:既可能是真新号,也可能是占位还没换掉。
|
|
|
+ * 多等 1.5 秒复核一次 —— 真新号复核后还是 0,不影响结果;
|
|
|
+ * 占位的话这一刻通常已经填上真值了(实测把 2019 粉丝读成 0 过)。 */
|
|
|
+ if (current.follow === 0 && current.fans === 0 && current.likes === 0) {
|
|
|
+ TikTokAuto.wait.tick(1500);
|
|
|
+ var recheck = tiktok.statNumbers();
|
|
|
+ if (recheck.follow !== 0 || recheck.fans !== 0 || recheck.likes !== 0) {
|
|
|
+ logger().info('profile', '三组全 0 复核后拿到真值:' +
|
|
|
+ recheck.follow + '/' + recheck.fans + '/' + recheck.likes);
|
|
|
+ return recheck;
|
|
|
+ }
|
|
|
+ }
|
|
|
return current;
|
|
|
}
|
|
|
previous = current;
|
|
|
@@ -4705,12 +4717,26 @@ TikTokAuto.profile = (function () {
|
|
|
function openList(which, config) {
|
|
|
var step = 'openList';
|
|
|
var tiktok = TikTokAuto.tiktok;
|
|
|
- var entry = tiktok.statEntry(which === 'following' ? 'following' : 'fans');
|
|
|
+ var wanted = which === 'following' ? 'following' : 'fans';
|
|
|
+
|
|
|
+ var entry = tiktok.statEntry(wanted);
|
|
|
if (!entry) {
|
|
|
- logger().warn(step, '用户主页上找不到 ' + which + ' 入口');
|
|
|
+ /* ★ 找不到入口时**再等一次**,不要立刻报失败。
|
|
|
+ * 实测(用户 2026-10-21 反馈):从搜索结果进主页后,标题已经出来了,
|
|
|
+ * 但数字区还在渲染,这时 `statEntry()` 返回 null,直接判"找不到 fans 入口",
|
|
|
+ * 用户看到的现象就是"还没加载出来就已经返回了"。重跑往往就正常 —— 典型的时序问题。
|
|
|
+ * 这里最多等 3 秒,够渲染完;真没有(比如私密号不显示列表)就如实失败。 */
|
|
|
+ logger().info(step, '暂时找不到 ' + wanted + ' 入口,等页面渲染完再试');
|
|
|
+ TikTokAuto.wait.waitFor(step + ':entry', function () {
|
|
|
+ entry = tiktok.statEntry(wanted);
|
|
|
+ return entry !== null ? true : null;
|
|
|
+ }, 3000, 300);
|
|
|
+ }
|
|
|
+ if (!entry) {
|
|
|
+ logger().warn(step, '用户主页上找不到 ' + wanted + ' 入口(等了 3 秒仍没有)');
|
|
|
return false;
|
|
|
}
|
|
|
- logger().step(step, '打开列表:' + which);
|
|
|
+ logger().step(step, '打开列表:' + wanted);
|
|
|
TikTokAuto.ui.clickNode(entry, step);
|
|
|
|
|
|
var ready = TikTokAuto.wait.waitFor(step, function () {
|
|
|
@@ -4880,10 +4906,49 @@ TikTokAuto.profile = (function () {
|
|
|
* 实测后果:一个粉丝数为 0 的账号被当成"读不到资料",或被误判过滤。
|
|
|
* 判据必须是"@username 节点已经存在"。
|
|
|
*/
|
|
|
+ /**
|
|
|
+ * 等用户主页**真的渲染完**,而不是"标题出来了就算到"。
|
|
|
+ *
|
|
|
+ * ★ 两段等待,缺一不可(实测踩出来的):
|
|
|
+ *
|
|
|
+ * ① 等 `@username` 出现 —— 这一步很快(约 585ms),因为标题先渲染;
|
|
|
+ * ② 再等顶部三组数字(关注/粉丝/获赞)**至少读到两个**。
|
|
|
+ *
|
|
|
+ * 只做 ① 会出两类问题,而且都很难查:
|
|
|
+ * · 数字区还是占位的 0 → 把"粉丝 78"读成"粉丝 0"→ 按数量门槛被错误跳过;
|
|
|
+ * · 数字区根本没渲染 → `statEntry()` 找不到"粉丝/关注"那个可点入口
|
|
|
+ * → 报"用户主页上找不到 fans 入口"(用户看到的就是"还没加载出来就返回了")。
|
|
|
+ *
|
|
|
+ * 第 ② 段是**尽力等**:有些账号(私密号、无数据的新号)可能只有一组数字,
|
|
|
+ * 所以超时也照常返回 true —— 不能因为等不到三组就把正常账号判成失败。
|
|
|
+ */
|
|
|
function waitProfile(step) {
|
|
|
- return TikTokAuto.wait.waitFor(step + ':profile', function () {
|
|
|
- return TikTokAuto.tiktok.profileUsernameNode() !== null ? true : null;
|
|
|
- }, 10000, 300) === true;
|
|
|
+ var tiktok = TikTokAuto.tiktok;
|
|
|
+ var arrived = TikTokAuto.wait.waitFor(step + ':profile', function () {
|
|
|
+ return tiktok.profileUsernameNode() !== null ? true : null;
|
|
|
+ }, 10000, 300);
|
|
|
+ if (arrived !== true) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ /* 第二段:数字区。**三组都要读到**,10 秒上限。
|
|
|
+ *
|
|
|
+ * 为什么要求三组而不是两组:TikTok 会先把数字渲染成占位的 0,
|
|
|
+ * 再逐组换成真值。要求"两组"时,`粉丝 0 / 关注 0 / 获赞 null` 这种
|
|
|
+ * 半渲染状态就算通过了 —— 实测把 @.1523615 的 2019 粉丝读成了 0,
|
|
|
+ * 而这类错误会直接影响数量门槛的判定(读成 0 就会被"粉丝≥1000"筛掉)。 */
|
|
|
+ var statsReady = TikTokAuto.wait.waitFor(step + ':stats', function () {
|
|
|
+ var stats = tiktok.statNumbers();
|
|
|
+ return (stats.follow !== null && stats.fans !== null && stats.likes !== null)
|
|
|
+ ? true : null;
|
|
|
+ }, 10000, 300);
|
|
|
+ if (statsReady !== true) {
|
|
|
+ /* 不是错误:确实有账号不显示某一组(新号可能没有获赞)。
|
|
|
+ * 但要在日志里留痕,因为后面的"读不到数量"多半是这里没等到。 */
|
|
|
+ var last = tiktok.statNumbers();
|
|
|
+ logger().info('profile', '三组数字 10 秒内没读全(关注 ' + last.follow +
|
|
|
+ ' / 粉丝 ' + last.fans + ' / 获赞 ' + last.likes + '),继续');
|
|
|
+ }
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -6012,6 +6077,46 @@ TikTokAuto.idflow = (function () {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 估算"够用的抓取预算"。
|
|
|
+ *
|
|
|
+ * ## 为什么要自动算
|
|
|
+ * 预算是**按时间**给的,而用户的目标是**按个数**("提取满 100 个")。
|
|
|
+ * 两者之间要经过"每个的平均耗时",而这个耗时取决于过滤设置(差 6 倍):
|
|
|
+ * · 不过滤 → 直接从列表抄 uid,约 1.5 秒/个;
|
|
|
+ * · 开了私密/无头像/数量门槛 → 每个都要打开用户页读资料,约 8 秒/个;
|
|
|
+ * · 再加上被过滤跳过的(实测收录率约 37%)→ 折算到"每个**收录**"约 20 秒。
|
|
|
+ *
|
|
|
+ * 所以固定给 30 分钟是错的:设 100 个要 38 分钟、设 1000 个要 5 个多小时。
|
|
|
+ * 用户设的数只当**下限**("至少跑这么久"),实际按提取上限放宽。
|
|
|
+ *
|
|
|
+ * ## 为什么还要封顶
|
|
|
+ * 4 小时是设备安全的边界(长时间亮屏运行会发热)。真要更多,分几轮跑更好 ——
|
|
|
+ * 每轮都刷盘,中断不丢,还能跨轮去重。
|
|
|
+ */
|
|
|
+ var PER_RESULT_MS = 20000; /* 折算到每个"收录"的耗时 */
|
|
|
+ var FIXED_OVERHEAD_MS = 5 * 60 * 1000; /* 启动 app / 搜索 / 开列表的固定开销 */
|
|
|
+ var HARD_CAP_MS = 4 * 60 * 60 * 1000; /* 4 小时硬顶 */
|
|
|
+
|
|
|
+ /** 解析 uid 的预算:按实际待解析个数算(每个约 3 秒 + 1 分钟缓冲),1~60 分钟 */
|
|
|
+ var PER_RESOLVE_MS = 3000;
|
|
|
+ var RESOLVE_CAP_MS = 60 * 60 * 1000;
|
|
|
+
|
|
|
+ function budgetFor(limit, userMinMs) {
|
|
|
+ var userMs = Math.max(1, userMinMs);
|
|
|
+ if (limit <= 0) {
|
|
|
+ /* 不限个数 → 没有依据可算,按用户设的走 */
|
|
|
+ return Math.min(userMs, HARD_CAP_MS);
|
|
|
+ }
|
|
|
+ var autoMs = limit * PER_RESULT_MS + FIXED_OVERHEAD_MS;
|
|
|
+ return Math.min(Math.max(userMs, autoMs), HARD_CAP_MS);
|
|
|
+ }
|
|
|
+
|
|
|
+ function resolveBudgetFor(count) {
|
|
|
+ var need = count * PER_RESOLVE_MS + 60000;
|
|
|
+ return Math.min(Math.max(10 * 60 * 1000, need), RESOLVE_CAP_MS);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 把界面配置里的 `id` 段收拢成引擎侧要用的形状。
|
|
|
*
|
|
|
@@ -6024,6 +6129,8 @@ TikTokAuto.idflow = (function () {
|
|
|
var filter = id.filter || {};
|
|
|
var output = id.output || {};
|
|
|
var guard = id.guard || {};
|
|
|
+ var limit = Number(output.limit) || 0;
|
|
|
+ var userMaxMs = Number(guard.maxRuntimeMs) || 30 * 60 * 1000;
|
|
|
|
|
|
return {
|
|
|
sources: (id.sources instanceof Array ? id.sources : []),
|
|
|
@@ -6045,10 +6152,11 @@ TikTokAuto.idflow = (function () {
|
|
|
/* 缺省开:数字 uid 在界面上读不到,不在这里换就得事后另跑一次 */
|
|
|
resolveUid: output.resolveUid !== false
|
|
|
},
|
|
|
- limit: Number(output.limit) || 0,
|
|
|
- maxRuntimeMs: Number(guard.maxRuntimeMs) || 30 * 60 * 1000,
|
|
|
- /* 解析 uid 的独立预算:默认 10 分钟 ≈ 240 个(每个约 2.5 秒)。
|
|
|
- * 与抓取预算分开,因为它们是先后发生的两件事(见下)。 */
|
|
|
+ limit: limit,
|
|
|
+ /** 用户设的(下限,界面「单次最长运行」) */
|
|
|
+ userMaxRuntimeMs: userMaxMs,
|
|
|
+ /** 实际生效的抓取预算(按 limit 自动放宽) */
|
|
|
+ maxRuntimeMs: budgetFor(limit, userMaxMs),
|
|
|
resolveMaxMs: Number(guard.resolveMaxMs) || 10 * 60 * 1000
|
|
|
};
|
|
|
}
|
|
|
@@ -6174,6 +6282,9 @@ TikTokAuto.idflow = (function () {
|
|
|
|
|
|
log.step('idflow', '开始解析数字 uid:' + names.length + ' 个(每个约 2~3 秒)');
|
|
|
var resolveStarted = Date.now();
|
|
|
+ /* 解析预算也按实际个数算:10 分钟只够 240 个,抓了 1000 个就不够用了 */
|
|
|
+ var resolveBudgetMs = resolveBudgetFor(names.length);
|
|
|
+ log.info('idflow', '解析预算 ' + Math.round(resolveBudgetMs / 60000) + ' 分钟');
|
|
|
var result = TikTokAuto.resolve.resolveAll(names, {
|
|
|
/**
|
|
|
* ★ 这里**只用自己的预算**,不继承抓取阶段的任何停止条件。
|
|
|
@@ -6188,7 +6299,7 @@ TikTokAuto.idflow = (function () {
|
|
|
* 用户主动停止仍然有效 —— 那是 StopSignal 从 `wait.tick` 抛出来的。
|
|
|
*/
|
|
|
shouldStop: function () {
|
|
|
- return Date.now() - resolveStarted >= settings.resolveMaxMs;
|
|
|
+ return Date.now() - resolveStarted >= resolveBudgetMs;
|
|
|
},
|
|
|
onProgress: function (done, total) {
|
|
|
progress('解析 uid ' + done + '/' + total, target.extracted);
|
|
|
@@ -6197,7 +6308,7 @@ TikTokAuto.idflow = (function () {
|
|
|
|
|
|
if (result.stopped) {
|
|
|
target.warnings.push('解析 uid 到达自身时长上限(' +
|
|
|
- Math.round(settings.resolveMaxMs / 60000) + ' 分钟),只换到 ' +
|
|
|
+ Math.round(resolveBudgetMs / 60000) + ' 分钟),只换到 ' +
|
|
|
result.map.length + '/' + names.length + ' 个');
|
|
|
}
|
|
|
|
|
|
@@ -6265,6 +6376,13 @@ TikTokAuto.idflow = (function () {
|
|
|
log.step('idflow', '=== TikTok ID 提取开始 ===');
|
|
|
log.info('idflow', '取材 ' + settings.sources.length + ' 条,过滤:' +
|
|
|
TikTokAuto.filters.describe(settings.filter));
|
|
|
+ /* 把预算说清楚:用户设的是下限,实际按提取上限放宽过。
|
|
|
+ * 不说的话,"怎么跑了 38 分钟"会变成第二个疑问。 */
|
|
|
+ log.info('idflow', '抓取预算 ' + Math.round(settings.maxRuntimeMs / 60000) + ' 分钟' +
|
|
|
+ (settings.limit > 0
|
|
|
+ ? '(按提取上限 ' + settings.limit + ' 个估算;你设的下限是 ' +
|
|
|
+ Math.round(settings.userMaxRuntimeMs / 60000) + ' 分钟)'
|
|
|
+ : '(未设提取上限,按你设的时长)'));
|
|
|
|
|
|
TikTokAuto.bridge.report({ status: 'running', phase: '准备', done: 0, total: 0 });
|
|
|
|